diff --git a/src/camera.zig b/src/camera.zig index a49c9ab..17496a0 100644 --- a/src/camera.zig +++ b/src/camera.zig @@ -17,16 +17,77 @@ const unit_vector = mat_math.unit_vector; const length = mat_math.length; const cross = mat_math.cross; +const AtomicOrder = @import("std").builtin.AtomicOrder; +const Thread = std.Thread; +const Semaphore = std.Thread.Semaphore; + +const Pixel = struct { + w: usize, + h: usize, + color: vec3, +}; + +const ThreadInfo = struct { + thread_index: usize, + semaphore_ptr: *Semaphore, +}; + +// Resolution parameters +const aspect_ratio: f64 = 16.0 / 9.0; +const image_width: usize = 2560; // Possible 128, 256, 512, 1024, 1280, 1920, 2560, 3840, 7680 +const image_height: usize = image_width / aspect_ratio; +const samples_per_pixel = 500; +const max_depth = 50; + +const n_threads_to_spawn = 100; + +var completion_count: usize = 0; +var next_entry_to_do: usize = 0; +var entry_count: usize = 0; +var entry_buffer: [image_width * image_height]Pixel = undefined; + +var _camera: *const Camera = undefined; + +fn addWork(semaphore_ptr: *Semaphore) void { + const entry_index = @atomicLoad(usize, &entry_count, AtomicOrder.seq_cst); + + // SeqCst guarantees that the msg write above is visible before the entry_count write below can be seen. + @atomicStore(usize, &entry_count, entry_index + 1, AtomicOrder.seq_cst); + + semaphore_ptr.post(); // Wake up all threads +} + +fn doWork(info: *ThreadInfo) void { + while (true) { + if (@atomicLoad(usize, &next_entry_to_do, AtomicOrder.seq_cst) < @atomicLoad(usize, &entry_count, AtomicOrder.seq_cst)) { + const entry_index = @atomicRmw(usize, &next_entry_to_do, .Add, 1, AtomicOrder.seq_cst); + const pixel = entry_buffer[entry_index]; + + var pixel_color = vec3{ 0, 0, 0 }; + for (0.._camera.samples_per_pixel) |_| { + const r = _camera.get_ray(pixel.h, pixel.w); + pixel_color += ray_color(r, _camera.max_depth, _camera.world); + } + entry_buffer[entry_index].color = pixel_color; + + _ = @atomicRmw(usize, &completion_count, .Add, 1, AtomicOrder.seq_cst); + } else { + info.semaphore_ptr.wait(); // Put all threads to sleep + } + } +} + pub const Camera = struct { aspect_ratio: f64, - image_width: i64, - samples_per_pixel: i64, - max_depth: i64, + image_width: usize, + samples_per_pixel: usize, + max_depth: usize, vfov: f64, defocus_angle: f64, focus_dist: f64, + world: *const HittableList, - image_height: i64, + image_height: usize, center: vec3, pixel_samples_scale: f64, pixel00_loc: vec3, @@ -39,11 +100,72 @@ pub const Camera = struct { v: vec3, w: vec3, - pub fn new() Camera { - const aspect_ratio: f64 = 16.0 / 9.0; - const image_width: i64 = 512; // Possible 128, 256, 512, 1024, 1280, 1920, 2560, 3840, 7680 - const samples_per_pixel = 50; - const max_depth = 10; + pub fn render(self: Camera, writer: anytype) !void { + _camera = &self; + + var semaphore = Semaphore{}; + var infos: [n_threads_to_spawn]ThreadInfo = undefined; + for (&infos, 0..) |*info, thread_index| { + info.thread_index = thread_index; + info.semaphore_ptr = &semaphore; + const handle = try Thread.spawn(.{}, doWork, .{info}); + handle.detach(); + } + + // Write the PPM header + try writer.print("P3\n{} {}\n255\n", .{ self.image_width, self.image_height }); + + for (0..self.image_width) |w| { + for (0..self.image_height) |h| { + entry_buffer[w * self.image_height + h] = Pixel{ .w = w, .h = h, .color = vec3{ -1, -1, -1 } }; + addWork(&semaphore); + } + } + + print("\nStarting rendering using {d} threads\n", .{n_threads_to_spawn}); + while (entry_count != @atomicLoad(usize, &completion_count, AtomicOrder.seq_cst)) { + pbar(@atomicLoad(usize, &completion_count, AtomicOrder.seq_cst), self.image_height * self.image_width); + } + + pbar(100, 100); + + for (0..self.image_height) |h| { + for (0..self.image_width) |w| { + try writeColor(self.get_pixel_color(w, h) * toVec3(self.pixel_samples_scale), writer); + } + } + } + + fn get_pixel_color(self: Camera, w: usize, h: usize) vec3 { + const pixel = entry_buffer[w * self.image_height + h]; + if ((pixel.w == w) and (pixel.h == h)) { + return pixel.color; + } + print("Pixel with different coordinates w: {}, h: {}, pixel.w: {}, pixel.h: {}", .{ w, h, pixel.w, pixel.h }); + return vec3{ 0, 0, 0 }; + } + + fn get_ray(self: Camera, h: usize, w: usize) Ray { + // Construct a camera ray originating from the origin and directed at randomly sampled + // point around the pixel location i, j. + + const offset = sample_square(); + const pixel_sample = self.pixel00_loc + + (toVec3(h) + toVec3(offset[0])) * self.pixel_delta_v + + (toVec3(w) + toVec3(offset[1])) * self.pixel_delta_u; + + const ray_origin = if (self.defocus_angle <= 0) self.center else self.defocus_disk_sample(); + const ray_direction = pixel_sample - ray_origin; + + return Ray{ .orig = ray_origin, .dir = ray_direction }; + } + + fn defocus_disk_sample(self: Camera) vec3 { + const p = random_in_unit_disk(); + return self.center + (toVec3(p[0]) * self.defocus_disk_u) + (toVec3(p[1]) * self.defocus_disk_v); + } + + pub fn new(world: *const HittableList) Camera { const vfov = 20; const lookfrom = vec3{ 13, 2, 3 }; const lookat = vec3{ 0, 0, 0 }; @@ -53,7 +175,6 @@ pub const Camera = struct { const focus_dist = 10; const camera_center = lookfrom; - const image_height: i64 = image_width / aspect_ratio; const pixel_samples_scale = 1.0 / @as(f64, @floatFromInt(samples_per_pixel)); // Camera @@ -76,7 +197,7 @@ pub const Camera = struct { const viewport_upper_left = camera_center - toVec3(focus_dist) * w - viewport_u / toVec3(2) - viewport_v / toVec3(2); const pixel00_loc = viewport_upper_left + toVec3(0.5) * (pixel_delta_u + pixel_delta_v); - const defocus_radius = focus_dist * @tan(utils.degrees_to_radians(defocus_angle / 2)); + const defocus_radius = focus_dist * @tan(utils.degrees_to_radians(defocus_angle / 2.0)); const defocus_disk_u = u * toVec3(defocus_radius); const defocus_disk_v = v * toVec3(defocus_radius); @@ -88,6 +209,7 @@ pub const Camera = struct { .vfov = vfov, .focus_dist = focus_dist, .defocus_angle = defocus_angle, + .world = world, .u = u, .v = v, @@ -103,50 +225,32 @@ pub const Camera = struct { .pixel_delta_v = pixel_delta_v, }; } - - pub fn render(self: Camera, world: HittableList, writer: anytype) !void { - // Write the PPM header - try writer.print("P3\n{} {}\n255\n", .{ self.image_width, self.image_height }); - - // Write the pixel data - for (0..@as(usize, @intCast(self.image_height))) |i| { - const h = @as(i64, @intCast(i)); - pbar(h, self.image_height); - for (0..@as(usize, @intCast(self.image_width))) |j| { - const w = @as(i64, @intCast(j)); - - var pixel_color = vec3{ 0, 0, 0 }; - for (0..@as(usize, @intCast(self.samples_per_pixel))) |_| { - const r = self.get_ray(h, w); - pixel_color += ray_color(r, self.max_depth, world); - } - - try writeColor(pixel_color * toVec3(self.pixel_samples_scale), writer); - } - } - } - - fn get_ray(self: Camera, h: i64, w: i64) Ray { - // Construct a camera ray originating from the origin and directed at randomly sampled - // point around the pixel location i, j. - - const offset = sample_square(); - const pixel_sample = self.pixel00_loc + - (toVec3((@as(f64, @floatFromInt(h))) + offset[0]) * self.pixel_delta_v) + - (toVec3((@as(f64, @floatFromInt(w))) + offset[1]) * self.pixel_delta_u); - - const ray_origin = if (self.defocus_angle <= 0) self.center else self.defocus_disk_sample(); - const ray_direction = pixel_sample - ray_origin; - - return Ray{ .orig = ray_origin, .dir = ray_direction }; - } - - fn defocus_disk_sample(self: Camera) vec3 { - const p = random_in_unit_disk(); - return self.center + (toVec3(p[0]) * self.defocus_disk_u) + (toVec3(p[1]) * self.defocus_disk_v); - } }; +fn splitPixels(allocator: std.mem.Allocator, pixels: []Pixel, num_sublists: usize) ![][]Pixel { + const len = pixels.len; + if (num_sublists == 0 or num_sublists > len) { + return error.InvalidNumberOfSublists; + } + + const sublist_size = len / num_sublists; + const remainder = len % num_sublists; + + var result = try allocator.alloc([]Pixel, num_sublists); + var start: usize = 0; + + for (0..num_sublists) |i| { + var end = start + sublist_size; + if (i < remainder) { + end += 1; + } + result[i] = pixels[start..end]; + start = end; + } + + return result; +} + fn random_in_unit_disk() vec3 { while (true) { const p = vec3{ utils.rand_mm(-1, 1), utils.rand_mm(-1, 1), 0 }; @@ -155,7 +259,7 @@ fn random_in_unit_disk() vec3 { } } -fn ray_color(ray: Ray, depth: i64, world: HittableList) vec3 { +fn ray_color(ray: Ray, depth: usize, world: *const HittableList) vec3 { if (depth <= 0) { return vec3{ 0, 0, 0 }; } @@ -186,6 +290,10 @@ fn writeColor(color: vec3, writer: anytype) !void { var g_float = color[1]; var b_float = color[2]; + if ((r_float < -0.999) or (g_float < -0.999) or (b_float < -0.999)) { + @panic("-1 color detected"); + } + r_float = utils.linear_to_gamma(r_float); g_float = utils.linear_to_gamma(g_float); b_float = utils.linear_to_gamma(b_float); @@ -198,11 +306,11 @@ fn writeColor(color: vec3, writer: anytype) !void { try writer.print("{} {} {}\n", .{ r, g, b }); } -fn pbar(value: i64, max: i64) void { +fn pbar(value: usize, max: usize) void { const used_char = "-"; const number_of_char = 60; - const percent_done: i64 = if (value == max - 1) 100 else @divFloor(value * 100, max); - const full_char: i64 = @divFloor(number_of_char * percent_done, 100); + const percent_done: usize = if (value == max - 1) 100 else @divFloor(value * 100, max); + const full_char: usize = @divFloor(number_of_char * percent_done, 100); print("\r|", .{}); @@ -215,7 +323,9 @@ fn pbar(value: i64, max: i64) void { } } - print("| {}%", .{percent_done}); + print("| {}% |", .{percent_done}); + print(" {} ", .{value}); + print("/ {}", .{max}); if (percent_done == 100) { print("\n", .{}); diff --git a/src/main.zig b/src/main.zig index d47db3b..8cfc35c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -32,21 +32,20 @@ pub fn main() !void { defer file.close(); var buffered_writer = std.io.bufferedWriter(file.writer()); const writer = buffered_writer.writer(); - const camera = Camera.new(); var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); - defer arena.deinit(); + errdefer arena.deinit(); const alloc = arena.allocator(); const world = try generateRandomScene(alloc); + const camera = Camera.new(&world); const start_time: i64 = std.time.milliTimestamp(); - try camera.render(world, &writer); + try camera.render(&writer); const end_time: i64 = std.time.milliTimestamp(); const total_time: f64 = @as(f64, @floatFromInt(end_time - start_time)) / 1000; - print("Rendering took {} s\n", .{total_time}); - print("Potential FPS: {}\n", .{1 / total_time}); + print("Rendering took {d} s\n\n", .{total_time}); // Flush the buffered writer to ensure all data is written to the file try buffered_writer.flush(); @@ -98,6 +97,45 @@ pub fn run_open_image() !void { } } +pub fn generateSameScene(alloc: std.mem.Allocator) !HittableList { + var spheres = std.ArrayList(Hittable).init(alloc); + + // Ground sphere + try spheres.append(.{ + .sphere = Sphere{ + .center = vec3{ 0, -1000, 0 }, + .radius = 1000, + .material = Material{ .lambertian = Lambertian{ .albedo = vec3{ 0.5, 0.5, 0.5 } } }, + }, + }); + + // Three large spheres + try spheres.append(.{ + .sphere = Sphere{ + .center = vec3{ 0, 1, 0 }, + .radius = 1.0, + .material = Material{ .dielectric = Dielectric{ .refraction_index = 1.5 } }, + }, + }); + try spheres.append(.{ + .sphere = Sphere{ + .center = vec3{ -4, 1, 0 }, + .radius = 1.0, + .material = Material{ .lambertian = Lambertian{ .albedo = vec3{ 0.4, 0.2, 0.1 } } }, + }, + }); + try spheres.append(.{ + .sphere = Sphere{ + .center = vec3{ 4, 1, 0 }, + .radius = 1.0, + .material = Material{ .metal = Metal{ .albedo = vec3{ 0.7, 0.6, 0.5 }, .fuzz = 0.0 } }, + }, + }); + + // Convert ArrayList to a slice and create HittableList + return HittableList{ .list = spheres.items }; +} + pub fn generateRandomScene(alloc: std.mem.Allocator) !HittableList { var spheres = std.ArrayList(Hittable).init(alloc); @@ -111,10 +149,13 @@ pub fn generateRandomScene(alloc: std.mem.Allocator) !HittableList { }); // Smaller spheres - var a: i32 = -11; + var a: i32 = -6; while (a < 11) : (a += 1) { - var b: i32 = -11; - while (b < 11) : (b += 1) { + var b: i32 = -6; + while (b < 6) : (b += 1) { + if ((-1 < a) and (a < 1) or ((-1 < b) and (b < 1))) { + continue; + } const choose_mat = utils.rand_01(); const center = vec3{ @as(f64, @floatFromInt(a)) + 0.9 * utils.rand_01(), diff --git a/src/utils.zig b/src/utils.zig index 6ba54bf..f296173 100644 --- a/src/utils.zig +++ b/src/utils.zig @@ -45,6 +45,10 @@ pub const Interval = struct { pub fn toVec3(x: anytype) vec3 { switch (@TypeOf(x)) { + usize => { + const x_float = @as(f64, @floatFromInt(x)); + return vec3{ x_float, x_float, x_float }; + }, comptime_float => { const x_float = @as(f64, x); return vec3{ x_float, x_float, x_float }; @@ -63,10 +67,7 @@ pub fn toVec3(x: anytype) vec3 { @Vector(3, f64) => { return x; }, - usize => { - const x_float = @as(f64, @floatFromInt(@as(i64, @intCast(x)))); - return vec3{ x_float, x_float, x_float }; - }, + else => { @panic("Unknow type passed through toVec3\n\n"); },