Skip to content

Commit

Permalink
Parallelism (#3)
Browse files Browse the repository at this point in the history
* Render to an intermediate canvas

* Make the "world" threadsafe

* Parallelize via Weave

* Properly free the canvas
  • Loading branch information
mratsim authored May 23, 2020
1 parent 9473cbe commit 26d434e
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 46 deletions.
24 changes: 14 additions & 10 deletions trace_of_radiance.nim
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,25 @@
# at your option. This file may not be copied, modified, or distributed except according to those terms.

import
# Stdlib
std/strformat,
# Internals
./trace_of_radiance/[
primitives,
physics/cameras,
physics/hittables,
render,
scenes,
sampling
sampling,
io/ppm
]

proc main() =
const aspect_ratio = 16.0 / 9.0
const image_width = 384
const image_height = int(image_width / aspect_ratio)
const samples_per_pixel = 100
const gamma_correction = 2.2
const max_depth = 50

stdout.write &"P3\n{image_width} {image_height}\n255\n"

var worldRNG: Rng
worldRNG.seed 0xFACADE
let world = worldRNG.random_scene()
Expand All @@ -44,12 +43,17 @@ proc main() =
aspect_ratio, aperture, dist_to_focus
)

stdout.renderToPPM(
cam, world,
image_height, image_width,
samples_per_pixel, max_depth
)
var canvas = newCanvas(
image_height, image_width,
samples_per_pixel,
gamma_correction
)
defer: canvas.delete()

canvas.render(cam, world.list(), max_depth)

stdout.exportToPPM canvas
stderr.write "\nDone.\n"


main()
25 changes: 11 additions & 14 deletions trace_of_radiance/io/ppm.nim
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,17 @@ import
# internal
../primitives

proc write*(f: File, pixelColor: Color, samples_per_pixel: int) =
var r = pixelColor.x
var g = pixelColor.y
var b = pixelColor.z

# Divide the color total by the number of samples
# We also do gamma correction for gamma = 2, i.e. pixel^(1/2)
let scale = 1.0 / float64(samples_per_pixel)
r = sqrt(scale * r)
g = sqrt(scale * g)
b = sqrt(scale * b)

proc exportToPPM*(f: File, canvas: Canvas) =
template conv(c: float64): int =
int(256 * clamp(c, 0.0, 0.999))

# Write the translated [0, 255] value of each color component
f.write &"{conv(r)} {conv(g)} {conv(b)}\n"
stdout.write &"P3\n{canvas.ncols} {canvas.nrows}\n255\n"

for i in countdown(canvas.nrows-1,0):
for j in 0 ..< canvas.ncols:
# Write the translated [0, 255] value of each color component
let pixel = canvas[i, j]
let r = pixel.x
let g = pixel.y
let b = pixel.z
f.write &"{conv(r)} {conv(g)} {conv(b)}\n"
32 changes: 26 additions & 6 deletions trace_of_radiance/physics/hittables/hittables_lists.nim
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,43 @@ import
../../primitives

type
HittableList* = object
Scene* = object
## A list of hittable objects.
## ⚠ not thread-safe
objects: seq[HittableVariant]

func add*(self: var HittableList, h: HittableVariant) {.inline.} =
HittableList* = object
## TODO openarray as value
## ⚠: lifetime
len: int
objects: ptr UncheckedArray[HittableVariant]

# Mutable routines
# ---------------------------------------------------------

func add*(self: var Scene, h: HittableVariant) {.inline.} =
self.objects.add h

func add*[T](self: var HittableList, h: T) {.inline.} =
func add*[T](self: var Scene, h: T) {.inline.} =
self.objects.add h.toVariant()

func clear*(self: var HittableList) {.inline.} =
func clear*(self: var Scene) {.inline.} =
self.objects.setLen(0)

# Immutable routines
# ---------------------------------------------------------

func list*(scene: Scene): HittableList {.inline.} =
assert scene.objects.len > 0
result.len = scene.objects.len
result.objects = cast[ptr UncheckedArray[HittableVariant]](
scene.objects[0].unsafeAddr
)

func hit*(self: HittableList, r: Ray, t_min, t_max: float64, rec: var HitRecord): bool =
var closest_so_far = t_max

# TODO: perf issue: https://github.com/nim-lang/Nim/issues/14421
for i in 0 ..< self.objects.len:
for i in 0 ..< self.len:
let hit = self.objects[i].hit(r, t_min, closest_so_far, rec)
if hit:
closest_so_far = rec.t
Expand Down
6 changes: 4 additions & 2 deletions trace_of_radiance/primitives.nim
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@

import primitives/[
safe_math, rays,
vec3s, point3s, colors
vec3s, point3s, colors,
canvas
]

export
safe_math, rays,
vec3s, point3s, colors
vec3s, point3s, colors,
canvas
57 changes: 57 additions & 0 deletions trace_of_radiance/primitives/canvas.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Trace of Radiance
# Copyright (c) 2020 Mamy André-Ratsimbazafy
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at http://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at http://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.

import
# Low-level
system/ansi_c,
# Stdlib
std/math,
# Internal
./colors

# Canvas
# ------------------------------------------------------------------------
# A Canvas is a 2D Buffer

type
Canvas* = object
## 2D Buffer
## Images are stored in row-major order
# Size 24 bytes
pixels*: ptr UncheckedArray[Color]
nrows*, ncols*: int32
samples_per_pixel*: int32
gamma_correction*: float32


proc newCanvas*(
height, width,
samples_per_pixel: SomeInteger,
gamma_correction: SomeFloat): Canvas =
result.nrows = int32 height
result.ncols = int32 width
result.samples_per_pixel = int32 samples_per_pixel
result.pixels = cast[ptr UncheckedArray[Color]](
c_malloc(csize_t height * width * sizeof(Color))
)
result.gamma_correction = float32(gamma_correction)

proc delete*(canvas: var Canvas) {.inline.} =
if not canvas.pixels.isNil:
c_free(canvas.pixels)

func draw*(canvas: var Canvas, row, col: SomeInteger, pixel: Color) {.inline.} =
# Draw a gamma-corrected pixel in the canvas
let scale = 1.0 / canvas.samples_per_pixel.float64
let gamma = 1.0 / canvas.gamma_correction.float64
let pos = row*canvas.ncols + col
canvas.pixels[pos].x = pow(scale * pixel.x, gamma)
canvas.pixels[pos].y = pow(scale * pixel.y, gamma)
canvas.pixels[pos].z = pow(scale * pixel.z, gamma)

proc `[]`*(canvas: Canvas, row, col: SomeInteger): Color {.inline.} =
canvas.pixels[row*canvas.ncols + col]
37 changes: 24 additions & 13 deletions trace_of_radiance/render.nim
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
# at your option. This file may not be copied, modified, or distributed except according to those terms.

import
std/strformat,
# Standard library

# 3rd party
weave,
# Internal
./primitives,
./sampling,
./io/ppm,
Expand Down Expand Up @@ -43,18 +47,25 @@ func radiance*(ray: Ray, world: Hittable, max_depth: int, rng: var Rng): Color =

return color(0, 0, 0)

proc renderToPPM*(output: File, cam: Camera, world: HittableList,
image_height, image_width, samples_per_pixel, max_depth: int) =
for j in countdown(image_height-1, 0):
stderr.write &"\rScanlines remaining: {j} "
stderr.flushFile()
for i in 0 ..< image_width:
proc render*(canvas: var Canvas, cam: Camera, world: HittableList, max_depth: int) =
init(Weave)

let canvas = canvas.addr # Mutable
let cam = cam.unsafeAddr # Too big for capture

parallelFor row in 0 ..< canvas.nrows:
captures: {canvas, cam, world, max_depth}
parallelFor col in 0 ..< canvas.ncols:
captures: {row, canvas, cam, world, max_depth}
var rng: Rng # We reseed per pixel to be able to parallelize the outer loops
rng.seed(j, i) # And use a "perfect hash" as the seed
rng.seed(row, col) # And use a "perfect hash" as the seed
var pixel = color(0, 0, 0)
for s in 0 ..< samples_per_pixel:
let u = (i.float64 + rng.random(float64)) / float64(image_width - 1)
let v = (j.float64 + rng.random(float64)) / float64(image_height - 1)
let r = cam.ray(u, v, rng)
for _ in 0 ..< canvas.samples_per_pixel:
loadBalance(Weave)
let u = (col.float64 + rng.random(float64)) / float64(canvas.ncols - 1)
let v = (row.float64 + rng.random(float64)) / float64(canvas.nrows - 1)
let r = cam[].ray(u, v, rng)
pixel += radiance(r, world, max_depth, rng)
output.write(pixel, samples_per_pixel)
canvas[].draw(row, col, pixel)

exit(Weave)
2 changes: 1 addition & 1 deletion trace_of_radiance/scenes.nim
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import
./sampling,
./primitives

func random_scene*(rng: var Rng): HittableList =
func random_scene*(rng: var Rng): Scene =
let ground_material = lambertian attenuation(0.5,0.5,0.5)
result.add sphere(center = point3(0,-1000,0), 1000, ground_material)

Expand Down
9 changes: 9 additions & 0 deletions trace_of_radiance/trace_of_radiance.nimble
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Package

version = "0.1.9"
author = "Mamy André-Ratsimbazafy"
description = "An educational raytracer"
license = "MIT or Apache License 2.0"

# Dependencies
requires "nim >= 1.2.0", "weave"

0 comments on commit 26d434e

Please sign in to comment.