├── src ├── modules │ ├── voxel_rt │ │ ├── Animator.zig │ │ ├── test.zig │ │ ├── vox │ │ │ ├── test.zig │ │ │ ├── types.zig │ │ │ └── loader.zig │ │ ├── gpu_types.zig │ │ ├── brick │ │ │ ├── MaterialAllocator.zig │ │ │ ├── State.zig │ │ │ └── Grid.zig │ │ ├── Sun.zig │ │ ├── terrain │ │ │ ├── perlin.zig │ │ │ └── terrain.zig │ │ ├── Camera.zig │ │ ├── Benchmark.zig │ │ └── ImguiGui.zig │ ├── test.zig │ ├── render │ │ ├── memory.zig │ │ ├── c.zig │ │ ├── dispatch.zig │ │ ├── consts.zig │ │ ├── pipeline.zig │ │ ├── validation_layer.zig │ │ ├── vk_utils.zig │ │ ├── GpuBufferMemory.zig │ │ ├── physical_device.zig │ │ ├── swapchain.zig │ │ ├── Context.zig │ │ └── Texture.zig │ ├── utils.zig │ ├── render.zig │ ├── VoxelRT.zig │ └── Input.zig ├── test.zig └── main.zig ├── screenshot.png ├── assets ├── models │ ├── doom.vox │ └── monu10.vox └── shaders │ ├── ui.frag │ ├── image.vert │ ├── ui.vert │ ├── height_map_gen.comp │ ├── rand.comp │ ├── image.frag │ ├── perlin.comp │ └── psrdnoise3.comp ├── .vscode ├── README.md ├── settings.json ├── tasks.json └── launch.json ├── .gitignore ├── .gitmodules └── README.md /src/modules/voxel_rt/Animator.zig: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test.zig: -------------------------------------------------------------------------------- 1 | test { 2 | _ = @import("modules/test.zig"); 3 | } 4 | -------------------------------------------------------------------------------- /src/modules/test.zig: -------------------------------------------------------------------------------- 1 | test { 2 | _ = @import("voxel_rt/test.zig"); 3 | } 4 | -------------------------------------------------------------------------------- /src/modules/voxel_rt/test.zig: -------------------------------------------------------------------------------- 1 | test { 2 | _ = @import("vox/test.zig"); 3 | } 4 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Avokadoen/zig_vulkan/HEAD/screenshot.png -------------------------------------------------------------------------------- /src/modules/voxel_rt/vox/test.zig: -------------------------------------------------------------------------------- 1 | test { 2 | _ = @import("loader.zig"); 3 | } 4 | -------------------------------------------------------------------------------- /assets/models/doom.vox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Avokadoen/zig_vulkan/HEAD/assets/models/doom.vox -------------------------------------------------------------------------------- /assets/models/monu10.vox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Avokadoen/zig_vulkan/HEAD/assets/models/monu10.vox -------------------------------------------------------------------------------- /.vscode/README.md: -------------------------------------------------------------------------------- 1 | # VsCode configuration 2 | 3 | Make sure to install Run on Save by emaraldwalk in order to get zig fmt on save 4 | 5 | also recommmend zig by either Marc TieHuis or prime31 + zls for vscode -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmake.configureOnOpen": false, 3 | "debug.onTaskErrors": "abort", 4 | "glsllint.glslangValidatorArgs": [ 5 | "--target-env", 6 | "vulkan1.3", 7 | "-I${workspaceFolder}assets/shaders" 8 | ], 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Zig generated files 2 | .zig-cache 3 | zig-out 4 | 5 | # Dependency based documentation 6 | deps/docs/* 7 | # Dependency based documentation readme 8 | !/deps/docs/README.md 9 | 10 | # Ignore any generated zig vulkan header (usually moved to root for zls) 11 | vk.zig 12 | 13 | *.spv 14 | 15 | imgui.ini 16 | -------------------------------------------------------------------------------- /assets/shaders/ui.frag: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | layout (binding = 0) uniform sampler2D fontSampler; 4 | 5 | layout (location = 0) in vec2 inUV; 6 | layout (location = 1) in vec4 inColor; 7 | 8 | layout (location = 0) out vec4 outColor; 9 | 10 | void main() 11 | { 12 | outColor = inColor * texture(fontSampler, inUV); 13 | } 14 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/vulkan-zig"] 2 | path = deps/vulkan-zig 3 | url = https://github.com/Snektron/vulkan-zig.git 4 | [submodule "deps/mach-glfw"] 5 | path = deps/mach-glfw 6 | url = https://github.com/hexops/mach-glfw.git 7 | [submodule "deps/zalgebra"] 8 | path = deps/zalgebra 9 | url = https://github.com/kooparse/zalgebra.git 10 | 11 | -------------------------------------------------------------------------------- /assets/shaders/image.vert: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | layout (location = 0) in vec3 inPos; 4 | layout (location = 1) in vec2 inUV; 5 | 6 | layout (location = 0) out vec2 outUV; 7 | 8 | out gl_PerVertex 9 | { 10 | vec4 gl_Position; 11 | }; 12 | 13 | void main() 14 | { 15 | outUV = inUV; 16 | gl_Position = vec4(inPos, 1.0); 17 | } 18 | -------------------------------------------------------------------------------- /src/modules/render/memory.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const vk = @import("vulkan"); 4 | 5 | const Context = @import("Context.zig"); 6 | 7 | pub const bytes_in_mb = 1048576; 8 | 9 | pub inline fn nonCoherentAtomSize(ctx: Context, size: vk.DeviceSize) vk.DeviceSize { 10 | const atom_size = ctx.physical_device_limits.non_coherent_atom_size; 11 | return atom_size * (std.math.divCeil(vk.DeviceSize, size, atom_size) catch unreachable); 12 | } 13 | -------------------------------------------------------------------------------- /src/modules/render/c.zig: -------------------------------------------------------------------------------- 1 | const vk = @import("vulkan"); 2 | const zglfw = @import("zglfw"); 3 | 4 | pub extern fn glfwGetInstanceProcAddress(instance: vk.Instance, procname: [*:0]const u8) vk.PfnVoidFunction; 5 | pub extern fn glfwGetPhysicalDevicePresentationSupport(instance: vk.Instance, pdev: vk.PhysicalDevice, queuefamily: u32) c_int; 6 | pub extern fn glfwCreateWindowSurface(instance: vk.Instance, window: *zglfw.Window, allocation_callbacks: ?*const vk.AllocationCallbacks, surface: *vk.SurfaceKHR) vk.Result; 7 | -------------------------------------------------------------------------------- /assets/shaders/ui.vert: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | layout (location = 0) in vec2 inPos; 4 | layout (location = 1) in vec2 inUV; 5 | layout (location = 2) in vec4 inColor; 6 | 7 | layout (push_constant) uniform PushConstants { 8 | vec2 scale; 9 | vec2 translate; 10 | } pushConstant; 11 | 12 | layout (location = 0) out vec2 outUV; 13 | layout (location = 1) out vec4 outColor; 14 | 15 | out gl_PerVertex 16 | { 17 | vec4 gl_Position; 18 | }; 19 | 20 | void main() 21 | { 22 | outUV = inUV; 23 | outColor = inColor; 24 | gl_Position = vec4(inPos * pushConstant.scale + pushConstant.translate, 0.0, 1.0); 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/render/dispatch.zig: -------------------------------------------------------------------------------- 1 | /// This file contains the vk Wrapper types used to call vulkan functions. 2 | /// Wrapper is a vulkan-zig construct that generates compile time types that 3 | /// links with vulkan functions depending on queried function requirements 4 | /// see vk X_Command types for implementation details 5 | const vk = @import("vulkan"); 6 | 7 | const consts = @import("consts.zig"); 8 | 9 | pub const Base = vk.BaseWrapper; 10 | 11 | // TODO: manually loading functions we care about, not all 12 | pub const Instance = vk.InstanceWrapper; 13 | 14 | pub const Device = vk.DeviceWrapper; 15 | 16 | pub const BeginCommandBufferError = Device.BeginCommandBufferError; 17 | -------------------------------------------------------------------------------- /assets/shaders/height_map_gen.comp: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | #include "perlin.comp" // cnoise 4 | 5 | layout(local_size_x = 32, local_size_y = 32) in; 6 | layout(Rgba8, binding = 0) uniform writeonly image2D img_output; 7 | 8 | layout(binding = 1) uniform GenerateData { 9 | vec4 offset_scale; 10 | uint seed; 11 | } gen_data; 12 | 13 | void main() { 14 | const vec2 image_size = imageSize(img_output); 15 | const vec2 offset = gen_data.offset_scale.xy * image_size; 16 | const vec3 pos = vec3(((gl_GlobalInvocationID.xy + offset) / image_size) * gen_data.offset_scale.w, 0); 17 | const float noise = cnoise(pos); 18 | imageStore(img_output, ivec2(gl_GlobalInvocationID.xy), vec4(noise, noise, noise, 1)); 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/utils.zig: -------------------------------------------------------------------------------- 1 | /// conveiance functions for the codebase 2 | const std = @import("std"); 3 | const Allocator = std.mem.Allocator; 4 | const ArrayList = std.ArrayList; 5 | 6 | // TODO: don't use arraylist, just allocate slice with allocator 7 | /// caller must deinit returned memory 8 | pub fn readFile(allocator: Allocator, absolute_path: []const u8) !ArrayList(u8) { 9 | const file = try std.fs.openFileAbsolute(absolute_path, .{ .mode = .read_only }); 10 | defer file.close(); 11 | 12 | var reader = file.reader(); 13 | const file_size = (try reader.context.stat()).size; 14 | var buffer = try ArrayList(u8).initCapacity(allocator, file_size); 15 | // set buffer len so that reader is aware of usable memory 16 | buffer.items.len = file_size; 17 | 18 | const read = try reader.readAll(buffer.items); 19 | if (read != file_size) { 20 | return error.DidNotReadWholeFile; 21 | } 22 | 23 | return buffer; 24 | } 25 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "type": "shell", 7 | "command": "zig build -Doptimize=Debug -Denable_ztracy=false", 8 | "group": "build", 9 | "problemMatcher": [ 10 | "$gcc" 11 | ] 12 | }, 13 | { 14 | "label": "safe", 15 | "type": "shell", 16 | "command": "zig build -Doptimize=ReleaseSafe -Denable_ztracy=true", 17 | "group": "build", 18 | "problemMatcher": [ 19 | "$gcc" 20 | ] 21 | }, 22 | { 23 | "label": "release", 24 | "type": "shell", 25 | "command": "zig build -Doptimize=ReleaseSafe -Denable_ztracy=false", 26 | "group": "build", 27 | "problemMatcher": [ 28 | "$gcc" 29 | ] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![frame capture](screenshot.png) 3 | 4 | # Zig vulkan renderer 5 | 6 | A toy renderer written in zig using vulkan and glfw 7 | 8 | # Requirements 9 | 10 | Zig build toolchain does most of the heavy lifting. The only systems 11 | requirement is the [Vulkan SDK](https://www.lunarg.com/vulkan-sdk/). 12 | Make sure you download Vulkan 1.4 or up 13 | 14 | **This project uses zig 0.14.0** 15 | 16 | # Run the project 17 | 18 | Do the following steps 19 | ```bash 20 | $ git clone 21 | $ cd 22 | $ zig build run 23 | ``` 24 | 25 | # Run tests 26 | 27 | Currently the code base is not really well tested, but you can run the few tests by doin ``zig build test`` 28 | 29 | # Sources: 30 | 31 | * Vulkan fundementals: 32 | * https://vkguide.dev/ 33 | * https://vulkan-tutorial.com 34 | * Setup Zig for Gamedev: https://dev.to/fabioarnold/setup-zig-for-gamedev-2bmf 35 | * Using vulkan-zig: https://github.com/Snektron/vulkan-zig/blob/master/examples 36 | -------------------------------------------------------------------------------- /src/modules/render.zig: -------------------------------------------------------------------------------- 1 | /// library with utility wrappers around vulkan functions 2 | pub const Context = @import("render/Context.zig"); 3 | /// Wrapper for vk buffer and memory to simplify handling of these in conjunction 4 | pub const GpuBufferMemory = @import("render/GpuBufferMemory.zig"); 5 | /// Wrapper a collection GpuBufferMemory used to stage transfers to device local memory 6 | pub const StagingRamp = @import("render/StagingRamp.zig"); 7 | /// Texture abstraction 8 | pub const Texture = @import("render/Texture.zig"); 9 | 10 | /// helper methods for handling of pipelines 11 | pub const consts = @import("render/consts.zig"); 12 | pub const dispatch = @import("render/dispatch.zig"); 13 | pub const memory = @import("render/memory.zig"); 14 | pub const physical_device = @import("render/physical_device.zig"); 15 | pub const pipeline = @import("render/pipeline.zig"); 16 | pub const swapchain = @import("render/swapchain.zig"); 17 | pub const validation_layer = @import("render/validation_layer.zig"); 18 | pub const vk_utils = @import("render/vk_utils.zig"); 19 | -------------------------------------------------------------------------------- /src/modules/render/consts.zig: -------------------------------------------------------------------------------- 1 | /// This file contains compiletime constants that are relevant for the renderer API. 2 | const std = @import("std"); 3 | const builtin = @import("builtin"); 4 | const vk = @import("vulkan"); 5 | 6 | // enable validation layer in debug 7 | pub const enable_debug_markers = builtin.mode == .Debug; 8 | pub const enable_validation_layers = builtin.mode == .Debug; 9 | pub const engine_name = "nop"; 10 | pub const engine_version = vk.makeApiVersion(0, 0, 1, 0); 11 | pub const application_version = vk.makeApiVersion(0, 0, 1, 0); 12 | 13 | pub const vulkan_version = vk.API_VERSION_1_4; 14 | 15 | const release_logical_device_extensions = [_][*:0]const u8{ 16 | vk.extensions.khr_swapchain.name, 17 | }; 18 | 19 | pub const logical_device_extensions = init_device_ext_blk: { 20 | if (enable_validation_layers) { 21 | break :init_device_ext_blk release_logical_device_extensions; 22 | } else { 23 | break :init_device_ext_blk release_logical_device_extensions; 24 | } 25 | }; 26 | 27 | pub const max_frames_in_flight = 2; 28 | -------------------------------------------------------------------------------- /src/modules/voxel_rt/gpu_types.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Vec3 = @Vector(3, f32); 3 | const Vec4 = @Vector(4, f32); 4 | 5 | pub const BufferConfig = @import("../render.zig").ComputeDrawPipeline.BufferConfig; 6 | 7 | // storage value that hold the binidng value in shader, and size of a given type 8 | const MapValue = struct { 9 | binding: u32, 10 | size: u64, 11 | }; 12 | 13 | // Camera uniform is defined in Camera.zig 14 | 15 | /// Materials define how a ray should interact with a given voxel 16 | pub const Material = extern struct { 17 | /// Type is the main attribute of a material and define reflection and refraction behaviour 18 | pub const Type = enum(u32) { 19 | /// normal diffuse material 20 | lambertian = 0, 21 | /// shiny material with fuzz 22 | metal = 1, 23 | /// glass and other see through material 24 | dielectric = 2, 25 | }; 26 | 27 | type: Type, 28 | albedo_r: f32, 29 | albedo_g: f32, 30 | albedo_b: f32, 31 | type_data: f32, 32 | }; 33 | 34 | pub const Node = extern struct { 35 | pub const Type = enum(u32) { 36 | empty = 0, 37 | parent, 38 | leaf, 39 | }; 40 | 41 | type: Type, 42 | value: u32, 43 | }; 44 | -------------------------------------------------------------------------------- /src/modules/voxel_rt/brick/MaterialAllocator.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | const brick_bits = @import("State.zig").brick_bits; 4 | 5 | pub const Entry = u32; 6 | 7 | pub const Cursor = std.atomic.Value(Entry); 8 | 9 | const IndexMap = std.AutoArrayHashMapUnmanaged( 10 | usize, 11 | usize, 12 | ); 13 | 14 | const MaterialAllocator = @This(); 15 | 16 | capacity: usize, 17 | next_index: Cursor = .init(0), 18 | 19 | // TODO: implement releaseEntry, this array must be behind a mutex, an Atomic may be used to probe if there are any entries in the array 20 | // released_entries: std.ArrayListUnmanaged(Entry) = .empty, 21 | 22 | pub fn init(capacity: usize) MaterialAllocator { 23 | return MaterialAllocator{ 24 | .capacity = capacity, 25 | }; 26 | } 27 | 28 | pub fn deinit(self: *MaterialAllocator, allocator: Allocator) void { 29 | _ = self; 30 | _ = allocator; 31 | // self.released_entries.deinit(allocator); 32 | } 33 | 34 | pub fn nextEntry(self: *MaterialAllocator) Entry { 35 | // if (self.released_entries.pop()) |free| { 36 | // return free; 37 | // } 38 | 39 | const next_entry = self.next_index.fetchAdd(brick_bits, .monotonic); 40 | std.debug.assert(next_entry < self.capacity); // no more material data, should not occur 41 | 42 | return next_entry; 43 | } 44 | 45 | // pub fn releaseEntry(self: *MaterialAllocator, allocator: Allocator, entry: Entry) error{OutOfMemory}!void { 46 | // self.released_entries.append(allocator, entry); 47 | // } 48 | -------------------------------------------------------------------------------- /assets/shaders/rand.comp: -------------------------------------------------------------------------------- 1 | /// File containing different random and hashing functions 2 | 3 | float Rand(float co) { return fract(sin(co*(91.3458)) * 47453.5453); } 4 | float Rand(vec2 co){ return fract(sin(dot(co.xy, vec2(12.9898,78.233))) * 43758.5453); } 5 | float Rand(vec3 co){ return Rand(co.xy+Rand(co.z)); } 6 | float Rand(vec2 co, float min, float max) { 7 | return min + (max - min) * Rand(co); 8 | } 9 | vec3 RandVec3(vec2 co) { 10 | float x = Rand(co); 11 | float y = Rand(vec2(co.x + x, co.y + x)); 12 | float z = Rand(vec2(co.x + y, co.y + y)); 13 | return vec3(x, y, z); 14 | } 15 | vec3 RandVec3(vec2 co, float min, float max) { 16 | float x = Rand(co, min, max); 17 | float y = Rand(vec2(co.x + x, co.y + x), min, max); 18 | float z = Rand(vec2(co.x + y, co.y + y), min, max); 19 | return vec3(x, y, z); 20 | } 21 | // Source: https://www.shadertoy.com/view/4djSRW 22 | float hash12(vec2 p) { 23 | vec3 p3 = fract(vec3(p.xyx) * .1031); 24 | p3 += dot(p3, p3.yzx + 33.33); 25 | return fract((p3.x + p3.y) * p3.z); 26 | } 27 | float hash12(vec2 p, float min, float max) { 28 | return fma(hash12(p), (max - min), min) ; 29 | } 30 | float hash13(vec3 p3) 31 | { 32 | p3 = fract(p3 * .1031); 33 | p3 += dot(p3, p3.zyx + 31.32); 34 | return fract((p3.x + p3.y) * p3.z); 35 | } 36 | vec2 hash23(vec3 p3) 37 | { 38 | p3 = fract(p3 * vec3(.1031, .1030, .0973)); 39 | p3 += dot(p3, p3.yzx+33.33); 40 | return fract((p3.xx+p3.yz)*p3.zy); 41 | } 42 | vec3 hash32(vec2 p) 43 | { 44 | vec3 p3 = fract(vec3(p.xyx) * vec3(.1031, .1030, .0973)); 45 | p3 += dot(p3, p3.yxz + 33.33); 46 | return fract((p3.xxy + p3.yzz) * p3.zyx); 47 | } 48 | vec3 hash32(vec2 p, float min, float max) { 49 | vec3 rng = hash32(p); 50 | float min_max_diff = max - min; 51 | return vec3( 52 | fma(rng.x, min_max_diff, min), 53 | fma(rng.y, min_max_diff, min), 54 | fma(rng.z, min_max_diff, min) 55 | ); 56 | } 57 | vec3 RandInHemisphere(vec2 co, vec3 normal) { 58 | vec3 in_unit_sphere = normalize(RandVec3(co, -1, 1)); 59 | if (dot(in_unit_sphere, normal) > 0.0) // In the same hemisphere as the normal 60 | return in_unit_sphere; 61 | else 62 | return -in_unit_sphere; 63 | } 64 | int sample_i = 1; 65 | vec3 RngSample(vec3 point) { 66 | return fma(point, vec3(100), vec3(sample_i * 6)); 67 | } 68 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "(Windows) Launch Debug", 9 | "type": "cppvsdbg", 10 | "request": "launch", 11 | "program": "${workspaceFolder}/zig-out/bin/zig_vulkan.exe", 12 | "args": [], 13 | "stopAtEntry": false, 14 | "cwd": "${fileDirname}", 15 | "environment": [], 16 | "preLaunchTask": "build" 17 | }, 18 | { 19 | "name": "(Windows) Launch Safe", 20 | "type": "cppvsdbg", 21 | "request": "launch", 22 | "program": "${workspaceFolder}/zig-out/bin/zig_vulkan.exe", 23 | "args": [], 24 | "stopAtEntry": false, 25 | "cwd": "${fileDirname}", 26 | "environment": [], 27 | "preLaunchTask": "safe" 28 | }, 29 | { 30 | "name": "(Windows) Launch Release", 31 | "type": "lldb", 32 | "request": "launch", 33 | "program": "${workspaceFolder}/zig-out/bin/zig_vulkan", 34 | "args": [], 35 | "cwd": "${fileDirname}", 36 | "preLaunchTask": "release" 37 | }, 38 | { 39 | "name": "(Linux) Launch Debug", 40 | "type": "lldb", 41 | "request": "launch", 42 | "program": "${workspaceFolder}/zig-out/bin/zig_vulkan", 43 | "args": [], 44 | "cwd": "${fileDirname}", 45 | "preLaunchTask": "build" 46 | }, 47 | { 48 | "name": "(Linux) Launch Safe", 49 | "type": "lldb", 50 | "request": "launch", 51 | "program": "${workspaceFolder}/zig-out/bin/zig_vulkan", 52 | "args": [], 53 | "cwd": "${fileDirname}", 54 | "preLaunchTask": "safe" 55 | }, 56 | { 57 | "name": "(Linux) Launch Release", 58 | "type": "lldb", 59 | "request": "launch", 60 | "program": "${workspaceFolder}/zig-out/bin/zig_vulkan", 61 | "args": [], 62 | "cwd": "${fileDirname}", 63 | "preLaunchTask": "release" 64 | } 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /assets/shaders/image.frag: -------------------------------------------------------------------------------- 1 | #version 450 2 | 3 | layout (binding = 0) uniform sampler2D imageSampler; 4 | 5 | layout (location = 0) in vec2 inUV; 6 | 7 | layout (location = 0) out vec4 outColor; 8 | 9 | // void main() 10 | // { 11 | // outColor = texture(imageSampler, inUV); 12 | // } 13 | 14 | /* 15 | Denoiser source: https://www.shadertoy.com/view/7d2SDD 16 | */ 17 | 18 | layout (push_constant) uniform PushConstant { 19 | int samples; 20 | float distributionBias; 21 | float pixelMultiplier; 22 | float inversHueTolerance; 23 | } pushConstant; 24 | 25 | #define GOLDEN_ANGLE 2.3999632 //3PI-sqrt(5)PI 26 | 27 | #define pow(a,b) pow(max(a,0.),b) // @morimea 28 | 29 | mat2 sample2D = mat2(cos(GOLDEN_ANGLE), sin(GOLDEN_ANGLE), -sin(GOLDEN_ANGLE), cos(GOLDEN_ANGLE)); 30 | 31 | vec3 sirBirdDenoise() { 32 | ivec2 imageResolution = textureSize(imageSampler, 0); 33 | vec3 denoisedColor = vec3(0.); 34 | 35 | const float sampleRadius = sqrt(float(pushConstant.samples)); 36 | const float sampleTrueRadius = 0.5/(sampleRadius*sampleRadius); 37 | vec2 samplePixel = vec2(1.0/imageResolution.x,1.0/imageResolution.y); 38 | vec3 sampleCenter = texture(imageSampler, inUV).rgb; 39 | vec3 sampleCenterNorm = normalize(sampleCenter); 40 | float sampleCenterSat = length(sampleCenter); 41 | 42 | float influenceSum = 0.0; 43 | float brightnessSum = 0.0; 44 | 45 | vec2 pixelRotated = vec2(0.,1.); 46 | 47 | for (float x = 0.0; x <= float(pushConstant.samples); x++) { 48 | 49 | pixelRotated *= sample2D; 50 | 51 | vec2 pixelOffset = pushConstant.pixelMultiplier * pixelRotated * sqrt(x) * 0.5; 52 | float pixelInfluence = 1.0 - sampleTrueRadius * pow(dot(pixelOffset, pixelOffset), pushConstant.distributionBias); 53 | pixelOffset *= samplePixel; 54 | 55 | vec3 thisDenoisedColor = texture(imageSampler, inUV + pixelOffset).rgb; 56 | 57 | pixelInfluence *= pixelInfluence*pixelInfluence; 58 | /* 59 | HUE + SATURATION FILTER 60 | */ 61 | pixelInfluence *= 62 | pow(0.5 + 0.5 * dot(sampleCenterNorm,normalize(thisDenoisedColor)), pushConstant.inversHueTolerance) 63 | * pow(1.0 - abs(length(thisDenoisedColor)-length(sampleCenterSat)),8.); 64 | 65 | influenceSum += pixelInfluence; 66 | denoisedColor += thisDenoisedColor*pixelInfluence; 67 | } 68 | 69 | return denoisedColor/influenceSum; 70 | 71 | } 72 | 73 | void main() 74 | { 75 | vec3 col = sirBirdDenoise(); 76 | 77 | outColor = vec4(col, 1.0); 78 | } 79 | -------------------------------------------------------------------------------- /src/modules/voxel_rt/Sun.zig: -------------------------------------------------------------------------------- 1 | const za = @import("zalgebra"); 2 | const math = @import("std").math; 3 | 4 | pub const Config = struct { 5 | animate: bool = true, 6 | animate_speed: f32 = 0.1, 7 | enabled: bool = true, 8 | color: [3]f32 = [_]f32{ 1, 1.1, 1 }, 9 | radius: f32 = 5, 10 | sun_distance: f32 = 1000, 11 | }; 12 | 13 | pub const Device = extern struct { 14 | position: [3]f32, 15 | enabled: u32, 16 | color: [3]f32, 17 | radius: f32, 18 | }; 19 | 20 | const Sun = @This(); 21 | 22 | device_data: Device, 23 | 24 | animate: bool, 25 | animate_speed: f32, 26 | 27 | slerp_index: usize, 28 | slerp_pos: f32, 29 | // used to rotate sun around grid 30 | slerp_orientations: [3]za.Quat, 31 | lerp_color: [3]za.Vec3, 32 | 33 | static_pos_vec: za.Vec3, 34 | 35 | pub fn init(config: Config) Sun { 36 | const slerp_orientations = [_]za.Quat{ 37 | za.Quat.fromEulerAngles(za.Vec3.new(0, 0, 0)), 38 | za.Quat.fromEulerAngles(za.Vec3.new(0, 10, 120)), 39 | za.Quat.fromEulerAngles(za.Vec3.new(0, 0, 240)), 40 | }; 41 | const static_pos_vec = za.Vec3.new(0, -config.sun_distance, 0); 42 | const lerp_color = [_]za.Vec3{ 43 | za.Vec3.new(1, 0.99, 0.823), 44 | za.Vec3.new(0.9, 0.45, 0.45), 45 | za.Vec3.new(1, 0.7569, 0.5412), 46 | }; 47 | 48 | return Sun{ 49 | .device_data = .{ 50 | .enabled = @intCast(@intFromBool(config.enabled)), 51 | .position = static_pos_vec.data, 52 | .color = config.color, 53 | .radius = config.radius, 54 | }, 55 | .animate = config.animate, 56 | .animate_speed = config.animate_speed, 57 | .slerp_index = 0, 58 | .slerp_pos = 0, 59 | .slerp_orientations = slerp_orientations, 60 | .static_pos_vec = static_pos_vec, 61 | .lerp_color = lerp_color, 62 | }; 63 | } 64 | 65 | pub inline fn update(self: *Sun, delta_time: f32) void { 66 | if (self.animate == false or self.device_data.enabled == 0) return; 67 | 68 | const next_index = (self.slerp_index + 1) % self.slerp_orientations.len; 69 | { 70 | const quat_a = self.slerp_orientations[self.slerp_index]; 71 | const quat_b = self.slerp_orientations[next_index]; 72 | self.device_data.position = quat_a.slerp(quat_b, self.slerp_pos).rotateVec(self.static_pos_vec).data; 73 | } 74 | 75 | { 76 | const color_a = self.lerp_color[self.slerp_index]; 77 | const color_b = self.lerp_color[next_index]; 78 | self.device_data.color = color_a.lerp(color_b, self.slerp_pos).data; 79 | } 80 | 81 | self.slerp_pos += self.animate_speed * delta_time; 82 | if (self.slerp_pos > 1) { 83 | self.slerp_pos = math.modf(self.slerp_pos).fpart; 84 | self.slerp_index = next_index; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/modules/voxel_rt/vox/types.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | const ArrayList = std.ArrayList; 4 | 5 | /// A vox file 6 | pub const Vox = struct { 7 | allocator: Allocator, 8 | version_number: i32, 9 | nodes: ArrayList(ChunkNode), 10 | generic_chunks: ArrayList(Chunk), 11 | pack_chunk: Chunk.Pack, 12 | size_chunks: []Chunk.Size, 13 | xyzi_chunks: [][]Chunk.XyziElement, 14 | rgba_chunk: [256]Chunk.RgbaElement, 15 | 16 | pub fn init(allocator: Allocator) Vox { 17 | return Vox{ 18 | .allocator = allocator, 19 | // if you enable strict parsing in the load function, then this will be validated in validateHeader 20 | .version_number = 150, 21 | .nodes = ArrayList(ChunkNode).init(allocator), 22 | .generic_chunks = ArrayList(Chunk).init(allocator), 23 | .pack_chunk = undefined, 24 | .size_chunks = undefined, 25 | .xyzi_chunks = undefined, 26 | .rgba_chunk = undefined, 27 | }; 28 | } 29 | 30 | pub fn deinit(self: Vox) void { 31 | for (self.xyzi_chunks) |chunk| { 32 | self.allocator.free(chunk); 33 | } 34 | 35 | self.allocator.free(self.size_chunks); 36 | self.allocator.free(self.xyzi_chunks); 37 | 38 | self.nodes.deinit(); 39 | self.generic_chunks.deinit(); 40 | } 41 | }; 42 | 43 | pub const ChunkNode = struct { 44 | type_id: Chunk.Type, 45 | generic_index: usize, 46 | index: usize, 47 | }; 48 | 49 | /// Generic Chunk and all Chunk types 50 | pub const Chunk = struct { 51 | /// num bytes of chunk content 52 | size: i32, 53 | /// num bytes of children chunks 54 | child_size: i32, 55 | 56 | pub const Type = enum { main, pack, size, xyzi, rgba }; 57 | 58 | pub const Pack = struct { 59 | /// num of SIZE and XYZI chunks 60 | num_models: i32, 61 | }; 62 | 63 | pub const Size = struct { 64 | size_x: i32, 65 | size_y: i32, 66 | /// gravity direction in vox ... 67 | size_z: i32, 68 | }; 69 | 70 | pub const XyziElement = packed struct { 71 | x: u8, 72 | y: u8, 73 | z: u8, 74 | color_index: u8, 75 | }; 76 | 77 | // * 78 | // * color [0-254] are mapped to palette index [1-255], e.g : 79 | // 80 | // for ( int i = 0; i <= 254; i++ ) { 81 | // palette[i + 1] = ReadRGBA(); 82 | // } 83 | pub const RgbaElement = packed struct { 84 | r: u8, 85 | g: u8, 86 | b: u8, 87 | a: u8, 88 | }; 89 | 90 | // Extension chunks below 91 | 92 | // pub const Material = struct { 93 | // pub const Type = enum { 94 | // diffuse, 95 | // metal, 96 | // glass, 97 | // emit 98 | // }; 99 | 100 | // @"type": Type, 101 | 102 | // } 103 | }; 104 | -------------------------------------------------------------------------------- /assets/shaders/perlin.comp: -------------------------------------------------------------------------------- 1 | // Classic Perlin 3D Noise 2 | // by Stefan Gustavson 3 | // 4 | vec4 permute(vec4 x){ 5 | return mod(fma(x, vec4(34.0), vec4(1.0)) * x, 289.0); 6 | } 7 | vec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;} 8 | vec3 fade(vec3 t) {return t*t*t*(t*(t*6.0-15.0)+10.0);} 9 | 10 | float cnoise(vec3 P){ 11 | vec3 Pi0 = floor(P); // Integer part for indexing 12 | vec3 Pi1 = Pi0 + vec3(1.0); // Integer part + 1 13 | Pi0 = mod(Pi0, 289.0); 14 | Pi1 = mod(Pi1, 289.0); 15 | 16 | const vec3 Pf0 = fract(P); // Fractional part for interpolation 17 | const vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0 18 | const vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x); 19 | const vec4 iy = vec4(Pi0.yy, Pi1.yy); 20 | const vec4 iz0 = Pi0.zzzz; 21 | const vec4 iz1 = Pi1.zzzz; 22 | 23 | const vec4 ixy = permute(permute(ix) + iy); 24 | const vec4 ixy0 = permute(ixy + iz0); 25 | const vec4 ixy1 = permute(ixy + iz1); 26 | 27 | const float inv_seven = 1 / 7.0; 28 | vec4 gx0 = ixy0 * inv_seven; 29 | vec4 gy0 = fract(floor(gx0) * inv_seven) - 0.5; 30 | gx0 = fract(gx0); 31 | const vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0); 32 | const vec4 sz0 = step(gz0, vec4(0.0)); 33 | gx0 -= sz0 * (step(0.0, gx0) - 0.5); 34 | gy0 -= sz0 * (step(0.0, gy0) - 0.5); 35 | 36 | vec4 gx1 = ixy1 * inv_seven; 37 | vec4 gy1 = fract(floor(gx1) * inv_seven) - 0.5; 38 | gx1 = fract(gx1); 39 | const vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1); 40 | const vec4 sz1 = step(gz1, vec4(0.0)); 41 | gx1 -= sz1 * (step(0.0, gx1) - 0.5); 42 | gy1 -= sz1 * (step(0.0, gy1) - 0.5); 43 | 44 | vec3 g000 = vec3(gx0.x, gy0.x, gz0.x); 45 | vec3 g100 = vec3(gx0.y, gy0.y, gz0.y); 46 | vec3 g010 = vec3(gx0.z, gy0.z, gz0.z); 47 | vec3 g110 = vec3(gx0.w, gy0.w, gz0.w); 48 | vec3 g001 = vec3(gx1.x, gy1.x, gz1.x); 49 | vec3 g101 = vec3(gx1.y, gy1.y, gz1.y); 50 | vec3 g011 = vec3(gx1.z, gy1.z, gz1.z); 51 | vec3 g111 = vec3(gx1.w, gy1.w, gz1.w); 52 | 53 | const vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110))); 54 | g000 *= norm0.x; 55 | g010 *= norm0.y; 56 | g100 *= norm0.z; 57 | g110 *= norm0.w; 58 | const vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111))); 59 | g001 *= norm1.x; 60 | g011 *= norm1.y; 61 | g101 *= norm1.z; 62 | g111 *= norm1.w; 63 | 64 | const float n000 = dot(g000, Pf0); 65 | const float n100 = dot(g100, vec3(Pf1.x, Pf0.yz)); 66 | const float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z)); 67 | const float n110 = dot(g110, vec3(Pf1.xy, Pf0.z)); 68 | const float n001 = dot(g001, vec3(Pf0.xy, Pf1.z)); 69 | const float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z)); 70 | const float n011 = dot(g011, vec3(Pf0.x, Pf1.yz)); 71 | const float n111 = dot(g111, Pf1); 72 | 73 | const vec3 fade_xyz = fade(Pf0); 74 | const vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z); 75 | const vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y); 76 | const float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x); 77 | return 2.2 * n_xyz; 78 | } 79 | -------------------------------------------------------------------------------- /src/modules/render/pipeline.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | 4 | const vk = @import("vulkan"); 5 | 6 | const swapchain = @import("swapchain.zig"); 7 | const utils = @import("../utils.zig"); 8 | 9 | const Context = @import("Context.zig"); 10 | 11 | pub fn createFramebuffers(allocator: Allocator, ctx: Context, swapchain_data: *const swapchain.Data, render_pass: vk.RenderPass, prev_framebuffer: ?[]vk.Framebuffer) ![]vk.Framebuffer { 12 | const image_views = swapchain_data.image_views; 13 | var framebuffers = prev_framebuffer orelse try allocator.alloc(vk.Framebuffer, image_views.len); 14 | for (image_views, 0..) |view, i| { 15 | const attachments = [_]vk.ImageView{ 16 | view, 17 | }; 18 | const framebuffer_info = vk.FramebufferCreateInfo{ 19 | .flags = .{}, 20 | .render_pass = render_pass, 21 | .attachment_count = attachments.len, 22 | .p_attachments = &attachments, 23 | .width = swapchain_data.extent.width, 24 | .height = swapchain_data.extent.height, 25 | .layers = 1, 26 | }; 27 | const framebuffer = try ctx.vkd.createFramebuffer(ctx.logical_device, &framebuffer_info, null); 28 | framebuffers[i] = framebuffer; 29 | } 30 | return framebuffers; 31 | } 32 | 33 | pub fn loadShaderStage( 34 | ctx: Context, 35 | // TODO: validate and document anytype here 36 | shader_code: anytype, 37 | stage: vk.ShaderStageFlags, 38 | specialization: ?*const vk.SpecializationInfo, 39 | ) !vk.PipelineShaderStageCreateInfo { 40 | const create_info = vk.ShaderModuleCreateInfo{ 41 | .flags = .{}, 42 | .p_code = @ptrCast(&shader_code), 43 | .code_size = shader_code.len, 44 | }; 45 | const module = try ctx.vkd.createShaderModule(ctx.logical_device, &create_info, null); 46 | 47 | return vk.PipelineShaderStageCreateInfo{ 48 | .flags = .{}, 49 | .stage = stage, 50 | .module = module, 51 | .p_name = "main", 52 | .p_specialization_info = specialization, 53 | }; 54 | } 55 | 56 | /// create command buffers with length of buffer_count, caller must deinit returned list 57 | pub fn createCmdBuffers(allocator: Allocator, ctx: Context, command_pool: vk.CommandPool, buffer_count: usize, prev_buffer: ?[]vk.CommandBuffer) ![]vk.CommandBuffer { 58 | var command_buffers = prev_buffer orelse try allocator.alloc(vk.CommandBuffer, buffer_count); 59 | const alloc_info = vk.CommandBufferAllocateInfo{ 60 | .command_pool = command_pool, 61 | .level = vk.CommandBufferLevel.primary, 62 | .command_buffer_count = @intCast(buffer_count), 63 | }; 64 | try ctx.vkd.allocateCommandBuffers(ctx.logical_device, &alloc_info, command_buffers.ptr); 65 | command_buffers.len = buffer_count; 66 | 67 | return command_buffers; 68 | } 69 | 70 | /// create a command buffers, caller must destroy returned buffer with allocator 71 | pub fn createCmdBuffer(ctx: Context, command_pool: vk.CommandPool) !vk.CommandBuffer { 72 | const alloc_info = vk.CommandBufferAllocateInfo{ 73 | .command_pool = command_pool, 74 | .level = vk.CommandBufferLevel.primary, 75 | .command_buffer_count = @intCast(1), 76 | }; 77 | var command_buffer: vk.CommandBuffer = undefined; 78 | try ctx.vkd.allocateCommandBuffers(ctx.logical_device, &alloc_info, @ptrCast(&command_buffer)); 79 | 80 | return command_buffer; 81 | } 82 | -------------------------------------------------------------------------------- /src/modules/render/validation_layer.zig: -------------------------------------------------------------------------------- 1 | /// Functions related to validation layers and debug messaging 2 | const std = @import("std"); 3 | const Allocator = std.mem.Allocator; 4 | 5 | const vk = @import("vulkan"); 6 | 7 | const constants = @import("consts.zig"); 8 | const dispatch = @import("dispatch.zig"); 9 | 10 | fn InfoType() type { 11 | if (constants.enable_validation_layers) { 12 | return struct { 13 | const Self = @This(); 14 | 15 | enabled_layer_count: u8, 16 | enabled_layer_names: [*]const [*:0]const u8, 17 | 18 | pub fn init(allocator: Allocator, vkb: dispatch.Base) !Self { 19 | const validation_layers = [_][*:0]const u8{"VK_LAYER_KHRONOS_validation"}; 20 | const is_valid = try isLayersPresent(allocator, vkb, validation_layers[0..validation_layers.len]); 21 | if (!is_valid) { 22 | std.debug.panic("debug build without validation layer support", .{}); 23 | } 24 | 25 | return Self{ 26 | .enabled_layer_count = validation_layers.len, 27 | .enabled_layer_names = @ptrCast(&validation_layers), 28 | }; 29 | } 30 | }; 31 | } else { 32 | return struct { 33 | const Self = @This(); 34 | 35 | enabled_layer_count: u8, 36 | enabled_layer_names: [*]const [*:0]const u8, 37 | 38 | pub fn init(allocator: Allocator, vkb: dispatch.Base) !Self { 39 | _ = allocator; 40 | _ = vkb; 41 | return Self{ 42 | .enabled_layer_count = 0, 43 | .enabled_layer_names = undefined, 44 | }; 45 | } 46 | }; 47 | } 48 | } 49 | pub const Info = InfoType(); 50 | 51 | /// check if validation layer exist 52 | fn isLayersPresent(allocator: Allocator, vkb: dispatch.Base, target_layers: []const [*:0]const u8) !bool { 53 | var layer_count: u32 = 0; 54 | _ = try vkb.enumerateInstanceLayerProperties(&layer_count, null); 55 | 56 | var available_layers = try std.ArrayList(vk.LayerProperties).initCapacity(allocator, layer_count); 57 | defer available_layers.deinit(); 58 | 59 | // TODO: handle vk.INCOMPLETE (Array too small) 60 | _ = try vkb.enumerateInstanceLayerProperties(&layer_count, available_layers.items.ptr); 61 | available_layers.items.len = layer_count; 62 | 63 | for (target_layers) |target_layer| { 64 | const t_str_len = std.mem.indexOfScalar(u8, target_layer[0..vk.MAX_EXTENSION_NAME_SIZE], 0) orelse continue; 65 | // check if target layer exist in available_layers 66 | inner: for (available_layers.items) |available_layer| { 67 | const layer_name = available_layer.layer_name; 68 | const l_str_len = std.mem.indexOfScalar(u8, layer_name[0..vk.MAX_EXTENSION_NAME_SIZE], 0) orelse continue; 69 | 70 | // if target_layer and available_layer is the same 71 | if (std.mem.eql(u8, target_layer[0..t_str_len], layer_name[0..l_str_len])) { 72 | break :inner; 73 | } 74 | } else return false; // if our loop never break, then a requested layer is missing 75 | } 76 | 77 | return true; 78 | } 79 | 80 | pub fn messageCallback( 81 | message_severity: vk.DebugUtilsMessageSeverityFlagsEXT, 82 | message_types: vk.DebugUtilsMessageTypeFlagsEXT, 83 | p_callback_data: ?*const vk.DebugUtilsMessengerCallbackDataEXT, 84 | p_user_data: ?*anyopaque, 85 | ) callconv(vk.vulkan_call_conv) vk.Bool32 { 86 | _ = p_user_data; 87 | _ = message_types; 88 | 89 | const error_mask = comptime blk: { 90 | break :blk vk.DebugUtilsMessageSeverityFlagsEXT{ 91 | .warning_bit_ext = true, 92 | .error_bit_ext = true, 93 | }; 94 | }; 95 | const is_severe = (error_mask.toInt() & message_severity.toInt()) > 0; 96 | const writer = if (is_severe) std.io.getStdErr().writer() else std.io.getStdOut().writer(); 97 | if (p_callback_data) |data| { 98 | const msg = data.p_message orelse ""; 99 | writer.print("validation layer: {s}\n", .{msg}) catch { 100 | std.debug.print("error from stdout print in message callback", .{}); 101 | }; 102 | } 103 | 104 | return vk.FALSE; 105 | } 106 | -------------------------------------------------------------------------------- /src/modules/render/vk_utils.zig: -------------------------------------------------------------------------------- 1 | /// vk_utils contains utility functions for the vulkan API to reduce boiler plate 2 | 3 | // TODO: most of these functions are only called once in the codebase, move to where they are 4 | // relevant, and those who are only called in one file should be in that file 5 | 6 | const std = @import("std"); 7 | const Allocator = std.mem.Allocator; 8 | 9 | const vk = @import("vulkan"); 10 | const dispatch = @import("dispatch.zig"); 11 | 12 | const Context = @import("Context.zig"); 13 | 14 | /// Check if extensions are available on host instance 15 | pub fn isInstanceExtensionsPresent(allocator: Allocator, vkb: dispatch.Base, target_extensions: []const [*:0]const u8) !bool { 16 | // query extensions available 17 | var supported_extensions_count: u32 = 0; 18 | // TODO: handle "VkResult.incomplete" 19 | _ = try vkb.enumerateInstanceExtensionProperties(null, &supported_extensions_count, null); 20 | 21 | var extensions = try std.ArrayList(vk.ExtensionProperties).initCapacity(allocator, supported_extensions_count); 22 | defer extensions.deinit(); 23 | 24 | _ = try vkb.enumerateInstanceExtensionProperties(null, &supported_extensions_count, extensions.items.ptr); 25 | extensions.items.len = supported_extensions_count; 26 | 27 | var matches: u32 = 0; 28 | for (target_extensions) |target_extension| { 29 | const t_str_len = std.mem.indexOfScalar(u8, target_extension[0..vk.MAX_EXTENSION_NAME_SIZE], 0) orelse continue; 30 | cmp: for (extensions.items) |existing| { 31 | const existing_name: [*:0]const u8 = @ptrCast(&existing.extension_name); 32 | const e_str_len = std.mem.indexOfScalar(u8, existing_name[0..vk.MAX_EXTENSION_NAME_SIZE], 0) orelse continue; 33 | if (std.mem.eql(u8, target_extension[0..t_str_len], existing_name[0..e_str_len])) { 34 | matches += 1; 35 | break :cmp; 36 | } 37 | } 38 | } 39 | 40 | return matches == target_extensions.len; 41 | } 42 | 43 | pub inline fn findMemoryTypeIndex(ctx: Context, type_filter: u32, memory_flags: vk.MemoryPropertyFlags) !u32 { 44 | const properties = ctx.vki.getPhysicalDeviceMemoryProperties(ctx.physical_device); 45 | { 46 | var i: u32 = 0; 47 | while (i < properties.memory_type_count) : (i += 1) { 48 | const left_shift: u5 = @intCast(i); 49 | const correct_type: bool = (type_filter & (@as(u32, 1) << left_shift)) != 0; 50 | if (correct_type and (properties.memory_types[i].property_flags.toInt() & memory_flags.toInt()) == memory_flags.toInt()) { 51 | return i; 52 | } 53 | } 54 | } 55 | 56 | return error.NotFound; 57 | } 58 | 59 | pub inline fn beginOneTimeCommandBuffer(ctx: Context, command_pool: vk.CommandPool) !vk.CommandBuffer { 60 | const allocate_info = vk.CommandBufferAllocateInfo{ 61 | .command_pool = command_pool, 62 | .level = .primary, 63 | .command_buffer_count = 1, 64 | }; 65 | var command_buffer: vk.CommandBuffer = undefined; 66 | try ctx.vkd.allocateCommandBuffers(ctx.logical_device, &allocate_info, @ptrCast(&command_buffer)); 67 | 68 | const begin_info = vk.CommandBufferBeginInfo{ 69 | .flags = .{ 70 | .one_time_submit_bit = true, 71 | }, 72 | .p_inheritance_info = null, 73 | }; 74 | try ctx.vkd.beginCommandBuffer(command_buffer, &begin_info); 75 | 76 | return command_buffer; 77 | } 78 | 79 | // TODO: synchronization should be improved in this function (currently very sub optimal)! 80 | pub inline fn endOneTimeCommandBuffer(ctx: Context, command_pool: vk.CommandPool, command_buffer: vk.CommandBuffer) !void { 81 | try ctx.vkd.endCommandBuffer(command_buffer); 82 | 83 | { 84 | @setRuntimeSafety(false); 85 | const semo_null_ptr: [*c]const vk.Semaphore = null; 86 | const wait_null_ptr: [*c]const vk.PipelineStageFlags = null; 87 | // perform the compute ray tracing, draw to target texture 88 | const submit_info = vk.SubmitInfo{ 89 | .wait_semaphore_count = 0, 90 | .p_wait_semaphores = semo_null_ptr, 91 | .p_wait_dst_stage_mask = wait_null_ptr, 92 | .command_buffer_count = 1, 93 | .p_command_buffers = @ptrCast(&command_buffer), 94 | .signal_semaphore_count = 0, 95 | .p_signal_semaphores = semo_null_ptr, 96 | }; 97 | try ctx.vkd.queueSubmit(ctx.graphics_queue, 1, @ptrCast(&submit_info), .null_handle); 98 | } 99 | 100 | try ctx.vkd.queueWaitIdle(ctx.graphics_queue); 101 | 102 | ctx.vkd.freeCommandBuffers(ctx.logical_device, command_pool, 1, @ptrCast(&command_buffer)); 103 | } 104 | -------------------------------------------------------------------------------- /src/modules/voxel_rt/brick/State.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Mutex = std.Thread.Mutex; 3 | 4 | pub const AtomicCount = std.atomic.Value(u32); 5 | pub const brick_dimension: u32 = 4; 6 | pub const brick_bits: u32 = brick_dimension * brick_dimension * brick_dimension; 7 | pub const brick_bytes: u32 = brick_bits / 8; 8 | pub const brick_words: u32 = brick_bytes / 4; 9 | pub const brick_log2: u32 = std.math.log2_int(u32, brick_bits); 10 | pub const BrickMap = std.meta.Int(.unsigned, State.brick_bits); 11 | pub const BrickMapLog2 = std.meta.Int(.unsigned, State.brick_log2); 12 | 13 | /// type used to record changes in host/device buffers in order to only send changed data to the gpu 14 | pub const DeviceDataDelta = struct { 15 | pub const empty = DeviceDataDelta{ 16 | .mutex = .{}, 17 | .state = .inactive, 18 | .from = 0, 19 | .to = 0, 20 | }; 21 | 22 | const DeltaState = enum { 23 | invalid, 24 | inactive, 25 | active, 26 | }; 27 | 28 | mutex: Mutex, 29 | state: DeltaState, 30 | from: usize, 31 | to: usize, 32 | 33 | pub fn resetDelta(self: *DeviceDataDelta) void { 34 | self.state = .inactive; 35 | self.from = std.math.maxInt(usize); 36 | self.to = std.math.minInt(usize); 37 | } 38 | 39 | pub fn registerDelta(self: *DeviceDataDelta, delta_index: usize) void { 40 | self.mutex.lock(); 41 | defer self.mutex.unlock(); 42 | 43 | self.state = .active; 44 | self.from = @min(self.from, delta_index); 45 | self.to = @max(self.to, delta_index + 1); 46 | } 47 | 48 | /// register a delta range 49 | pub fn registerDeltaRange(self: *DeviceDataDelta, from: usize, to: usize) void { 50 | self.mutex.lock(); 51 | defer self.mutex.unlock(); 52 | 53 | self.state = .active; 54 | self.from = @min(self.from, from); 55 | self.to = @max(self.to, to + 1); 56 | } 57 | }; 58 | 59 | // uniform binding: 2 60 | pub const Device = extern struct { 61 | // how many voxels in each axis 62 | voxel_dim_x: u32, 63 | voxel_dim_y: u32, 64 | voxel_dim_z: u32, 65 | // how many bricks in each axis 66 | dim_x: u32, 67 | dim_y: u32, 68 | dim_z: u32, 69 | 70 | padding1: u32 = 0, 71 | padding2: u32 = 0, 72 | 73 | // holds the min point, and the base t advance 74 | // base t advance dictate the minimum stretch of distance a ray can go for each iteration 75 | // at 0.1 it will move atleast 10% of a given voxel 76 | min_point_base_t: [4]f32, 77 | // holds the max_point, and the brick scale 78 | max_point_scale: [4]f32, 79 | }; 80 | 81 | // pub const Unloaded = packed struct { 82 | // lod_color: u24, 83 | // flags: u6, 84 | // }; 85 | 86 | pub const BrickStatusMask = extern struct { 87 | pub const Status = enum(u2) { 88 | empty = 0, 89 | loaded = 1, 90 | }; 91 | 92 | bits: c_uint, 93 | 94 | pub fn write(self: *BrickStatusMask, state: Status, at: u5) void { 95 | // zero out bits 96 | self.bits &= ~(@as(u32, 0b1) << at); 97 | const state_bit: u32 = @intCast(@intFromEnum(state)); 98 | self.bits |= state_bit << at; 99 | } 100 | 101 | pub fn read(self: BrickStatusMask, at: u5) Status { 102 | var bits = self.bits; 103 | bits &= @as(u32, 0b1) << at; 104 | bits = bits >> at; 105 | return @enumFromInt(@as(u2, @intCast(bits))); 106 | } 107 | }; 108 | 109 | pub const IndexToBrick = c_uint; 110 | 111 | pub const Brick = struct { 112 | pub const IndexType = enum(u1) { 113 | voxel_start_index, 114 | brick_lod_index, 115 | }; 116 | 117 | pub const StartIndex = packed struct(u32) { 118 | value: u31, 119 | type: IndexType, 120 | }; 121 | 122 | const unset_bits: u32 = std.math.maxInt(u32); 123 | pub const unset_index: StartIndex = @bitCast(unset_bits); 124 | 125 | pub const empty: Occupancy = [_]u8{0} ** brick_bytes; 126 | pub const Occupancy = [brick_bytes]u8; 127 | }; 128 | 129 | pub const MaterialIndices = u8; 130 | 131 | const State = @This(); 132 | 133 | brick_statuses: []BrickStatusMask, 134 | brick_statuses_delta: DeviceDataDelta = .empty, 135 | brick_indices: []IndexToBrick, 136 | brick_indices_delta: DeviceDataDelta = .empty, 137 | 138 | // we keep a single bricks delta structure since active_bricks is shared 139 | bricks_occupancy_delta: DeviceDataDelta = .empty, 140 | brick_occupancy: []u8, 141 | 142 | bricks_start_indices_delta: DeviceDataDelta = .empty, 143 | brick_start_indices: []Brick.StartIndex, 144 | 145 | material_indices_delta: DeviceDataDelta = .empty, 146 | // assigned through a bucket 147 | material_indices: []MaterialIndices, 148 | 149 | /// how many bricks are used in the grid, keep in mind that this is used in a multithread context 150 | active_bricks: AtomicCount, 151 | 152 | device_state: Device, 153 | 154 | // used to determine which worker is scheduled a job 155 | work_segment_size: usize, 156 | -------------------------------------------------------------------------------- /assets/shaders/psrdnoise3.comp: -------------------------------------------------------------------------------- 1 | // psrdnoise (c) Stefan Gustavson and Ian McEwan, 2 | // ver. 2021-12-02, published under the MIT license: 3 | // https://github.com/stegu/psrdnoise/ 4 | 5 | // PLEASE NOTE THAT THE CODE IS MODIFIED 6 | 7 | // Permutation polynomial for the hash value 8 | vec4 permute(vec4 i) { 9 | vec4 im = mod(i, 289.0); 10 | return mod(fma(im, vec4(34.0), vec4(10.0)) * im, 289.0); 11 | } 12 | 13 | // 14 | // 3-D tiling simplex noise with rotating gradients and first order 15 | // analytical derivatives. 16 | // "vec3 x" is the point (x,y,z) to evaluate 17 | // "vec3 period" is the desired periods along x,y,z, up to 289. 18 | // (If Perlin's grid is used, multiples of 3 up to 288 are allowed.) 19 | // "float alpha" is the rotation (in radians) for the swirling gradients. 20 | // The "float" return value is the noise value, and 21 | // the "out vec3 gradient" argument returns the x,y,z partial derivatives. 22 | // 23 | // The function executes 15-20% faster if alpha is constant == 0.0 24 | // across all fragments being executed in parallel. 25 | // (This speedup will not happen if FASTROTATION is enabled. Do not specify 26 | // FASTROTATION if you are not actually going to use the rotation.) 27 | // 28 | // Setting any period to 0.0 or a negative value will skip the periodic 29 | // wrap for that dimension. Setting all periods to 0.0 makes the function 30 | // execute 10-15% faster. 31 | // 32 | // Not using the return value for the gradient will make the compiler 33 | // eliminate the code for computing it. This speeds up the function by 34 | // around 10%. 35 | // 36 | float psrdnoise(vec3 x, vec3 period, float alpha, out vec3 gradient) 37 | { 38 | const mat3 M = mat3(0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 0.0); 39 | const mat3 Mi = mat3(-0.5, 0.5, 0.5, 0.5,-0.5, 0.5, 0.5, 0.5, -0.5); 40 | 41 | const vec3 uvw = M * x; 42 | vec3 i0 = floor(uvw); 43 | 44 | const vec3 f0 = fract(uvw); 45 | const vec3 g_ = step(f0.xyx, f0.yzz), l_ = 1.0 - g_; 46 | const vec3 g = vec3(l_.z, g_.xy), l = vec3(l_.xy, g_.z); 47 | const vec3 o1 = min( g, l ), o2 = max( g, l ); 48 | vec3 i1 = i0 + o1, i2 = i0 + o2; 49 | vec3 i3 = i0 + vec3(1.0); 50 | 51 | const vec3 v0 = Mi * i0; 52 | const vec3 v1 = Mi * i1; 53 | const vec3 v2 = Mi * i2; 54 | const vec3 v3 = Mi * i3; 55 | const vec3 x0 = x - v0; 56 | const vec3 x1 = x - v1; 57 | const vec3 x2 = x - v2; 58 | const vec3 x3 = x - v3; 59 | 60 | if(any(greaterThan(period, vec3(0.0)))) { 61 | vec4 vx = vec4(v0.x, v1.x, v2.x, v3.x); 62 | vec4 vy = vec4(v0.y, v1.y, v2.y, v3.y); 63 | vec4 vz = vec4(v0.z, v1.z, v2.z, v3.z); 64 | 65 | if(period.x > 0.0) vx = mod(vx, period.x); 66 | if(period.y > 0.0) vy = mod(vy, period.y); 67 | if(period.z > 0.0) vz = mod(vz, period.z); 68 | 69 | i0 = floor(M * vec3(vx.x, vy.x, vz.x) + 0.5); 70 | i1 = floor(M * vec3(vx.y, vy.y, vz.y) + 0.5); 71 | i2 = floor(M * vec3(vx.z, vy.z, vz.z) + 0.5); 72 | i3 = floor(M * vec3(vx.w, vy.w, vz.w) + 0.5); 73 | } 74 | 75 | const vec4 hash = permute(permute(permute(vec4(i0.z, i1.z, i2.z, i3.z)) + vec4(i0.y, i1.y, i2.y, i3.y)) + vec4(i0.x, i1.x, i2.x, i3.x)); 76 | const vec4 theta = hash * 3.883222077; 77 | const vec4 sz = fma(hash, vec4(-0.006920415), vec4(0.996539792)); 78 | const vec4 psi = hash * 0.108705628; 79 | const vec4 Ct = cos(theta); 80 | const vec4 St = sin(theta); 81 | const vec4 sz_prime = sqrt(1.0 - sz * sz); 82 | vec4 gx, gy, gz; 83 | if(alpha != 0.0) { 84 | const vec4 px = Ct * sz_prime; 85 | const vec4 py = St * sz_prime; 86 | const vec4 Sp = sin(psi); 87 | const vec4 Cp = cos(psi); 88 | const vec4 Ctp = fma(St, Sp, -(Ct * Cp)); 89 | const vec4 qx = mix(Ctp * St, Sp, sz); 90 | const vec4 qy = mix(-Ctp * Ct, Cp, sz); 91 | const vec4 qz = -fma(py, Cp, px * Sp); 92 | const vec4 Sa = vec4(sin(alpha)); 93 | const vec4 Ca = vec4(cos(alpha)); 94 | gx = fma(Ca, px, Sa * qx); 95 | gy = fma(Ca, py, Sa * qy); 96 | gz = fma(Ca, sz, Sa * qz); 97 | } else { 98 | gx = Ct * sz_prime; 99 | gy = St * sz_prime; 100 | gz = sz; 101 | } 102 | 103 | const vec3 g0 = vec3(gx.x, gy.x, gz.x); 104 | const vec3 g1 = vec3(gx.y, gy.y, gz.y); 105 | const vec3 g2 = vec3(gx.z, gy.z, gz.z); 106 | const vec3 g3 = vec3(gx.w, gy.w, gz.w); 107 | 108 | const vec4 w = max(0.5 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0); 109 | const vec4 w2 = w * w; 110 | const vec4 w3 = w2 * w; 111 | 112 | const vec4 gdotx = vec4(dot(g0, x0), dot(g1,x1), dot(g2,x2), dot(g3,x3)); 113 | const float n = dot(w3, gdotx); 114 | const vec4 dw = -6.0 * w2 * gdotx; 115 | const vec3 dn0 = fma(vec3(w3.x), g0, vec3(dw.x * x0) ); 116 | const vec3 dn1 = fma(vec3(w3.y), g1, vec3(dw.y * x1) ); 117 | const vec3 dn2 = fma(vec3(w3.z), g2, vec3(dw.z * x2) ); 118 | const vec3 dn3 = fma(vec3(w3.w), g3, vec3(dw.w * x3) ); 119 | gradient = 39.5 * (dn0 + dn1 + dn2 + dn3); 120 | return 39.5 * n; 121 | } 122 | -------------------------------------------------------------------------------- /src/modules/voxel_rt/terrain/perlin.zig: -------------------------------------------------------------------------------- 1 | // This code is directly taken from ray tracing the next week 2 | // - https://raytracing.github.io/books/RayTracingTheNextWeek.html#perlinnoise 3 | // The original source is c++ and changes have been made to accomodate Zig 4 | 5 | const std = @import("std"); 6 | 7 | // TODO: point_count might have to be locked to exp of 2 i.e 2^8 8 | pub fn PerlinNoiseGenerator(comptime point_count: u32) type { 9 | // TODO: argument and input validation on PermInt & NoiseFloat 10 | const PermInt: type = i32; 11 | const NoiseFloat: type = f64; 12 | 13 | return struct { 14 | const Perlin = @This(); 15 | 16 | rand_float: [point_count]NoiseFloat, 17 | perm_x: [point_count]PermInt, 18 | perm_y: [point_count]PermInt, 19 | perm_z: [point_count]PermInt, 20 | rng: std.Random, 21 | 22 | pub fn init(seed: u64) Perlin { 23 | var prng = std.Random.DefaultPrng.init(seed); 24 | const rng = prng.random(); 25 | const generate_perm_fn = struct { 26 | inline fn generate_perm(random: std.Random) [point_count]PermInt { 27 | var perm: [point_count]PermInt = undefined; 28 | // TODO: replace for with while to avoid casting in loop 29 | for (&perm, 0..) |*p, i| { 30 | p.* = @intCast(i); 31 | } 32 | 33 | { 34 | var i: usize = point_count - 1; 35 | while (i > 0) : (i -= 1) { 36 | const target = random.intRangeLessThan(usize, 0, i); 37 | const tmp = perm[i]; 38 | perm[i] = perm[target]; 39 | perm[target] = tmp; 40 | } 41 | } 42 | return perm; 43 | } 44 | }.generate_perm; 45 | 46 | var rand_float: [point_count]NoiseFloat = undefined; 47 | for (&rand_float) |*float| { 48 | float.* = rng.float(NoiseFloat); 49 | } 50 | const perm_x = generate_perm_fn(rng); 51 | const perm_y = generate_perm_fn(rng); 52 | const perm_z = generate_perm_fn(rng); 53 | 54 | return Perlin{ 55 | .rand_float = rand_float, 56 | .perm_x = perm_x, 57 | .perm_y = perm_y, 58 | .perm_z = perm_z, 59 | .rng = rng, 60 | }; 61 | } 62 | 63 | pub fn noise(self: Perlin, comptime PointType: type, point: [3]PointType) NoiseFloat { 64 | comptime { 65 | const info = @typeInfo(PointType); 66 | switch (info) { 67 | .Float => {}, 68 | else => @compileError("PointType must be a float type"), 69 | } 70 | } 71 | 72 | const and_value = point_count - 1; 73 | const i = @as(usize, @intFromFloat(4 * point[0])) & and_value; 74 | const j = @as(usize, @intFromFloat(4 * point[2])) & and_value; 75 | const k = @as(usize, @intFromFloat(4 * point[1])) & and_value; 76 | 77 | return self.rand_float[@intCast(self.perm_x[i] ^ self.perm_y[j] ^ self.perm_z[k])]; 78 | } 79 | 80 | pub fn smoothNoise(self: Perlin, comptime PointType: type, point: [3]PointType) NoiseFloat { 81 | comptime { 82 | const info = @typeInfo(PointType); 83 | switch (info) { 84 | .float => {}, 85 | else => @compileError("PointType must be a float type"), 86 | } 87 | } 88 | 89 | var c: [2][2][2]NoiseFloat = undefined; 90 | { 91 | const and_value = point_count - 1; 92 | 93 | const i: usize = @intFromFloat(@floor(point[0])); 94 | const j: usize = @intFromFloat(@floor(point[1])); 95 | const k: usize = @intFromFloat(@floor(point[2])); 96 | var di: usize = 0; 97 | while (di < 2) : (di += 1) { 98 | var dj: usize = 0; 99 | while (dj < 2) : (dj += 1) { 100 | var dk: usize = 0; 101 | while (dk < 2) : (dk += 1) { 102 | c[di][dj][dk] = self.rand_float[ 103 | @intCast( 104 | self.perm_x[(i + di) & and_value] ^ 105 | self.perm_y[(j + dj) & and_value] ^ 106 | self.perm_z[(k + dk) & and_value], 107 | ) 108 | ]; 109 | } 110 | } 111 | } 112 | } 113 | 114 | const u = blk: { 115 | const tmp = point[0] - @floor(point[0]); 116 | break :blk tmp * tmp * (3 - 2 * tmp); 117 | }; 118 | const v = blk: { 119 | const tmp = point[1] - @floor(point[1]); 120 | break :blk tmp * tmp * (3 - 2 * tmp); 121 | }; 122 | const w = blk: { 123 | const tmp = point[2] - @floor(point[2]); 124 | break :blk tmp * tmp * (3 - 2 * tmp); 125 | }; 126 | 127 | // perform trilinear filtering 128 | var accum: NoiseFloat = 0; 129 | { 130 | var i: usize = 0; 131 | while (i < 2) : (i += 1) { 132 | const fi: NoiseFloat = @floatFromInt(i); 133 | var j: usize = 0; 134 | while (j < 2) : (j += 1) { 135 | const fj: NoiseFloat = @floatFromInt(j); 136 | var k: usize = 0; 137 | while (k < 2) : (k += 1) { 138 | const fk: NoiseFloat = @floatFromInt(k); 139 | accum += (fi * u + (1 - fi) * (1 - u)) * 140 | (fj * v + (1 - fj) * (1 - v)) * 141 | (fk * w + (1 - fk) * (1 - w)) * c[i][j][k]; 142 | } 143 | } 144 | } 145 | } 146 | return accum; 147 | } 148 | }; 149 | } 150 | -------------------------------------------------------------------------------- /src/modules/voxel_rt/Camera.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const za = @import("zalgebra"); 3 | const Vec3 = @Vector(3, f32); 4 | 5 | pub const Config = struct { 6 | viewport_height: f32 = 2, 7 | origin: Vec3 = za.Vec3.zero().data, 8 | samples_per_pixel: i32 = 2, 9 | max_bounce: i32 = 2, 10 | turn_rate: f32 = 0.1, 11 | normal_speed: f32 = 1, 12 | sprint_speed: f32 = 2, 13 | user_input_diabled: bool = false, 14 | }; 15 | 16 | const Camera = @This(); 17 | 18 | turn_rate: f32, 19 | 20 | normal_speed: f32, 21 | sprint_speed: f32, 22 | movement_speed: f32, 23 | 24 | user_input_diabled: bool, 25 | 26 | /// changes to viewport_x should call propogatePitchChange 27 | viewport_width: f32, 28 | viewport_height: f32, 29 | 30 | pitch: za.Quat, 31 | yaw: za.Quat, 32 | 33 | vertical_fov: f32, 34 | d_camera: Device, 35 | 36 | pub fn init(vertical_fov: f32, image_width: u32, image_height: u32, config: Config) Camera { 37 | const aspect_ratio: f32 = @as(f32, @floatFromInt(image_width)) / @as(f32, @floatFromInt(image_height)); 38 | 39 | const a: comptime_float = std.math.pi * (1.0 / 180.0); 40 | const viewport_height = blk: { 41 | const theta = vertical_fov * a; 42 | const height = config.viewport_height; 43 | break :blk height * @tan(theta * 0.5); 44 | }; 45 | const viewport_width = aspect_ratio * viewport_height; 46 | 47 | const forward = za.Vec3.forward(); 48 | const right = za.Vec3.up().cross(forward).norm(); 49 | const up = forward.cross(right).norm(); 50 | 51 | const horizontal = right.scale(viewport_width); 52 | const vertical = up.scale(viewport_height); 53 | const lower_left_corner = config.origin - horizontal.scale(0.5).data - vertical.scale(0.5).data - forward.data; 54 | 55 | return Camera{ 56 | .turn_rate = config.turn_rate, 57 | .normal_speed = config.normal_speed, 58 | .sprint_speed = config.sprint_speed, 59 | .movement_speed = config.normal_speed, 60 | .user_input_diabled = config.user_input_diabled, 61 | .viewport_width = viewport_width, 62 | .viewport_height = viewport_height, 63 | .vertical_fov = vertical_fov, 64 | .pitch = za.Quat.identity(), 65 | .yaw = za.Quat.identity(), 66 | .d_camera = Device{ 67 | .image_width = image_width, 68 | .image_height = image_height, 69 | .horizontal = horizontal.data, 70 | .vertical = vertical.data, 71 | .lower_left_corner = lower_left_corner, 72 | .origin = config.origin, 73 | .samples_per_pixel = config.samples_per_pixel, 74 | .max_bounce = config.max_bounce + 1, // + 1 so that max bounce of 0 means only primary ray for the user of API ... 75 | }, 76 | }; 77 | } 78 | 79 | /// set camera movement speed to sprint 80 | pub fn activateSprint(self: *Camera) void { 81 | self.movement_speed = self.normal_speed * self.sprint_speed; 82 | } 83 | 84 | /// set camera movement speed to normal speed 85 | pub fn disableSprint(self: *Camera) void { 86 | self.movement_speed = self.normal_speed; 87 | } 88 | 89 | pub fn setOrigin(self: *Camera, origin: Vec3) void { 90 | self.d_camera.origin = origin; 91 | self.propogatePitchChange(); 92 | } 93 | 94 | pub fn disableInput(self: *Camera) void { 95 | self.user_input_diabled = true; 96 | } 97 | 98 | pub fn enableInput(self: *Camera) void { 99 | self.user_input_diabled = false; 100 | } 101 | 102 | /// camera should always be reset after being used 103 | /// programtically to avoid invalid camera state 104 | pub fn reset(self: *Camera) void { 105 | self.enableInput(); 106 | self.yaw = za.Quat.identity(); 107 | self.pitch = za.Quat.identity(); 108 | self.propogatePitchChange(); 109 | } 110 | 111 | /// Move camera 112 | pub fn translate(self: *Camera, delta_time: f32, by: za.Vec3) void { 113 | if (self.user_input_diabled) return; 114 | 115 | const norm = by.norm(); 116 | const delta = self.orientation().rotateVec(norm.scale(delta_time * self.movement_speed)); 117 | if (std.math.isNan(delta.x())) { 118 | return; 119 | } 120 | self.d_camera.origin += delta.data; 121 | self.propogatePitchChange(); 122 | } 123 | 124 | pub fn turnPitch(self: *Camera, angle: f32) void { 125 | if (self.user_input_diabled) return; 126 | 127 | // Axis angle to quaternion: https://www.euclideanspace.com/maths/geometry/rotations/conversions/angleToQuaternion/index.htm 128 | const h_angle = angle * self.turn_rate; 129 | const i = @sin(h_angle); 130 | const w = @cos(h_angle); 131 | const prev_pitch = self.pitch; 132 | self.pitch = self.pitch.mul(za.Quat{ .w = w, .x = i, .y = 0.0, .z = 0.0 }); 133 | 134 | // arbitrary restrict rotation so that camera does not become inversed 135 | const euler_x_rotation = self.pitch.extractEulerAngles().x(); 136 | if (@abs(euler_x_rotation) >= 90) { 137 | self.pitch = prev_pitch; 138 | } 139 | 140 | self.propogatePitchChange(); 141 | } 142 | 143 | pub fn turnYaw(self: *Camera, angle: f32) void { 144 | if (self.user_input_diabled) return; 145 | 146 | const h_angle = angle * self.turn_rate; 147 | const j = @sin(h_angle); 148 | const w = @cos(h_angle); 149 | self.yaw = self.yaw.mul(za.Quat{ .w = w, .x = 0.0, .y = j, .z = 0.0 }); 150 | self.propogatePitchChange(); 151 | } 152 | 153 | pub inline fn orientation(self: Camera) za.Quat { 154 | return self.yaw.mul(self.pitch).norm(); 155 | } 156 | 157 | /// Get byte size of Camera's GPU data 158 | pub inline fn getGpuSize() u64 { 159 | return @sizeOf(Device); 160 | } 161 | 162 | inline fn forwardDir(self: Camera) za.Vec3 { 163 | return self.orientation().rotateVec(za.Vec3.new(0, 0, 1)); 164 | } 165 | 166 | // used to update values that depend on camera orientation 167 | pub inline fn propogatePitchChange(self: *Camera) void { 168 | const forward = self.forwardDir(); 169 | const right = za.Vec3.up().cross(forward).norm(); 170 | const up = forward.cross(right).norm(); 171 | 172 | self.d_camera.horizontal = right.scale(self.viewport_width).data; 173 | self.d_camera.vertical = up.scale(self.viewport_height).data; 174 | self.d_camera.lower_left_corner = self.lowerLeftCorner(); 175 | } 176 | 177 | inline fn lowerLeftCorner(self: Camera) Vec3 { 178 | const @"0.5": Vec3 = @splat(0.5); 179 | return self.d_camera.origin - self.d_camera.horizontal * @"0.5" - self.d_camera.vertical * @"0.5" - self.forwardDir().data; 180 | } 181 | 182 | // uniform Camera, binding: 1 183 | pub const Device = extern struct { 184 | image_width: u32, 185 | image_height: u32, 186 | 187 | horizontal: Vec3, 188 | vertical: Vec3, 189 | lower_left_corner: Vec3, 190 | origin: Vec3, 191 | samples_per_pixel: i32, 192 | max_bounce: i32, 193 | }; 194 | -------------------------------------------------------------------------------- /src/modules/VoxelRT.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | 4 | const tracy = @import("ztracy"); 5 | 6 | const za = @import("zalgebra"); 7 | const Vec2 = @Vector(2, f32); 8 | 9 | const render = @import("../modules/render.zig"); 10 | const Context = render.Context; 11 | 12 | const Pipeline = @import("voxel_rt/Pipeline.zig"); 13 | pub const Camera = @import("voxel_rt/Camera.zig"); 14 | pub const Sun = @import("voxel_rt/Sun.zig"); 15 | pub const BrickGrid = @import("voxel_rt/brick/Grid.zig"); 16 | pub const GridState = @import("voxel_rt/brick/State.zig"); 17 | pub const Benchmark = @import("voxel_rt/Benchmark.zig"); 18 | pub const gpu_types = @import("voxel_rt/gpu_types.zig"); 19 | pub const terrain = @import("voxel_rt/terrain/terrain.zig"); 20 | pub const vox = @import("voxel_rt/vox/loader.zig"); 21 | 22 | pub const Config = struct { 23 | internal_resolution_width: u32 = 1280, 24 | internal_resolution_height: u32 = 720, 25 | pipeline: Pipeline.Config = .{}, 26 | camera: Camera.Config = .{}, 27 | sun: Sun.Config = .{}, 28 | }; 29 | 30 | const VoxelRT = @This(); 31 | 32 | camera: *Camera, 33 | sun: *Sun, 34 | 35 | brick_grid: *BrickGrid, 36 | pipeline: Pipeline, 37 | 38 | /// init VoxelRT, api takes ownership of the brick_grid 39 | pub fn init(allocator: Allocator, ctx: Context, brick_grid: *BrickGrid, config: Config) !VoxelRT { 40 | const camera = try allocator.create(Camera); 41 | errdefer allocator.destroy(camera); 42 | camera.* = Camera.init(75, config.internal_resolution_width, config.internal_resolution_height, config.camera); 43 | 44 | const sun = try allocator.create(Sun); 45 | errdefer allocator.destroy(sun); 46 | sun.* = Sun.init(config.sun); 47 | 48 | var pipeline = try Pipeline.init( 49 | ctx, 50 | allocator, 51 | .{ 52 | .width = config.internal_resolution_width, 53 | .height = config.internal_resolution_height, 54 | }, 55 | brick_grid.state.*, 56 | camera, 57 | sun, 58 | config.pipeline, 59 | ); 60 | errdefer pipeline.deinit(ctx); 61 | 62 | try pipeline.transferGridState(ctx, brick_grid.state.*); 63 | 64 | return VoxelRT{ 65 | .camera = camera, 66 | .sun = sun, 67 | .brick_grid = brick_grid, 68 | .pipeline = pipeline, 69 | }; 70 | } 71 | 72 | pub fn createBenchmark(self: *VoxelRT) Benchmark { 73 | return Benchmark.init(self.camera, self.brick_grid.state.*, self.sun.device_data.enabled > 0); 74 | } 75 | 76 | pub fn draw(self: *VoxelRT, ctx: Context, delta_time: f32) !void { 77 | try self.pipeline.draw(ctx, delta_time); 78 | } 79 | 80 | pub fn updateSun(self: *VoxelRT, delta_time: f32) void { 81 | self.sun.update(delta_time); 82 | } 83 | 84 | /// push the materials to GPU 85 | pub fn pushMaterials(self: *VoxelRT, ctx: Context, materials: []const gpu_types.Material) !void { 86 | try self.pipeline.transferMaterials(ctx, 0, materials); 87 | } 88 | 89 | /// push the albedo to GPU 90 | pub fn pushAlbedo(self: *VoxelRT, ctx: Context, albedos: []const gpu_types.Albedo) !void { 91 | try self.pipeline.transferAlbedos(ctx, 0, albedos); 92 | } 93 | 94 | /// flush all grid data to GPU 95 | pub fn debugFlushGrid(self: *VoxelRT, ctx: Context) void { 96 | if (@import("builtin").mode != .Debug) { 97 | @compileError("calling " ++ @src().fn_name ++ " in " ++ @tagName(@import("builtin").mode)); 98 | } 99 | 100 | self.pipeline.transferBrickStatuses(ctx, 0, self.brick_grid.state.brick_statuses) catch unreachable; 101 | self.pipeline.transferBrickIndices(ctx, 0, self.brick_grid.state.brick_indices) catch unreachable; 102 | self.pipeline.transferBricks(ctx, 0, self.brick_grid.state.bricks) catch unreachable; 103 | self.pipeline.transferMaterialIndices(ctx, 0, self.brick_grid.state.material_indices) catch unreachable; 104 | } 105 | 106 | /// update grid device data based on changes 107 | pub fn updateGridDelta(self: *VoxelRT, ctx: Context) !void { 108 | { 109 | const transfer_zone = tracy.ZoneN(@src(), "grid type transfer"); 110 | defer transfer_zone.End(); 111 | 112 | const delta = &self.brick_grid.state.brick_statuses_delta; 113 | delta.mutex.lock(); 114 | defer delta.mutex.unlock(); 115 | 116 | if (delta.state == .active) { 117 | try self.pipeline.transferBrickStatuses(ctx, delta.from, self.brick_grid.state.brick_statuses[delta.from..delta.to]); 118 | delta.resetDelta(); 119 | } 120 | } 121 | { 122 | const transfer_zone = tracy.ZoneN(@src(), "grid index transfer"); 123 | defer transfer_zone.End(); 124 | 125 | const delta = &self.brick_grid.state.brick_indices_delta; 126 | delta.mutex.lock(); 127 | defer delta.mutex.unlock(); 128 | 129 | if (delta.state == .active) { 130 | try self.pipeline.transferBrickIndices(ctx, delta.from, self.brick_grid.state.brick_indices[delta.from..delta.to]); 131 | delta.resetDelta(); 132 | } 133 | } 134 | { 135 | const transfer_zone = tracy.ZoneN(@src(), "bricks occupancy transfer"); 136 | defer transfer_zone.End(); 137 | 138 | const delta = &self.brick_grid.state.bricks_occupancy_delta; 139 | delta.mutex.lock(); 140 | defer delta.mutex.unlock(); 141 | 142 | if (delta.state == .active) { 143 | try self.pipeline.transferBrickOccupancy(ctx, delta.from, self.brick_grid.state.brick_occupancy[delta.from..delta.to]); 144 | delta.resetDelta(); 145 | } 146 | } 147 | { 148 | const transfer_zone = tracy.ZoneN(@src(), "bricks start indices transfer"); 149 | defer transfer_zone.End(); 150 | 151 | const delta = &self.brick_grid.state.bricks_start_indices_delta; 152 | delta.mutex.lock(); 153 | defer delta.mutex.unlock(); 154 | 155 | if (delta.state == .active) { 156 | try self.pipeline.transferBrickStartIndex(ctx, delta.from, self.brick_grid.state.brick_start_indices[delta.from..delta.to]); 157 | delta.resetDelta(); 158 | } 159 | } 160 | { 161 | const transfer_zone = tracy.ZoneN(@src(), "material indices transfer"); 162 | defer transfer_zone.End(); 163 | const delta = &self.brick_grid.state.material_indices_delta; 164 | delta.mutex.lock(); 165 | defer delta.mutex.unlock(); 166 | 167 | if (delta.state == .active) { 168 | try self.pipeline.transferMaterialIndices(ctx, delta.from, self.brick_grid.state.material_indices[delta.from..delta.to]); 169 | delta.resetDelta(); 170 | } 171 | } 172 | } 173 | 174 | pub fn deinit(self: VoxelRT, allocator: Allocator, ctx: Context) void { 175 | allocator.destroy(self.camera); 176 | allocator.destroy(self.sun); 177 | self.pipeline.deinit(ctx); 178 | } 179 | -------------------------------------------------------------------------------- /src/modules/voxel_rt/terrain/terrain.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | 4 | const za = @import("zalgebra"); 5 | const stbi = @import("stbi"); 6 | 7 | const tracy = @import("ztracy"); 8 | 9 | const render = @import("../../render.zig"); 10 | const Context = render.Context; 11 | 12 | const BrickGrid = @import("../brick/Grid.zig"); 13 | 14 | const gpu_types = @import("../gpu_types.zig"); 15 | const Perlin = @import("perlin.zig").PerlinNoiseGenerator(256); 16 | 17 | const Material = enum(u8) { 18 | water = 0, 19 | grass, 20 | dirt, 21 | rock, 22 | 23 | pub fn getMaterialIndex(self: Material, rnd: std.Random) u8 { 24 | switch (self) { 25 | .water => return 0, 26 | .grass => { 27 | const roll = rnd.float(f32); 28 | return 1 + @as(u8, @intFromFloat(@round(roll))); 29 | }, 30 | .dirt => { 31 | const roll = rnd.float(f32); 32 | return 3 + @as(u8, @intFromFloat(@round(roll))); 33 | }, 34 | .rock => { 35 | const roll = rnd.float(f32); 36 | return 5 + @as(u8, @intFromFloat(@round(roll))); 37 | }, 38 | } 39 | } 40 | }; 41 | 42 | /// populate a voxel grid with perlin noise terrain on CPU 43 | pub fn generateCpu(comptime threads_count: usize, allocator: Allocator, seed: u64, scale: f32, ocean_level: usize, grid: *BrickGrid) !void { // TODO: return Terrain 44 | const zone = tracy.ZoneNS(@src(), "generate terrain chunk", 1); 45 | defer zone.End(); 46 | 47 | const perlin = blk: { 48 | const p = try allocator.create(Perlin); 49 | p.* = Perlin.init(seed); 50 | break :blk p; 51 | }; 52 | defer allocator.destroy(perlin); 53 | 54 | const voxel_dim = [3]f32{ 55 | @floatFromInt(grid.state.device_state.voxel_dim_x), 56 | @floatFromInt(grid.state.device_state.voxel_dim_y), 57 | @floatFromInt(grid.state.device_state.voxel_dim_z), 58 | }; 59 | const point_mod = [3]f32{ 60 | (1 / voxel_dim[0]) * scale, 61 | (1 / voxel_dim[1]) * scale, 62 | (1 / voxel_dim[2]) * scale, 63 | }; 64 | 65 | // create our gen function 66 | const insert_job_gen_fn = struct { 67 | pub fn insert(thread_id: usize, thread_name: [:0]const u8, perlin_: *const Perlin, voxel_dim_: [3]f32, point_mod_: [3]f32, ocean_level_v: usize, grid_: *BrickGrid) void { 68 | tracy.SetThreadName(thread_name.ptr); 69 | const gen_zone = tracy.ZoneN(@src(), "terrain gen"); 70 | defer gen_zone.End(); 71 | 72 | const thread_segment_size: f32 = blk: { 73 | if (threads_count == 0) { 74 | break :blk voxel_dim_[0]; 75 | } else { 76 | break :blk @ceil(voxel_dim_[0] / @as(f32, @floatFromInt(threads_count))); 77 | } 78 | }; 79 | 80 | const terrain_max_height: f32 = voxel_dim_[1] * 0.5; 81 | const inv_terrain_max_height = 1.0 / terrain_max_height; 82 | 83 | var point: [3]f32 = undefined; 84 | const thread_x_begin = thread_segment_size * @as(f32, @floatFromInt(thread_id)); 85 | const thread_x_end = @min(thread_x_begin + thread_segment_size, voxel_dim_[0]); 86 | var x: f32 = thread_x_begin; 87 | while (x < thread_x_end) : (x += 1) { 88 | const i_x: usize = @intFromFloat(x); 89 | var z: f32 = 0; 90 | while (z < voxel_dim_[2]) : (z += 1) { 91 | const i_z: usize = @intFromFloat(z); 92 | 93 | point[0] = x * point_mod_[0]; 94 | point[1] = 0; 95 | point[2] = z * point_mod_[2]; 96 | 97 | const height: usize = @intFromFloat(@min(perlin_.smoothNoise(f32, point), 1) * terrain_max_height); 98 | var i_y: usize = height / 2; 99 | while (i_y < height) : (i_y += 1) { 100 | const height_lerp = za.lerp(f32, 1, 3.4, @as(f32, @floatFromInt(i_y)) * inv_terrain_max_height); 101 | const material_value = height_lerp + perlin_.rng.float(f32) * 0.5; 102 | const material: Material = @enumFromInt(@as(u8, @intFromFloat(@floor(material_value)))); 103 | grid_.*.insert(i_x, i_y, i_z, material.getMaterialIndex(perlin_.rng)); 104 | } 105 | while (i_y < ocean_level_v) : (i_y += 1) { 106 | grid_.*.insert(i_x, i_y, i_z, 0); // insert water 107 | } 108 | } 109 | } 110 | } 111 | }.insert; 112 | 113 | if (threads_count == 0) { 114 | // run on main thread 115 | @call(.{ .modifier = .always_inline }, insert_job_gen_fn, .{ 0, perlin, voxel_dim, point_mod, ocean_level, grid }); 116 | } else { 117 | var threads: [threads_count]std.Thread = undefined; 118 | comptime var i = 0; 119 | inline while (i < threads_count) : (i += 1) { 120 | const thread_name = comptime std.fmt.comptimePrint("terrain thread {d}", .{i}); 121 | threads[i] = try std.Thread.spawn(.{}, insert_job_gen_fn, .{ i, thread_name, perlin, voxel_dim, point_mod, ocean_level, grid }); 122 | } 123 | i = 0; 124 | inline while (i < threads_count) : (i += 1) { 125 | threads[i].join(); 126 | } 127 | } 128 | } 129 | 130 | pub const materials = [_]gpu_types.Material{ 131 | // Water 132 | .{ 133 | .type = .dielectric, 134 | // Water is 1.3333... glass is 1.52 135 | .albedo_r = 0.117, 136 | .albedo_g = 0.45, 137 | .albedo_b = 0.85, 138 | .type_data = 1.333, 139 | }, 140 | // Grass 1 141 | .{ 142 | .type = .lambertian, 143 | .albedo_r = 0.0, 144 | .albedo_g = 0.6, 145 | .albedo_b = 0.0, 146 | .type_data = 0.0, 147 | }, 148 | // Grass 2 149 | .{ 150 | .type = .lambertian, 151 | .albedo_r = 0.0, 152 | .albedo_g = 0.5019, 153 | .albedo_b = 0.0, 154 | .type_data = 0.0, 155 | }, 156 | // Dirt 1 157 | .{ 158 | .type = .lambertian, 159 | .albedo_r = 0.301, 160 | .albedo_g = 0.149, 161 | .albedo_b = 0.0, 162 | .type_data = 0.0, 163 | }, 164 | // Dirt 2 165 | .{ 166 | .type = .lambertian, 167 | .albedo_r = 0.4, 168 | .albedo_g = 0.2, 169 | .albedo_b = 0.0, 170 | .type_data = 0.0, 171 | }, 172 | // Rock 1 173 | .{ 174 | .type = .lambertian, 175 | .albedo_r = 0.275, 176 | .albedo_g = 0.275, 177 | .albedo_b = 0.275, 178 | .type_data = 0.0, 179 | }, 180 | // Rock 2 181 | .{ 182 | .type = .lambertian, 183 | .albedo_r = 0.225, 184 | .albedo_g = 0.225, 185 | .albedo_b = 0.225, 186 | .type_data = 0.0, 187 | }, 188 | // Iron 189 | .{ 190 | .type = .metal, 191 | .albedo_r = 0.6, 192 | .albedo_g = 0.337, 193 | .albedo_b = 0.282, 194 | .type_data = 0.45, 195 | }, 196 | }; 197 | -------------------------------------------------------------------------------- /src/modules/voxel_rt/Benchmark.zig: -------------------------------------------------------------------------------- 1 | /// This file contains logic to perform a simple benchmark report to test the renderer 2 | const std = @import("std"); 3 | const za = @import("zalgebra"); 4 | 5 | const BrickState = @import("brick/State.zig"); 6 | const Camera = @import("Camera.zig"); 7 | const Context = @import("../render.zig").Context; 8 | 9 | const Benchmark = @This(); 10 | 11 | brick_state: BrickState, 12 | sun_enabled: bool, 13 | camera: *Camera, 14 | timer: f32, 15 | 16 | path_point_fraction: f32, 17 | path_orientation_fraction: f32, 18 | 19 | report: Report, 20 | 21 | /// You should probably use VoxelRT.createBenchmark ... 22 | pub fn init(camera: *Camera, brick_state: BrickState, sun_enabled: bool) Benchmark { 23 | const path_point_fraction = Configuration.benchmark_duration / @as(f32, @floatFromInt(Configuration.path_points.len)); 24 | const path_orientation_fraction = Configuration.benchmark_duration / @as(f32, @floatFromInt(Configuration.path_orientations.len)); 25 | 26 | // initialize camera state 27 | camera.disableInput(); 28 | camera.d_camera.origin = Configuration.path_points[0].data; 29 | // HACK: use yaw quat as orientation and ignore pitch 30 | camera.yaw = Configuration.path_orientations[0]; 31 | camera.pitch = za.Quat.identity(); 32 | camera.propogatePitchChange(); 33 | 34 | return Benchmark{ 35 | .brick_state = brick_state, 36 | .sun_enabled = sun_enabled, 37 | .camera = camera, 38 | .timer = 0, 39 | .path_point_fraction = path_point_fraction, 40 | .path_orientation_fraction = path_orientation_fraction, 41 | .report = Report.init(brick_state), 42 | }; 43 | } 44 | 45 | /// Update benchmark and camera state, return true if benchmark has completed 46 | pub fn update(self: *Benchmark, dt: f32) bool { 47 | self.timer += dt; 48 | 49 | const path_point_index: usize = @intFromFloat(@divFloor(self.timer, self.path_point_fraction)); 50 | if (path_point_index < Configuration.path_points.len - 1) { 51 | const path_point_lerp_pos = @rem(self.timer, self.path_point_fraction) / self.path_point_fraction; 52 | const left = Configuration.path_points[path_point_index]; 53 | const right = Configuration.path_points[path_point_index + 1]; 54 | self.camera.d_camera.origin = left.lerp(right, path_point_lerp_pos).data; 55 | } 56 | 57 | const path_orientation_index: usize = @intFromFloat(@divFloor(self.timer, self.path_orientation_fraction)); 58 | if (path_orientation_index < Configuration.path_orientations.len - 1) { 59 | const path_orientation_lerp_pos = @rem(self.timer, self.path_orientation_fraction) / self.path_orientation_fraction; 60 | const left = Configuration.path_orientations[path_orientation_index]; 61 | const right = Configuration.path_orientations[path_orientation_index + 1]; 62 | self.camera.yaw = left.lerp(right, path_orientation_lerp_pos); 63 | self.camera.pitch = za.Quat.identity(); 64 | } 65 | 66 | self.camera.propogatePitchChange(); 67 | 68 | self.report.min_delta_time = @min(self.report.min_delta_time, dt); 69 | self.report.max_delta_time = @max(self.report.max_delta_time, dt); 70 | self.report.delta_time_sum += dt; 71 | self.report.delta_time_sum_samples += 1; 72 | 73 | return self.timer >= Configuration.benchmark_duration; 74 | } 75 | 76 | pub fn printReport(self: Benchmark, device_name: []const u8) void { 77 | self.report.print(device_name, self.camera.d_camera, self.sun_enabled); 78 | } 79 | 80 | pub const Report = struct { 81 | const Vec3U = za.GenericVector(3, u32); 82 | 83 | min_delta_time: f32, 84 | max_delta_time: f32, 85 | 86 | delta_time_sum: f32, 87 | delta_time_sum_samples: u32, 88 | brick_dim: Vec3U, 89 | 90 | pub fn init(brick_state: BrickState) Report { 91 | return Report{ 92 | .min_delta_time = std.math.floatMax(f32), 93 | .max_delta_time = 0, 94 | .delta_time_sum = 0, 95 | .delta_time_sum_samples = 0, 96 | .brick_dim = Vec3U.new( 97 | brick_state.device_state.voxel_dim_x, 98 | brick_state.device_state.voxel_dim_y, 99 | brick_state.device_state.voxel_dim_z, 100 | ), 101 | }; 102 | } 103 | 104 | pub fn average(self: Report) f32 { 105 | const delta_time_sum_samples_f: f32 = @floatFromInt(self.delta_time_sum_samples); 106 | return self.delta_time_sum / delta_time_sum_samples_f; 107 | } 108 | 109 | pub fn print(self: Report, device_name: []const u8, d_camera: Camera.Device, sun_enabled: bool) void { 110 | const report_fmt = "{s: <25}: {d:>8.3}\n{s: <25}: {d:>8.3}\n{s: <25}: {d:>8.3}\n"; 111 | const sun_fmt = "{s: <25}: {any}\n"; 112 | const camera_fmt = "Camera state info:\n{s: <30}: (x = {d}, y = {d})\n{s: <30}: {d}\n{s: <30}: {d}\n"; 113 | std.log.info("\n{s:-^50}\n{s: <25}: {s}\n" ++ report_fmt ++ "{s: <25}: {any}\n" ++ sun_fmt ++ camera_fmt, .{ 114 | "BENCHMARK REPORT", 115 | "GPU", 116 | device_name, 117 | "Min frame time", 118 | self.min_delta_time * std.time.ms_per_s, 119 | "Max frame time", 120 | self.max_delta_time * std.time.ms_per_s, 121 | "Avg frame time", 122 | self.average() * std.time.ms_per_s, 123 | "Brick state info", 124 | self.brick_dim.data, 125 | "Sun enabled", 126 | sun_enabled, 127 | " > image dimensions", 128 | d_camera.image_width, 129 | d_camera.image_height, 130 | " > max bounce", 131 | d_camera.max_bounce, 132 | " > samples per pixel", 133 | d_camera.samples_per_pixel, 134 | }); 135 | } 136 | }; 137 | 138 | // TODO: these are static for now, but should be configured through a file 139 | // TODO: there should also be gui functionality to record paths and orientations 140 | // in such a file ... 141 | pub const Configuration = struct { 142 | 143 | // total duration of benchmarks in seconds 144 | pub const benchmark_duration: f32 = 60; 145 | 146 | pub const path_points = [_]za.Vec3{ 147 | za.Vec3.new(0, 0, 0), 148 | za.Vec3.new(2, 5, 0), 149 | za.Vec3.new(3, 5, 5), 150 | za.Vec3.new(5, 2, 1), 151 | za.Vec3.new(10, 0, 10), 152 | za.Vec3.new(20, -20, 20), 153 | za.Vec3.new(10, -25, 15), 154 | za.Vec3.new(10, -22, 20), 155 | za.Vec3.new(10, -30, 25), 156 | za.Vec3.new(5, -10, 10), 157 | za.Vec3.new(0, 13, 0), 158 | }; 159 | 160 | pub const path_orientations = [_]za.Quat{ 161 | za.Quat.identity(), 162 | za.Quat.fromEulerAngles(za.Vec3.new(0, 45, 0)), 163 | za.Quat.fromEulerAngles(za.Vec3.new(10, -20, 0)), 164 | za.Quat.fromEulerAngles(za.Vec3.new(20, 180, 0)), 165 | za.Quat.fromEulerAngles(za.Vec3.new(50, 90, 0)), 166 | za.Quat.fromEulerAngles(za.Vec3.new(60, 0, 0)), 167 | za.Quat.fromEulerAngles(za.Vec3.new(80, -10, 0)), 168 | za.Quat.fromEulerAngles(za.Vec3.new(75, -40, 0)), 169 | za.Quat.fromEulerAngles(za.Vec3.new(80, -10, 0)), 170 | za.Quat.fromEulerAngles(za.Vec3.new(80, -90, 0)), 171 | za.Quat.fromEulerAngles(za.Vec3.new(0, -145, 0)), 172 | }; 173 | }; 174 | -------------------------------------------------------------------------------- /src/modules/render/GpuBufferMemory.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const vk = @import("vulkan"); 3 | 4 | const vk_utils = @import("vk_utils.zig"); 5 | const Context = @import("Context.zig"); 6 | 7 | const tracy = @import("ztracy"); 8 | 9 | const memory_util = @import("memory.zig"); 10 | 11 | /// Vulkan buffer abstraction 12 | const GpuBufferMemory = @This(); 13 | 14 | // TODO: might not make sense if different type, should be array 15 | /// how many elements does the buffer contain 16 | len: u32, 17 | // TODO: rename capacity, and add an accumulating size variable 18 | /// how many bytes *can* be stored in the buffer 19 | size: vk.DeviceSize, 20 | buffer: vk.Buffer, 21 | memory: vk.DeviceMemory, 22 | mapped: ?*anyopaque, 23 | 24 | /// user has to make sure to call deinit on buffer 25 | pub fn init(ctx: Context, size: vk.DeviceSize, buf_usage_flags: vk.BufferUsageFlags, mem_prop_flags: vk.MemoryPropertyFlags) !GpuBufferMemory { 26 | const buffer = blk: { 27 | const buffer_info = vk.BufferCreateInfo{ 28 | .flags = .{}, 29 | .size = size, 30 | .usage = buf_usage_flags, 31 | .sharing_mode = .exclusive, 32 | .queue_family_index_count = 0, 33 | .p_queue_family_indices = undefined, 34 | }; 35 | break :blk try ctx.vkd.createBuffer(ctx.logical_device, &buffer_info, null); 36 | }; 37 | errdefer ctx.vkd.destroyBuffer(ctx.logical_device, buffer, null); 38 | 39 | const memory = blk: { 40 | const memory_requirements = ctx.vkd.getBufferMemoryRequirements(ctx.logical_device, buffer); 41 | const memory_type_index = try vk_utils.findMemoryTypeIndex(ctx, memory_requirements.memory_type_bits, mem_prop_flags); 42 | const allocate_info = vk.MemoryAllocateInfo{ 43 | .allocation_size = memory_requirements.size, 44 | .memory_type_index = memory_type_index, 45 | }; 46 | break :blk try ctx.vkd.allocateMemory(ctx.logical_device, &allocate_info, null); 47 | }; 48 | errdefer ctx.vkd.freeMemory(ctx.logical_device, memory, null); 49 | 50 | try ctx.vkd.bindBufferMemory(ctx.logical_device, buffer, memory, 0); 51 | 52 | return GpuBufferMemory{ 53 | .len = 0, 54 | .size = size, 55 | .buffer = buffer, 56 | .memory = memory, 57 | .mapped = null, 58 | }; 59 | } 60 | 61 | pub const CopyConfig = struct { 62 | src_offset: vk.DeviceSize = 0, 63 | dst_offset: vk.DeviceSize = 0, 64 | size: vk.DeviceSize = 0, 65 | }; 66 | pub fn copy(self: GpuBufferMemory, ctx: Context, into: *GpuBufferMemory, command_pool: vk.CommandPool, config: CopyConfig) !void { 67 | const copy_zone = tracy.ZoneN(@src(), "copy buffer"); 68 | defer copy_zone.End(); 69 | const command_buffer = try vk_utils.beginOneTimeCommandBuffer(ctx, command_pool); 70 | var copy_region = vk.BufferCopy{ 71 | .src_offset = config.src_offset, 72 | .dst_offset = config.dst_offset, 73 | .size = config.size, 74 | }; 75 | ctx.vkd.cmdCopyBuffer(command_buffer, self.buffer, into.buffer, 1, @ptrCast(©_region)); 76 | try vk_utils.endOneTimeCommandBuffer(ctx, command_pool, command_buffer); 77 | } 78 | 79 | /// Transfer data from host to device 80 | pub fn transferToDevice(self: *GpuBufferMemory, ctx: Context, comptime T: type, offset: usize, data: []const T) !void { 81 | const transfer_zone = tracy.ZoneN(@src(), @src().fn_name); 82 | defer transfer_zone.End(); 83 | 84 | if (self.mapped != null) { 85 | // TODO: implement a transfer for already mapped memory 86 | return error.MemoryAlreadyMapped; // can't used transfer if memory is externally mapped 87 | } 88 | // transfer empty data slice is NOP 89 | if (data.len <= 0) return; 90 | 91 | const size = data.len * @sizeOf(T); 92 | const map_size = memory_util.nonCoherentAtomSize(ctx, size); 93 | if (map_size + offset > self.size) return error.OutOfDeviceMemory; // size greater than buffer 94 | 95 | const gpu_mem = (try ctx.vkd.mapMemory(ctx.logical_device, self.memory, offset, map_size, .{})) orelse return error.FailedToMapGPUMem; 96 | const gpu_mem_align: [*]align(1) T = @ptrCast(gpu_mem); 97 | var typed_gpu_mem: [*]T = @alignCast(gpu_mem_align); 98 | @memcpy(typed_gpu_mem[0..data.len], data); 99 | ctx.vkd.unmapMemory(ctx.logical_device, self.memory); 100 | self.len = @intCast(data.len); 101 | } 102 | 103 | pub fn map(self: *GpuBufferMemory, ctx: Context, offset: vk.DeviceSize, size: vk.DeviceSize) !void { 104 | const map_size = blk: { 105 | if (size == vk.WHOLE_SIZE) { 106 | break :blk vk.WHOLE_SIZE; 107 | } 108 | const atom_size = memory_util.nonCoherentAtomSize(ctx, size); 109 | if (atom_size + offset > self.size) { 110 | return error.InsufficientMemory; 111 | } 112 | break :blk atom_size; 113 | }; 114 | 115 | self.mapped = (try ctx.vkd.mapMemory(ctx.logical_device, self.memory, offset, map_size, .{})) orelse return error.FailedToMapGPUMem; 116 | } 117 | 118 | pub fn unmap(self: *GpuBufferMemory, ctx: Context) void { 119 | if (self.mapped != null) { 120 | ctx.vkd.unmapMemory(ctx.logical_device, self.memory); 121 | self.mapped = null; 122 | } 123 | } 124 | 125 | pub fn flush(self: GpuBufferMemory, ctx: Context, offset: vk.DeviceSize, size: vk.DeviceSize) !void { 126 | const atom_size = memory_util.nonCoherentAtomSize(ctx, size); 127 | if (atom_size + offset > self.size) return error.InsufficientMemory; // size greater than buffer 128 | 129 | const map_range = vk.MappedMemoryRange{ 130 | .memory = self.memory, 131 | .offset = offset, 132 | .size = atom_size, 133 | }; 134 | try ctx.vkd.flushMappedMemoryRanges( 135 | ctx.logical_device, 136 | 1, 137 | @ptrCast(&map_range), 138 | ); 139 | } 140 | 141 | pub fn transferFromDevice(self: *GpuBufferMemory, ctx: Context, comptime T: type, data: []T) !void { 142 | const transfer_zone = tracy.ZoneN(@src(), @src().fn_name); 143 | defer transfer_zone.End(); 144 | 145 | if (self.mapped != null) { 146 | // TODO: implement a transfer for already mapped memory 147 | return error.MemoryAlreadyMapped; // can't used transfer if memory is externally mapped 148 | } 149 | 150 | const gpu_mem = (try ctx.vkd.mapMemory(ctx.logical_device, self.memory, 0, self.size, .{})) orelse return error.FailedToMapGPUMem; 151 | const gpu_mem_start = @intFromPtr(gpu_mem); 152 | 153 | var i: usize = 0; 154 | var offset: usize = 0; 155 | { 156 | @setRuntimeSafety(false); 157 | while (offset + @sizeOf(T) <= self.size and i < data.len) : (i += 1) { 158 | offset = @sizeOf(T) * i; 159 | const address = gpu_mem_start + offset; 160 | std.debug.assert(std.mem.alignForward(usize, address, @alignOf(T)) == address); 161 | 162 | data[i] = @as(*T, @ptrFromInt(address)).*; 163 | } 164 | } 165 | ctx.vkd.unmapMemory(ctx.logical_device, self.memory); 166 | } 167 | 168 | /// Transfer data from host to device, allows you to send multiple chunks of data in the same buffer. 169 | /// offsets are index offsets, not byte offsets 170 | pub fn batchTransfer(self: *GpuBufferMemory, ctx: Context, comptime T: type, offsets: []usize, datas: [][]const T) !void { 171 | const transfer_zone = tracy.ZoneN(@src(), @src().fn_name); 172 | defer transfer_zone.End(); 173 | 174 | if (self.mapped != null) { 175 | // TODO: implement a transfer for already mapped memory 176 | return error.MemoryAlreadyMapped; // can't used transfer if memory is externally mapped 177 | } 178 | 179 | if (offsets.len == 0) return; 180 | if (offsets.len != datas.len) { 181 | return error.OffsetDataMismatch; // inconsistent offset and data size indicate a programatic error 182 | } 183 | 184 | // calculate how far in the memory location we are going 185 | const size = datas[datas.len - 1].len * @sizeOf(T) + offsets[offsets.len - 1] * @sizeOf(T); 186 | if (self.size < size) { 187 | return error.InsufficentBufferSize; // size of buffer is less than data being transfered 188 | } 189 | 190 | const gpu_mem = (try ctx.vkd.mapMemory(ctx.logical_device, self.memory, 0, self.size, .{})) orelse return error.FailedToMapGPUMem; 191 | const gpu_mem_start = @intFromPtr(gpu_mem); 192 | { 193 | @setRuntimeSafety(false); 194 | for (offsets, 0..) |offset, i| { 195 | const byte_offset = offset * @sizeOf(T); 196 | for (datas[i], 0..) |element, j| { 197 | const mem_location = gpu_mem_start + byte_offset + j * @sizeOf(T); 198 | const ptr: *T = @ptrFromInt(mem_location); 199 | ptr.* = element; 200 | } 201 | } 202 | } 203 | ctx.vkd.unmapMemory(ctx.logical_device, self.memory); 204 | 205 | self.len = std.math.max(self.len, @intCast(datas[datas.len - 1].len + offsets[offsets.len - 1])); 206 | } 207 | 208 | /// destroy buffer and free memory 209 | pub fn deinit(self: GpuBufferMemory, ctx: Context) void { 210 | if (self.mapped != null) { 211 | ctx.vkd.unmapMemory(ctx.logical_device, self.memory); 212 | } 213 | if (self.buffer != .null_handle) { 214 | ctx.vkd.destroyBuffer(ctx.logical_device, self.buffer, null); 215 | } 216 | if (self.memory != .null_handle) { 217 | ctx.vkd.freeMemory(ctx.logical_device, self.memory, null); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/modules/voxel_rt/brick/Grid.zig: -------------------------------------------------------------------------------- 1 | // This file contains an implementaion of "Real-time Ray tracing and Editing of Large Voxel Scenes" 2 | // source: https://dspace.library.uu.nl/handle/1874/315917 3 | 4 | const std = @import("std"); 5 | const math = std.math; 6 | const Allocator = std.mem.Allocator; 7 | const ArrayList = std.ArrayList; 8 | 9 | const State = @import("State.zig"); 10 | const AtomicCount = State.AtomicCount; 11 | const MaterialAllocator = @import("MaterialAllocator.zig"); 12 | 13 | pub const Config = struct { 14 | // Default value is all bricks 15 | brick_alloc: ?usize = null, 16 | base_t: f32 = 0.01, 17 | min_point: [3]f32 = [_]f32{ 0.0, 0.0, 0.0 }, 18 | scale: f32 = 1.0, 19 | workers_count: usize = 4, 20 | }; 21 | 22 | const BrickGrid = @This(); 23 | 24 | allocator: Allocator, 25 | // grid state that is shared with the workers 26 | state: *State, 27 | material_allocator: *MaterialAllocator, 28 | 29 | /// Initialize a BrickGrid that can be raytraced 30 | /// @param: 31 | /// - allocator: used to allocate bricks and the grid, also to clean up these in deinit 32 | /// - dim_x: how many bricks *maps* (or chunks) in x dimension 33 | /// - dim_y: how many bricks *maps* (or chunks) in y dimension 34 | /// - dim_z: how many bricks *maps* (or chunks) in z dimension 35 | /// - config: config options for the brickmap 36 | pub fn init(allocator: Allocator, dim_x: u32, dim_y: u32, dim_z: u32, config: Config) !BrickGrid { 37 | std.debug.assert(config.workers_count > 0); 38 | std.debug.assert(dim_x * dim_y * dim_z > 0); 39 | 40 | const brick_count = dim_x * dim_y * dim_z; 41 | 42 | // each mask has 32 entries 43 | const brick_statuses = try allocator.alloc(State.BrickStatusMask, (std.math.divCeil(u32, brick_count, 32) catch unreachable)); 44 | errdefer allocator.free(brick_statuses); 45 | @memset(brick_statuses, .{ .bits = 0 }); 46 | 47 | const brick_indices = try allocator.alloc(State.IndexToBrick, brick_count); 48 | errdefer allocator.free(brick_indices); 49 | @memset(brick_indices, 0); 50 | 51 | const brick_alloc = config.brick_alloc orelse brick_count; 52 | 53 | const brick_occupancy = try allocator.alloc(u8, brick_alloc * State.brick_bytes); 54 | errdefer allocator.free(brick_occupancy); 55 | @memset(brick_occupancy, 0); 56 | 57 | const brick_start_indices = try allocator.alloc(State.Brick.StartIndex, brick_alloc); 58 | errdefer allocator.free(brick_start_indices); 59 | @memset(brick_start_indices, State.Brick.unset_index); 60 | 61 | const material_index_count = brick_alloc * State.brick_bits; 62 | const material_indices = try allocator.alloc(State.MaterialIndices, material_index_count); 63 | errdefer allocator.free(material_indices); 64 | @memset(material_indices, 0); 65 | 66 | const min_point_base_t = blk: { 67 | const min_point = config.min_point; 68 | const base_t = config.base_t; 69 | var result: [4]f32 = undefined; 70 | @memcpy(result[0..3], &min_point); 71 | result[3] = base_t; 72 | break :blk result; 73 | }; 74 | const max_point_scale = [4]f32{ 75 | min_point_base_t[0] + @as(f32, @floatFromInt(dim_x)) * config.scale, 76 | min_point_base_t[1] + @as(f32, @floatFromInt(dim_y)) * config.scale, 77 | min_point_base_t[2] + @as(f32, @floatFromInt(dim_z)) * config.scale, 78 | config.scale, 79 | }; 80 | 81 | const work_segment_size = try std.math.divCeil(u32, dim_x, @intCast(config.workers_count)); 82 | 83 | const state = try allocator.create(State); 84 | errdefer allocator.destroy(state); 85 | state.* = .{ 86 | .brick_statuses = brick_statuses, 87 | .brick_indices = brick_indices, 88 | .brick_occupancy = brick_occupancy, 89 | .brick_start_indices = brick_start_indices, 90 | .material_indices = material_indices, 91 | .active_bricks = AtomicCount.init(0), 92 | .work_segment_size = work_segment_size, 93 | .device_state = State.Device{ 94 | .voxel_dim_x = dim_x * State.brick_dimension, 95 | .voxel_dim_y = dim_y * State.brick_dimension, 96 | .voxel_dim_z = dim_z * State.brick_dimension, 97 | .dim_x = dim_x, 98 | .dim_y = dim_y, 99 | .dim_z = dim_z, 100 | .min_point_base_t = min_point_base_t, 101 | .max_point_scale = max_point_scale, 102 | }, 103 | }; 104 | 105 | const material_allocator = try allocator.create(MaterialAllocator); 106 | errdefer allocator.destroy(material_allocator); 107 | material_allocator.* = .init(material_indices.len); 108 | 109 | return BrickGrid{ 110 | .allocator = allocator, 111 | .state = state, 112 | .material_allocator = material_allocator, 113 | }; 114 | } 115 | 116 | /// Clean up host memory, does not account for device 117 | pub fn deinit(self: BrickGrid) void { 118 | self.allocator.free(self.state.brick_statuses); 119 | self.allocator.free(self.state.brick_indices); 120 | self.allocator.free(self.state.brick_occupancy); 121 | self.allocator.free(self.state.brick_start_indices); 122 | self.allocator.free(self.state.material_indices); 123 | 124 | self.allocator.destroy(self.material_allocator); 125 | self.allocator.destroy(self.state); 126 | } 127 | 128 | // perform a insert in the grid 129 | pub fn insert(self: *BrickGrid, x: usize, y: usize, z: usize, material_index: u8) void { 130 | std.debug.assert(x < self.state.device_state.voxel_dim_x); 131 | std.debug.assert(y < self.state.device_state.voxel_dim_y); 132 | std.debug.assert(z < self.state.device_state.voxel_dim_z); 133 | 134 | // Flip Y for more intutive coordinates 135 | const flipped_y = self.state.device_state.voxel_dim_y - 1 - y; 136 | 137 | const grid_index = gridAt(self.state.*.device_state, x, flipped_y, z); 138 | const brick_status_index = grid_index / 32; 139 | const brick_status_offset: u5 = @intCast(grid_index % 32); 140 | const brick_status = self.state.brick_statuses[brick_status_index].read(brick_status_offset); 141 | const brick_index = blk: { 142 | if (brick_status == .loaded) { 143 | break :blk self.state.brick_indices[grid_index]; 144 | } 145 | 146 | // atomically fetch previous brick count and then add 1 to count 147 | break :blk self.state.*.active_bricks.fetchAdd(1, .monotonic); 148 | }; 149 | 150 | const occupancy_from = brick_index * State.brick_bytes; 151 | const occupancy_to = brick_index * State.brick_bytes + State.brick_bytes; 152 | const brick_occupancy = self.state.brick_occupancy[occupancy_from..occupancy_to]; 153 | var brick_material_index = &self.state.brick_start_indices[brick_index]; 154 | 155 | // set the voxel to exist 156 | const nth_bit = voxelAt(x, flipped_y, z); 157 | 158 | // set the color information for the given voxel 159 | { 160 | // set the brick's material index if unset 161 | if (brick_material_index.* == State.Brick.unset_index) { 162 | const material_entry = self.material_allocator.nextEntry(); 163 | brick_material_index.value = @intCast(material_entry); 164 | brick_material_index.type = .voxel_start_index; 165 | 166 | // store brick material start index 167 | self.state.bricks_start_indices_delta.registerDelta(brick_index); 168 | } 169 | 170 | std.debug.assert(brick_material_index.type == .voxel_start_index); 171 | std.debug.assert(brick_material_index.value == std.mem.alignForward(u31, brick_material_index.value, 16)); 172 | 173 | const new_voxel_material_index = brick_material_index.value + nth_bit; 174 | self.state.*.material_indices[new_voxel_material_index] = material_index; 175 | 176 | self.state.material_indices_delta.registerDelta(new_voxel_material_index); 177 | } 178 | 179 | // set voxel 180 | const mask_index = nth_bit / @bitSizeOf(u8); 181 | const mask_bit: u3 = @intCast(@rem(nth_bit, @bitSizeOf(u8))); 182 | brick_occupancy[mask_index] |= @as(u8, 1) << mask_bit; 183 | 184 | // store brick changes 185 | self.state.bricks_occupancy_delta.registerDelta(occupancy_from + mask_index); 186 | 187 | // set the brick as loaded 188 | self.state.brick_statuses[brick_status_index].write(.loaded, brick_status_offset); 189 | self.state.brick_statuses_delta.registerDelta(brick_status_index); 190 | 191 | // register brick index 192 | self.state.brick_indices[grid_index] = brick_index; 193 | self.state.brick_indices_delta.registerDelta(grid_index); 194 | } 195 | 196 | // TODO: test 197 | /// get brick index from global index coordinates 198 | fn voxelAt(x: usize, y: usize, z: usize) State.BrickMapLog2 { 199 | const brick_x: usize = @rem(x, State.brick_dimension); 200 | const brick_y: usize = @rem(y, State.brick_dimension); 201 | const brick_z: usize = @rem(z, State.brick_dimension); 202 | return @intCast(brick_x + State.brick_dimension * (brick_z + State.brick_dimension * brick_y)); 203 | } 204 | 205 | /// get grid index from global index coordinates 206 | fn gridAt(device_state: State.Device, x: usize, y: usize, z: usize) usize { 207 | const grid_x: u32 = @intCast(x / State.brick_dimension); 208 | const grid_y: u32 = @intCast(y / State.brick_dimension); 209 | const grid_z: u32 = @intCast(z / State.brick_dimension); 210 | return @intCast(grid_x + device_state.dim_x * (grid_z + device_state.dim_z * grid_y)); 211 | } 212 | 213 | /// count the set bits up to range_to (exclusive) 214 | fn countBits(bits: [State.brick_bytes]u8, range_to: u32) u32 { 215 | var bit: State.BrickMap = @bitCast(bits); 216 | var count: State.BrickMap = 0; 217 | var i: u32 = 0; 218 | while (i < range_to and bit != 0) : (i += 1) { 219 | count += bit & 1; 220 | bit = bit >> 1; 221 | } 222 | return @intCast(count); 223 | } 224 | -------------------------------------------------------------------------------- /src/modules/render/physical_device.zig: -------------------------------------------------------------------------------- 1 | /// Abstractions around vulkan physical device 2 | const std = @import("std"); 3 | const Allocator = std.mem.Allocator; 4 | const ArrayList = std.ArrayList; 5 | 6 | const vk = @import("vulkan"); 7 | const dispatch = @import("dispatch.zig"); 8 | const constants = @import("consts.zig"); 9 | const swapchain = @import("swapchain.zig"); 10 | const vk_utils = @import("vk_utils.zig"); 11 | const validation_layer = @import("validation_layer.zig"); 12 | const Context = @import("Context.zig"); 13 | 14 | pub const QueueFamilyIndices = struct { 15 | compute: u32, 16 | compute_queue_count: u32, 17 | graphics: u32, 18 | 19 | // TODO: use internal allocator that is suitable 20 | /// Initialize a QueueFamilyIndices instance, internal allocation is handled by QueueFamilyIndices (no manuall cleanup) 21 | pub fn init(allocator: Allocator, vki: dispatch.Instance, physical_device: vk.PhysicalDevice, surface: vk.SurfaceKHR) !QueueFamilyIndices { 22 | var queue_family_count: u32 = 0; 23 | vki.getPhysicalDeviceQueueFamilyProperties(physical_device, &queue_family_count, null); 24 | 25 | var queue_families = try allocator.alloc(vk.QueueFamilyProperties, queue_family_count); 26 | defer allocator.free(queue_families); 27 | 28 | vki.getPhysicalDeviceQueueFamilyProperties(physical_device, &queue_family_count, queue_families.ptr); 29 | queue_families.len = queue_family_count; 30 | 31 | const compute_bit = vk.QueueFlags{ 32 | .compute_bit = true, 33 | }; 34 | const graphics_bit = vk.QueueFlags{ 35 | .graphics_bit = true, 36 | }; 37 | 38 | var compute_index: ?u32 = null; 39 | var compute_queue_count: u32 = 0; 40 | var graphics_index: ?u32 = null; 41 | var present_index: ?u32 = null; 42 | for (queue_families, 0..) |queue_family, i| { 43 | const index: u32 = @intCast(i); 44 | 45 | const is_graphics = graphics_index == null and queue_family.queue_flags.contains(graphics_bit); 46 | const is_present = present_index == null and (try vki.getPhysicalDeviceSurfaceSupportKHR(physical_device, index, surface)) == vk.TRUE; 47 | if (is_graphics and is_present) { 48 | graphics_index = index; 49 | present_index = index; 50 | } 51 | 52 | const is_compute = queue_family.queue_flags.contains(compute_bit); 53 | const id_first_compute = is_compute and compute_index == null; 54 | const is_discrete_compute = is_compute and !is_graphics and !is_present; 55 | if (id_first_compute or is_discrete_compute) { 56 | compute_index = index; 57 | compute_queue_count = queue_family.queue_count; 58 | } 59 | } 60 | 61 | if (compute_index == null) { 62 | return error.ComputeIndexMissing; 63 | } 64 | if (graphics_index == null) { 65 | return error.GraphicsIndexMissing; 66 | } 67 | if (present_index == null) { 68 | return error.PresentIndexMissing; 69 | } 70 | 71 | return QueueFamilyIndices{ 72 | .compute = compute_index.?, 73 | .compute_queue_count = compute_queue_count, 74 | .graphics = graphics_index.?, 75 | }; 76 | } 77 | }; 78 | 79 | /// check if physical device supports given target extensions 80 | // TODO: unify with getRequiredInstanceExtensions? 81 | pub fn isDeviceExtensionsPresent(allocator: Allocator, vki: dispatch.Instance, device: vk.PhysicalDevice, target_extensions: []const [*:0]const u8) !bool { 82 | // query extensions available 83 | var supported_extensions_count: u32 = 0; 84 | // TODO: handle "VkResult.incomplete" 85 | _ = try vki.enumerateDeviceExtensionProperties(device, null, &supported_extensions_count, null); 86 | 87 | var extensions = try ArrayList(vk.ExtensionProperties).initCapacity(allocator, supported_extensions_count); 88 | defer extensions.deinit(); 89 | 90 | _ = try vki.enumerateDeviceExtensionProperties(device, null, &supported_extensions_count, extensions.items.ptr); 91 | extensions.items.len = supported_extensions_count; 92 | 93 | var matches: u32 = 0; 94 | for (target_extensions) |target_extension| { 95 | const t_str_len = std.mem.indexOfScalar(u8, target_extension[0..vk.MAX_EXTENSION_NAME_SIZE], 0) orelse continue; 96 | 97 | cmp: for (extensions.items) |existing| { 98 | const existing_name: [*:0]const u8 = @ptrCast(&existing.extension_name); 99 | const e_str_len = std.mem.indexOfScalar(u8, existing_name[0..vk.MAX_EXTENSION_NAME_SIZE], 0) orelse continue; 100 | if (std.mem.eql(u8, target_extension[0..t_str_len], existing_name[0..e_str_len])) { 101 | matches += 1; 102 | break :cmp; 103 | } 104 | } 105 | } 106 | 107 | return matches == target_extensions.len; 108 | } 109 | 110 | // TODO: use internal allocator that is suitable 111 | /// select primary physical device in init 112 | pub fn selectPrimary(allocator: Allocator, vki: dispatch.Instance, instance: vk.Instance, surface: vk.SurfaceKHR) !vk.PhysicalDevice { 113 | var device_count: u32 = 0; 114 | _ = try vki.enumeratePhysicalDevices(instance, &device_count, null); // TODO: handle incomplete 115 | if (device_count < 0) { 116 | std.debug.panic("no GPU suitable for vulkan identified"); 117 | } 118 | 119 | var devices = try allocator.alloc(vk.PhysicalDevice, device_count); 120 | defer allocator.free(devices); 121 | 122 | _ = try vki.enumeratePhysicalDevices(instance, &device_count, devices.ptr); // TODO: handle incomplete 123 | devices.len = device_count; 124 | 125 | var device_score: i32 = -1; 126 | var device_index: ?usize = null; 127 | for (devices, 0..) |device, i| { 128 | const new_score = try deviceHeuristic(allocator, vki, device, surface); 129 | if (device_score < new_score) { 130 | device_score = new_score; 131 | device_index = i; 132 | } 133 | } 134 | 135 | if (device_index == null) { 136 | return error.NoSuitablePhysicalDevice; 137 | } 138 | 139 | const val = devices[device_index.?]; 140 | return val; 141 | } 142 | 143 | /// Any suiteable GPU should result in a positive value, an unsuitable GPU might return a negative value 144 | fn deviceHeuristic(allocator: Allocator, vki: dispatch.Instance, device: vk.PhysicalDevice, surface: vk.SurfaceKHR) !i32 { 145 | // TODO: rewrite function to have clearer distinction between required and bonus features 146 | // possible solutions: 147 | // - return error if missing feature and discard negative return value (use u32 instead) 148 | // - 2 bitmaps 149 | const property_score = blk: { 150 | const device_properties = vki.getPhysicalDeviceProperties(device); 151 | const discrete = @as(i32, @intFromBool(device_properties.device_type == vk.PhysicalDeviceType.discrete_gpu)) + 5; 152 | break :blk discrete; 153 | }; 154 | 155 | const feature_score: i32 = blk: { 156 | // required by vulkan 1.4 157 | // var eigth_bit_storage_feature = vk.PhysicalDevice8BitStorageFeatures{}; 158 | 159 | var maintenance4_feature = vk.PhysicalDeviceMaintenance4Features{ 160 | .p_next = null, // @ptrCast(&eigth_bit_storage_feature), 161 | }; 162 | 163 | var p_features: vk.PhysicalDeviceFeatures2 = .{ 164 | .p_next = @ptrCast(&maintenance4_feature), 165 | .features = .{}, 166 | }; 167 | 168 | // Silence validation by calling vkGetPhysicalDeviceFeatures 169 | _ = vki.getPhysicalDeviceFeatures(device); 170 | 171 | vki.getPhysicalDeviceFeatures2(device, &p_features); 172 | if (maintenance4_feature.maintenance_4 == vk.FALSE) { 173 | break :blk -1000; 174 | } 175 | break :blk 10; 176 | }; 177 | 178 | const queue_fam_score: i32 = blk: { 179 | _ = QueueFamilyIndices.init(allocator, vki, device, surface) catch break :blk -1000; 180 | break :blk 10; 181 | }; 182 | 183 | const extensions_score: i32 = blk: { 184 | const extension_slice = constants.logical_device_extensions[0..]; 185 | const extensions_available = try isDeviceExtensionsPresent(allocator, vki, device, extension_slice); 186 | if (!extensions_available) { 187 | break :blk -1000; 188 | } 189 | break :blk 10; 190 | }; 191 | 192 | const swapchain_score: i32 = blk: { 193 | if (swapchain.SupportDetails.init(allocator, vki, device, surface)) |ok| { 194 | defer ok.deinit(allocator); 195 | break :blk 10; 196 | } else |_| { 197 | break :blk -1000; 198 | } 199 | }; 200 | 201 | return -30 + property_score + feature_score + queue_fam_score + extensions_score + swapchain_score; 202 | } 203 | 204 | pub fn createLogicalDevice(allocator: Allocator, ctx: Context) !vk.Device { 205 | 206 | // merge indices if they are identical according to vulkan spec 207 | var family_indices = [_]u32{ ctx.queue_indices.graphics, undefined }; 208 | var indices: usize = 1; 209 | if (ctx.queue_indices.compute != ctx.queue_indices.graphics) { 210 | family_indices[indices] = ctx.queue_indices.compute; 211 | indices += 1; 212 | } 213 | 214 | var queue_create_infos = try allocator.alloc(vk.DeviceQueueCreateInfo, indices); 215 | defer allocator.free(queue_create_infos); 216 | 217 | const queue_priority = [_]f32{1.0}; 218 | for (family_indices[0..indices], 0..) |family_index, i| { 219 | queue_create_infos[i] = .{ 220 | .flags = .{}, 221 | .queue_family_index = family_index, 222 | .queue_count = if (family_index == ctx.queue_indices.compute) ctx.queue_indices.compute_queue_count else 1, 223 | .p_queue_priorities = &queue_priority, 224 | }; 225 | } 226 | 227 | var synchronization2 = vk.PhysicalDeviceSynchronization2FeaturesKHR{ 228 | .synchronization_2 = vk.TRUE, 229 | }; 230 | var maintenance_4_features = vk.PhysicalDeviceMaintenance4Features{ 231 | .p_next = @ptrCast(&synchronization2), 232 | .maintenance_4 = vk.TRUE, 233 | }; 234 | var device_feature_1_2 = vk.PhysicalDeviceVulkan12Features{ 235 | .p_next = @ptrCast(&maintenance_4_features), 236 | .shader_int_8 = vk.TRUE, 237 | .storage_buffer_8_bit_access = vk.TRUE, 238 | }; 239 | const device_features = vk.PhysicalDeviceFeatures2{ 240 | .p_next = @ptrCast(&device_feature_1_2), 241 | .features = .{}, 242 | }; 243 | const validation_layer_info = try validation_layer.Info.init(allocator, ctx.vkb); 244 | 245 | const create_info = vk.DeviceCreateInfo{ 246 | .p_next = @ptrCast(&device_features), 247 | .flags = .{}, 248 | .queue_create_info_count = @intCast(queue_create_infos.len), 249 | .p_queue_create_infos = queue_create_infos.ptr, 250 | .enabled_layer_count = validation_layer_info.enabled_layer_count, 251 | .pp_enabled_layer_names = validation_layer_info.enabled_layer_names, 252 | .enabled_extension_count = constants.logical_device_extensions.len, 253 | .pp_enabled_extension_names = &constants.logical_device_extensions, 254 | .p_enabled_features = null, 255 | }; 256 | return ctx.vki.createDevice(ctx.physical_device, &create_info, null); 257 | } 258 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const assert = std.debug.assert; 3 | const Allocator = std.mem.Allocator; 4 | const ArrayList = std.ArrayList; 5 | 6 | const zglfw = @import("zglfw"); 7 | const za = @import("zalgebra"); 8 | const ztracy = @import("ztracy"); 9 | 10 | const render = @import("modules/render.zig"); 11 | const consts = render.consts; 12 | 13 | const Input = @import("modules/Input.zig"); 14 | 15 | // TODO: API topology 16 | const VoxelRT = @import("modules/VoxelRT.zig"); 17 | const BrickGrid = VoxelRT.BrickGrid; 18 | const gpu_types = VoxelRT.gpu_types; 19 | const vox = VoxelRT.vox; 20 | const terrain = VoxelRT.terrain; 21 | 22 | pub const application_name = "zig vulkan"; 23 | pub const internal_render_resolution = za.GenericVector(2, u32).new(1024, 576); 24 | 25 | // TODO: wrap this in render to make main seem simpler :^) 26 | var delta_time: f64 = 0; 27 | 28 | var activate_sprint: bool = false; 29 | var call_translate: u8 = 0; 30 | var camera_translate = za.Vec3.zero(); 31 | 32 | var input: Input = undefined; 33 | var call_yaw = false; 34 | var call_pitch = false; 35 | var mouse_delta = za.Vec2.zero(); 36 | var mouse_ignore_frames: u32 = 5; 37 | 38 | pub fn main() anyerror!void { 39 | ztracy.SetThreadName("main thread"); 40 | const main_zone = ztracy.ZoneN(@src(), "main"); 41 | defer main_zone.End(); 42 | 43 | const stderr = std.io.getStdErr().writer(); 44 | 45 | // create a gpa with default configuration 46 | var alloc = if (consts.enable_validation_layers) std.heap.GeneralPurposeAllocator(.{}){} else std.heap.c_allocator; 47 | defer { 48 | if (consts.enable_validation_layers) { 49 | const leak = alloc.deinit(); 50 | if (leak == .leak) { 51 | stderr.print("leak detected in gpa!", .{}) catch unreachable; 52 | } 53 | } 54 | } 55 | const allocator = if (consts.enable_validation_layers) alloc.allocator() else alloc; 56 | 57 | // Initialize the library * 58 | try zglfw.init(); 59 | defer zglfw.terminate(); 60 | 61 | if (!zglfw.isVulkanSupported()) { 62 | std.debug.panic("vulkan not supported on device (glfw)", .{}); 63 | } 64 | 65 | // Create a windowed mode window 66 | zglfw.windowHint(.client_api, .no_api); 67 | zglfw.windowHint(.center_cursor, true); 68 | zglfw.windowHint(.maximized, true); 69 | zglfw.windowHint(.scale_to_monitor, true); 70 | zglfw.windowHint(.focused, true); 71 | var window = try zglfw.Window.create(3840, 2160, application_name, null); 72 | defer window.destroy(); 73 | 74 | const ctx = try render.Context.init(allocator, application_name, window); 75 | defer ctx.deinit(); 76 | 77 | var grid = try BrickGrid.init(allocator, 128, 64, 128, .{ 78 | .min_point = [3]f32{ -32, -16, -32 }, 79 | .scale = 0.5, 80 | .workers_count = 4, 81 | }); 82 | defer grid.deinit(); 83 | 84 | const model = try vox.load(false, allocator, "../assets/models/doom.vox"); 85 | defer model.deinit(); 86 | 87 | var materials: [256]gpu_types.Material = undefined; 88 | // insert terrain materials 89 | for (terrain.materials, 0..) |material, i| { 90 | materials[i] = material; 91 | } 92 | 93 | for ( 94 | model.rgba_chunk[0 .. model.rgba_chunk.len - terrain.materials.len], 95 | materials[terrain.materials.len..], 96 | ) |rgba, *material| { 97 | const material_type: gpu_types.Material.Type = if (@as(f32, @floatFromInt(rgba.a)) / 255.0 < 0.8) .dielectric else .lambertian; 98 | const material_data: f32 = if (material_type == .dielectric) 1.52 else 0.0; 99 | material.* = .{ 100 | .type = material_type, 101 | .albedo_r = @as(f32, @floatFromInt(rgba.r)) / 255.0, 102 | .albedo_g = @as(f32, @floatFromInt(rgba.g)) / 255.0, 103 | .albedo_b = @as(f32, @floatFromInt(rgba.b)) / 255.0, 104 | .type_data = material_data, 105 | }; 106 | } 107 | 108 | // Test what we are loading 109 | for (model.xyzi_chunks[0]) |xyzi| { 110 | const material_index: u8 = xyzi.color_index + @as(u8, @intCast(terrain.materials.len)); 111 | grid.insert( 112 | @as(usize, @intCast(xyzi.x)) + 200, 113 | @as(usize, @intCast(xyzi.z)) + 50, 114 | @as(usize, @intCast(xyzi.y)) + 150, 115 | material_index, 116 | ); 117 | } 118 | 119 | // generate terrain on CPU 120 | try terrain.generateCpu(2, allocator, 420, 4, 20, &grid); 121 | 122 | var voxel_rt = try VoxelRT.init(allocator, ctx, &grid, .{ 123 | .internal_resolution_width = internal_render_resolution.x(), 124 | .internal_resolution_height = internal_render_resolution.y(), 125 | .camera = .{ 126 | .samples_per_pixel = 2, 127 | .max_bounce = 2, 128 | }, 129 | .sun = .{ 130 | .enabled = true, 131 | }, 132 | .pipeline = .{ 133 | .staging_buffers = 3, 134 | }, 135 | }); 136 | defer voxel_rt.deinit(allocator, ctx); 137 | 138 | try voxel_rt.pushMaterials(ctx, materials[0..]); 139 | 140 | try window.setInputMode(zglfw.InputMode.cursor, zglfw.Cursor.Mode.disabled); 141 | 142 | // init input module with default input handler functions 143 | input = try Input.init( 144 | allocator, 145 | window, 146 | gameKeyInputFn, 147 | mouseBtnInputFn, 148 | gameCursorPosInputFn, 149 | ); 150 | defer input.deinit(allocator); 151 | try input.setInputModeCursor(.disabled); 152 | input.setImguiWantInput(false); 153 | 154 | var prev_frame = std.time.milliTimestamp(); 155 | // Loop until the user closes the window 156 | while (!window.shouldClose()) { 157 | const current_frame = std.time.milliTimestamp(); 158 | delta_time = @as(f64, @floatFromInt(current_frame - prev_frame)) / @as(f64, std.time.ms_per_s); 159 | // f32 variant of delta_time 160 | const dt: f32 = @floatCast(delta_time); 161 | 162 | if (call_translate > 0) { 163 | if (activate_sprint) { 164 | voxel_rt.camera.activateSprint(); 165 | } else { 166 | voxel_rt.camera.disableSprint(); 167 | } 168 | voxel_rt.camera.translate(dt, camera_translate); 169 | } 170 | if (call_yaw) { 171 | voxel_rt.camera.turnYaw(-mouse_delta.x() * dt); 172 | } 173 | if (call_pitch) { 174 | voxel_rt.camera.turnPitch(mouse_delta.y() * dt); 175 | } 176 | if (call_translate > 0 or call_yaw or call_pitch) { 177 | call_yaw = false; 178 | call_pitch = false; 179 | mouse_delta.data[0] = 0; 180 | mouse_delta.data[1] = 0; 181 | // try voxel_rt.debugUpdateTerrain(ctx); 182 | } 183 | voxel_rt.updateSun(dt); 184 | 185 | try voxel_rt.updateGridDelta(ctx); 186 | try voxel_rt.draw(ctx, dt); 187 | 188 | // Poll for and process events 189 | zglfw.pollEvents(); 190 | prev_frame = current_frame; 191 | 192 | input.updateCursor() catch {}; 193 | 194 | ztracy.FrameMark(); 195 | } 196 | } 197 | 198 | fn gameKeyInputFn(event: Input.KeyEvent) void { 199 | if (event.action == .press) { 200 | switch (event.key) { 201 | Input.Key.w => { 202 | call_translate += 1; 203 | camera_translate.data[2] -= 1; 204 | }, 205 | Input.Key.s => { 206 | call_translate += 1; 207 | camera_translate.data[2] += 1; 208 | }, 209 | Input.Key.d => { 210 | call_translate += 1; 211 | camera_translate.data[0] += 1; 212 | }, 213 | Input.Key.a => { 214 | call_translate += 1; 215 | camera_translate.data[0] -= 1; 216 | }, 217 | Input.Key.left_control => { 218 | call_translate += 1; 219 | camera_translate.data[1] += 1; 220 | }, 221 | Input.Key.left_shift => activate_sprint = true, 222 | Input.Key.space => { 223 | call_translate += 1; 224 | camera_translate.data[1] -= 1; 225 | }, 226 | Input.Key.escape => { 227 | input.setCursorPosCallback(menuCursorPosInputFn); 228 | input.setKeyCallback(menuKeyInputFn); 229 | input.setInputModeCursor(.normal) catch std.debug.panic("failed to set input mode cursor", .{}); 230 | input.setImguiWantInput(true); 231 | }, 232 | else => {}, 233 | } 234 | } else if (event.action == .release) { 235 | switch (event.key) { 236 | Input.Key.w => { 237 | call_translate -= 1; 238 | camera_translate.data[2] += 1; 239 | }, 240 | Input.Key.s => { 241 | call_translate -= 1; 242 | camera_translate.data[2] -= 1; 243 | }, 244 | Input.Key.d => { 245 | call_translate -= 1; 246 | camera_translate.data[0] -= 1; 247 | }, 248 | Input.Key.a => { 249 | call_translate -= 1; 250 | camera_translate.data[0] += 1; 251 | }, 252 | Input.Key.left_control => { 253 | call_translate -= 1; 254 | camera_translate.data[1] -= 1; 255 | }, 256 | Input.Key.left_shift => { 257 | activate_sprint = false; 258 | }, 259 | Input.Key.space => { 260 | call_translate -= 1; 261 | camera_translate.data[1] += 1; 262 | }, 263 | else => {}, 264 | } 265 | } 266 | } 267 | 268 | fn menuKeyInputFn(event: Input.KeyEvent) void { 269 | if (event.action == .press) { 270 | switch (event.key) { 271 | Input.Key.escape => { 272 | input.setCursorPosCallback(gameCursorPosInputFn); 273 | input.setKeyCallback(gameKeyInputFn); 274 | input.setImguiWantInput(false); 275 | input.setInputModeCursor(.disabled) catch std.debug.panic("failed to set input mode cursor", .{}); 276 | 277 | // ignore first 5 frames of input after 278 | mouse_ignore_frames = 5; 279 | }, 280 | else => {}, 281 | } 282 | } 283 | } 284 | 285 | fn mouseBtnInputFn(event: Input.MouseButtonEvent) void { 286 | if (event.action == Input.Action.press) { 287 | if (event.button == Input.MouseButton.left) {} else if (event.button == Input.MouseButton.right) {} 288 | } 289 | if (event.action == Input.Action.release) { 290 | if (event.button == Input.MouseButton.left) {} else if (event.button == Input.MouseButton.right) {} 291 | } 292 | } 293 | 294 | fn gameCursorPosInputFn(event: Input.CursorPosEvent) void { 295 | const State = struct { 296 | var prev_event: ?Input.CursorPosEvent = null; 297 | }; 298 | defer State.prev_event = event; 299 | 300 | if (mouse_ignore_frames == 0) { 301 | // let prev_event be defined before processing Input 302 | if (State.prev_event) |p_event| { 303 | mouse_delta.data[0] += @floatCast(event.x - p_event.x); 304 | mouse_delta.data[1] += @floatCast(event.y - p_event.y); 305 | } 306 | call_yaw = call_yaw or mouse_delta.x() < -0.00001 or mouse_delta.x() > 0.00001; 307 | call_pitch = call_pitch or mouse_delta.y() < -0.00001 or mouse_delta.y() > 0.00001; 308 | } 309 | mouse_ignore_frames = if (mouse_ignore_frames > 0) mouse_ignore_frames - 1 else 0; 310 | } 311 | 312 | fn menuCursorPosInputFn(event: Input.CursorPosEvent) void { 313 | _ = event; 314 | } 315 | -------------------------------------------------------------------------------- /src/modules/render/swapchain.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | const ArrayList = std.ArrayList; 4 | 5 | const vk = @import("vulkan"); 6 | const zglfw = @import("zglfw"); 7 | 8 | const dispatch = @import("dispatch.zig"); 9 | const physical_device = @import("physical_device.zig"); 10 | const QueueFamilyIndices = physical_device.QueueFamilyIndices; 11 | const Context = @import("Context.zig"); 12 | const Texture = @import("Texture.zig"); 13 | 14 | pub const ViewportScissor = struct { 15 | viewport: [1]vk.Viewport, 16 | scissor: [1]vk.Rect2D, 17 | 18 | /// utility to create simple view state info 19 | pub fn init(extent: vk.Extent2D) ViewportScissor { 20 | // TODO: this can be broken down a bit since the code is pretty cluster fck 21 | const width = extent.width; 22 | const height = extent.height; 23 | return .{ 24 | .viewport = [1]vk.Viewport{ 25 | .{ 26 | .x = 0, 27 | .y = 0, 28 | .width = @floatFromInt(width), 29 | .height = @floatFromInt(height), 30 | .min_depth = 0.0, 31 | .max_depth = 1.0, 32 | }, 33 | }, 34 | .scissor = [1]vk.Rect2D{ 35 | .{ .offset = .{ 36 | .x = 0, 37 | .y = 0, 38 | }, .extent = extent }, 39 | }, 40 | }; 41 | } 42 | }; 43 | 44 | // TODO: rename 45 | // TODO: mutex! : the data is shared between rendering implementation and pipeline 46 | // pipeline will attempt to update the data in the event of rescale which might lead to RC 47 | pub const Data = struct { 48 | allocator: Allocator, 49 | swapchain: vk.SwapchainKHR, 50 | images: []vk.Image, 51 | image_views: []vk.ImageView, 52 | format: vk.Format, 53 | extent: vk.Extent2D, 54 | support_details: SupportDetails, 55 | 56 | // create a swapchain data struct, caller must make sure to call deinit 57 | pub fn init(allocator: Allocator, ctx: Context, command_pool: vk.CommandPool, old_swapchain: ?vk.SwapchainKHR) !Data { 58 | const support_details = try SupportDetails.init(allocator, ctx.vki, ctx.physical_device, ctx.surface); 59 | errdefer support_details.deinit(allocator); 60 | 61 | const sc_create_info = blk1: { 62 | const format = support_details.selectSwapChainFormat(); 63 | const present_mode = support_details.selectSwapchainPresentMode(); 64 | const extent = try support_details.constructSwapChainExtent(ctx.window_ptr); 65 | 66 | const max_images = if (support_details.capabilities.max_image_count == 0) std.math.maxInt(u32) else support_details.capabilities.max_image_count; 67 | const image_count = @min(support_details.capabilities.min_image_count + 1, max_images); 68 | 69 | const Config = struct { 70 | sharing_mode: vk.SharingMode, 71 | index_count: u32, 72 | p_indices: [*]const u32, 73 | }; 74 | const sharing_config = Config{ 75 | .sharing_mode = .exclusive, 76 | .index_count = 1, 77 | .p_indices = @ptrCast(&ctx.queue_indices.graphics), 78 | }; 79 | 80 | break :blk1 vk.SwapchainCreateInfoKHR{ 81 | .flags = .{}, 82 | .surface = ctx.surface, 83 | .min_image_count = image_count, 84 | .image_format = format.format, 85 | .image_color_space = format.color_space, 86 | .image_extent = extent, 87 | .image_array_layers = 1, 88 | .image_usage = vk.ImageUsageFlags{ .color_attachment_bit = true }, 89 | .image_sharing_mode = sharing_config.sharing_mode, 90 | .queue_family_index_count = sharing_config.index_count, 91 | .p_queue_family_indices = sharing_config.p_indices, 92 | .pre_transform = support_details.capabilities.current_transform, 93 | .composite_alpha = vk.CompositeAlphaFlagsKHR{ .opaque_bit_khr = true }, 94 | .present_mode = present_mode, 95 | .clipped = vk.TRUE, 96 | .old_swapchain = old_swapchain orelse .null_handle, 97 | }; 98 | }; 99 | const swapchain_khr = try ctx.vkd.createSwapchainKHR(ctx.logical_device, &sc_create_info, null); 100 | const swapchain_images = blk: { 101 | // TODO: handle incomplete 102 | var image_count: u32 = 0; 103 | _ = try ctx.vkd.getSwapchainImagesKHR(ctx.logical_device, swapchain_khr, &image_count, null); 104 | 105 | const images = try allocator.alloc(vk.Image, image_count); 106 | errdefer allocator.free(images); 107 | 108 | // TODO: handle incomplete 109 | _ = try ctx.vkd.getSwapchainImagesKHR(ctx.logical_device, swapchain_khr, &image_count, images.ptr); 110 | break :blk images; 111 | }; 112 | errdefer allocator.free(swapchain_images); 113 | 114 | // Assumption: you will never have more than 16 swapchain images.. 115 | const max_swapchain_size = 16; 116 | std.debug.assert(swapchain_images.len <= max_swapchain_size); 117 | 118 | var transition_configs: [max_swapchain_size]Texture.TransitionConfig = undefined; 119 | for (transition_configs[0..swapchain_images.len], swapchain_images) |*transition_config, image| { 120 | transition_config.* = .{ 121 | .image = image, 122 | .old_layout = .undefined, 123 | .new_layout = .present_src_khr, 124 | }; 125 | } 126 | try Texture.transitionImageLayouts(ctx, command_pool, transition_configs[0..swapchain_images.len]); 127 | 128 | const image_views = blk: { 129 | var views = try allocator.alloc(vk.ImageView, swapchain_images.len); 130 | errdefer allocator.free(views); 131 | 132 | const components = vk.ComponentMapping{ 133 | .r = .identity, 134 | .g = .identity, 135 | .b = .identity, 136 | .a = .identity, 137 | }; 138 | const subresource_range = vk.ImageSubresourceRange{ 139 | .aspect_mask = .{ .color_bit = true }, 140 | .base_mip_level = 0, 141 | .level_count = 1, 142 | .base_array_layer = 0, 143 | .layer_count = 1, 144 | }; 145 | for (swapchain_images, 0..) |image, i| { 146 | const create_info = vk.ImageViewCreateInfo{ 147 | .flags = .{}, 148 | .image = image, 149 | .view_type = .@"2d", 150 | .format = sc_create_info.image_format, 151 | .components = components, 152 | .subresource_range = subresource_range, 153 | }; 154 | views[i] = try ctx.vkd.createImageView(ctx.logical_device, &create_info, null); 155 | } 156 | 157 | break :blk views; 158 | }; 159 | errdefer allocator.free(image_views); 160 | 161 | return Data{ 162 | .allocator = allocator, 163 | .swapchain = swapchain_khr, 164 | .images = swapchain_images, 165 | .image_views = image_views, 166 | .format = sc_create_info.image_format, 167 | .extent = sc_create_info.image_extent, 168 | .support_details = support_details, 169 | }; 170 | } 171 | 172 | pub fn deinit(self: Data, ctx: Context) void { 173 | for (self.image_views) |view| { 174 | ctx.vkd.destroyImageView(ctx.logical_device, view, null); 175 | } 176 | self.allocator.free(self.image_views); 177 | self.allocator.free(self.images); 178 | self.support_details.deinit(self.allocator); 179 | 180 | ctx.vkd.destroySwapchainKHR(ctx.logical_device, self.swapchain, null); 181 | } 182 | }; 183 | 184 | pub const SupportDetails = struct { 185 | const Self = @This(); 186 | 187 | capabilities: vk.SurfaceCapabilitiesKHR, 188 | formats: []vk.SurfaceFormatKHR, 189 | present_modes: []vk.PresentModeKHR, 190 | 191 | /// caller has to make sure to also call deinit 192 | pub fn init(allocator: Allocator, vki: dispatch.Instance, device: vk.PhysicalDevice, surface: vk.SurfaceKHR) !Self { 193 | const capabilities = try vki.getPhysicalDeviceSurfaceCapabilitiesKHR(device, surface); 194 | 195 | var format_count: u32 = 0; 196 | // TODO: handle incomplete 197 | _ = try vki.getPhysicalDeviceSurfaceFormatsKHR(device, surface, &format_count, null); 198 | if (format_count <= 0) { 199 | return error.NoSurfaceFormatsSupported; 200 | } 201 | const formats = blk: { 202 | var formats = try allocator.alloc(vk.SurfaceFormatKHR, format_count); 203 | _ = try vki.getPhysicalDeviceSurfaceFormatsKHR(device, surface, &format_count, formats.ptr); 204 | formats.len = format_count; 205 | break :blk formats; 206 | }; 207 | errdefer allocator.free(formats); 208 | 209 | var present_modes_count: u32 = 0; 210 | _ = try vki.getPhysicalDeviceSurfacePresentModesKHR(device, surface, &present_modes_count, null); 211 | if (present_modes_count <= 0) { 212 | return error.NoPresentModesSupported; 213 | } 214 | const present_modes = blk: { 215 | var present_modes = try allocator.alloc(vk.PresentModeKHR, present_modes_count); 216 | _ = try vki.getPhysicalDeviceSurfacePresentModesKHR(device, surface, &present_modes_count, present_modes.ptr); 217 | present_modes.len = present_modes_count; 218 | break :blk present_modes; 219 | }; 220 | errdefer allocator.free(present_modes); 221 | 222 | return Self{ 223 | .capabilities = capabilities, 224 | .formats = formats, 225 | .present_modes = present_modes, 226 | }; 227 | } 228 | 229 | pub fn selectSwapChainFormat(self: Self) vk.SurfaceFormatKHR { 230 | // TODO: in some cases this is a valid state? 231 | // if so return error here instead ... 232 | std.debug.assert(self.formats.len > 0); 233 | 234 | for (self.formats) |format| { 235 | if (format.format == .b8g8r8a8_unorm and format.color_space == .srgb_nonlinear_khr) { 236 | return format; 237 | } 238 | } 239 | 240 | return self.formats[0]; 241 | } 242 | 243 | pub fn selectSwapchainPresentMode(self: Self) vk.PresentModeKHR { 244 | for (self.present_modes) |present_mode| { 245 | if (present_mode == .mailbox_khr) { 246 | return present_mode; 247 | } 248 | } 249 | 250 | return .fifo_khr; 251 | } 252 | 253 | pub fn constructSwapChainExtent(self: Self, window: *zglfw.Window) !vk.Extent2D { 254 | if (self.capabilities.current_extent.width != std.math.maxInt(u32)) { 255 | return self.capabilities.current_extent; 256 | } else { 257 | const window_size = blk: { 258 | const size = window.getFramebufferSize(); 259 | break :blk vk.Extent2D{ 260 | .width = @intCast(size[0]), 261 | .height = @intCast(size[1]), 262 | }; 263 | }; 264 | 265 | const clamp = std.math.clamp; 266 | const min = self.capabilities.min_image_extent; 267 | const max = self.capabilities.max_image_extent; 268 | return vk.Extent2D{ 269 | .width = clamp(window_size.width, min.width, max.width), 270 | .height = clamp(window_size.height, min.height, max.height), 271 | }; 272 | } 273 | } 274 | 275 | pub fn deinit(self: Self, allocator: Allocator) void { 276 | allocator.free(self.formats); 277 | allocator.free(self.present_modes); 278 | } 279 | }; 280 | -------------------------------------------------------------------------------- /src/modules/voxel_rt/vox/loader.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | 4 | const types = @import("types.zig"); 5 | const Vox = types.Vox; 6 | const Chunk = types.Chunk; 7 | 8 | /// VOX loader implemented according to VOX specification: https://github.com/ephtracy/voxel-model/blob/master/MagicaVoxel-file-format-vox.txt 9 | pub fn load(comptime strict: bool, allocator: Allocator, path: []const u8) !Vox { 10 | const file = blk: { 11 | const use_path = try buildPath(allocator, path); 12 | defer allocator.free(use_path); 13 | break :blk try std.fs.openFileAbsolute(use_path, .{}); 14 | }; 15 | defer file.close(); 16 | 17 | const file_buffer = blk: { 18 | const size = (try file.stat()).size; 19 | // + 1 to check if read is successfull 20 | break :blk try allocator.alloc(u8, size + 1); 21 | }; 22 | defer allocator.free(file_buffer); 23 | 24 | const bytes_read = try file.readAll(file_buffer); 25 | if (file_buffer.len <= bytes_read) { 26 | return error.InsufficientBuffer; 27 | } 28 | 29 | return parseBuffer(strict, allocator, file_buffer); 30 | } 31 | 32 | pub const ParseError = error{ 33 | InvalidId, 34 | ExpectedSizeHeader, 35 | ExpectedXyziHeader, 36 | ExpectedRgbaHeader, 37 | UnexpectedVersion, 38 | InvalidFileContent, 39 | MultiplePackChunks, 40 | }; 41 | pub fn parseBuffer(comptime strict: bool, allocator: Allocator, buffer: []const u8) !Vox { 42 | if (strict == true) { 43 | try validateHeader(buffer); 44 | } 45 | 46 | var vox = Vox.init(allocator); 47 | errdefer vox.deinit(); 48 | 49 | // insert main node 50 | try vox.generic_chunks.append(try chunkFrom(buffer[8..])); 51 | try vox.nodes.append(.{ 52 | .type_id = .main, 53 | .generic_index = 0, 54 | .index = 0, 55 | }); 56 | 57 | // id (4) + chunk size (4) + child size (4) 58 | const chunk_stride = 4 * 3; 59 | // skip main chunk 60 | var pos: usize = 8 + chunk_stride; 61 | 62 | // Parse pack_chunk if any 63 | if (buffer[pos] == 'P') { 64 | // parse generic chunk 65 | try vox.generic_chunks.append(try chunkFrom(buffer[pos..])); 66 | pos += chunk_stride; 67 | 68 | // Parse pack 69 | vox.pack_chunk = Chunk.Pack{ 70 | .num_models = parseI32(buffer[pos..]), 71 | }; 72 | pos += 4; 73 | } else { 74 | vox.pack_chunk = Chunk.Pack{ 75 | .num_models = 1, 76 | }; 77 | } 78 | try vox.nodes.append(.{ 79 | .type_id = .pack, 80 | .generic_index = 0, 81 | .index = 0, 82 | }); 83 | 84 | const num_models: usize = @intCast(vox.pack_chunk.num_models); 85 | // allocate voxel data according to pack information 86 | vox.size_chunks = try allocator.alloc(Chunk.Size, num_models); 87 | vox.xyzi_chunks = try allocator.alloc([]Chunk.XyziElement, num_models); 88 | 89 | // TODO: pos will cause out of bounds easily, make code more robust! 90 | var model: usize = 0; 91 | while (model < num_models) : (model += 1) { 92 | // parse SIZE chunk 93 | { 94 | if (strict) { 95 | if (!std.mem.eql(u8, buffer[pos .. pos + 4], "SIZE")) { 96 | return ParseError.ExpectedSizeHeader; 97 | } 98 | } 99 | 100 | // parse generic chunk 101 | try vox.generic_chunks.append(try chunkFrom(buffer[pos..])); 102 | pos += chunk_stride; 103 | 104 | const size = Chunk.Size{ 105 | .size_x = parseI32(buffer[pos..]), 106 | .size_y = parseI32(buffer[pos + 4 ..]), 107 | .size_z = parseI32(buffer[pos + 8 ..]), 108 | }; 109 | pos += 12; 110 | vox.size_chunks[model] = size; 111 | try vox.nodes.append(.{ 112 | .type_id = .size, 113 | .generic_index = vox.generic_chunks.items.len, 114 | .index = model, 115 | }); 116 | } 117 | 118 | // parse XYZI chunk 119 | { 120 | if (strict) { 121 | if (!std.mem.eql(u8, buffer[pos .. pos + 4], "XYZI")) { 122 | return ParseError.ExpectedXyziHeader; 123 | } 124 | } 125 | 126 | // parse generic chunk 127 | try vox.generic_chunks.append(try chunkFrom(buffer[pos..])); 128 | pos += chunk_stride; 129 | 130 | const voxel_count = parseI32(buffer[pos..]); 131 | pos += 4; 132 | const xyzis = try allocator.alloc(Chunk.XyziElement, @intCast(voxel_count)); 133 | { 134 | var i: usize = 0; 135 | while (i < voxel_count) : (i += 1) { 136 | xyzis[i].x = buffer[pos]; 137 | xyzis[i].y = buffer[pos + 1]; 138 | xyzis[i].z = buffer[pos + 2]; 139 | xyzis[i].color_index = buffer[pos + 3]; 140 | pos += 4; 141 | } 142 | } 143 | vox.xyzi_chunks[model] = xyzis; 144 | try vox.nodes.append(.{ 145 | .type_id = .xyzi, 146 | .generic_index = vox.generic_chunks.items.len, 147 | .index = model, 148 | }); 149 | } 150 | } 151 | 152 | var rgba_set: bool = false; 153 | while (pos < buffer.len) { 154 | // Parse potential extensions and RGBA 155 | switch (buffer[pos]) { 156 | 'R' => { 157 | // check if it is probable that there is a RGBA chunk remaining 158 | if (strict) { 159 | if (!std.mem.eql(u8, buffer[pos .. pos + 4], "RGBA")) { 160 | return ParseError.ExpectedRgbaHeader; 161 | } 162 | } 163 | // parse generic chunk 164 | try vox.generic_chunks.append(try chunkFrom(buffer[pos..])); 165 | pos += chunk_stride; 166 | 167 | vox.rgba_chunk[0] = Chunk.RgbaElement{ 168 | .r = 0, 169 | .g = 0, 170 | .b = 0, 171 | .a = 1, 172 | }; 173 | var i: usize = 1; 174 | while (i < 255) : (i += 1) { 175 | vox.rgba_chunk[i] = Chunk.RgbaElement{ 176 | .r = buffer[pos], 177 | .g = buffer[pos + 1], 178 | .b = buffer[pos + 2], 179 | .a = buffer[pos + 3], 180 | }; 181 | pos += 4; 182 | } 183 | rgba_set = true; 184 | }, 185 | else => { 186 | // skip bytes 187 | pos += 4; 188 | }, 189 | } 190 | } 191 | 192 | if (rgba_set == false) { 193 | const default: *const [256]Chunk.RgbaElement = @ptrCast(&default_rgba); 194 | @memcpy(vox.rgba_chunk[0..], default[0..]); 195 | } 196 | 197 | return vox; 198 | } 199 | 200 | inline fn parseI32(buffer: []const u8) i32 { 201 | const i32_ptr: *const i32 = @ptrCast(@alignCast(&buffer[0])); 202 | return i32_ptr.*; 203 | } 204 | 205 | /// Parse a buffer into a chunk, buffer *has* to start with the first character in the id 206 | inline fn chunkFrom(buffer: []const u8) std.fmt.ParseIntError!Chunk { 207 | const size = parseI32(buffer[4..]); 208 | const child_size = parseI32(buffer[8..]); 209 | 210 | return Chunk{ 211 | .size = size, 212 | .child_size = child_size, 213 | }; 214 | } 215 | 216 | inline fn validateHeader(buffer: []const u8) ParseError!void { 217 | if (std.mem.eql(u8, buffer[0..4], "VOX ") == false) { 218 | return error.InvalidId; // Vox format should start with "VOX " 219 | } 220 | 221 | const version = buffer[4]; 222 | if (version != 150) { 223 | return error.UnexpectedVersion; // Expect version 150 224 | } 225 | 226 | if (std.mem.eql(u8, buffer[8..12], "MAIN") == false) { 227 | return error.InvalidFileContent; // Missing main chunk in file 228 | } 229 | } 230 | 231 | inline fn buildPath(allocator: Allocator, path: []const u8) ![]const u8 { 232 | var buf: [std.fs.max_path_bytes]u8 = undefined; 233 | const exe_path = try std.fs.selfExeDirPath(buf[0..]); 234 | const path_segments = [_][]const u8{ exe_path, path }; 235 | 236 | const zig_use_path = try std.fs.path.join(allocator, path_segments[0..]); 237 | errdefer allocator.destroy(zig_use_path.ptr); 238 | 239 | const sep = [_]u8{std.fs.path.sep}; 240 | _ = std.mem.replace(u8, zig_use_path, "\\", sep[0..], zig_use_path); 241 | _ = std.mem.replace(u8, zig_use_path, "/", sep[0..], zig_use_path); 242 | 243 | return zig_use_path; 244 | } 245 | 246 | const default_rgba = [256]u32{ 247 | 0x00000000, 0xffffffff, 0xffccffff, 0xff99ffff, 0xff66ffff, 0xff33ffff, 0xff00ffff, 0xffffccff, 0xffccccff, 0xff99ccff, 0xff66ccff, 0xff33ccff, 0xff00ccff, 0xffff99ff, 0xffcc99ff, 0xff9999ff, 248 | 0xff6699ff, 0xff3399ff, 0xff0099ff, 0xffff66ff, 0xffcc66ff, 0xff9966ff, 0xff6666ff, 0xff3366ff, 0xff0066ff, 0xffff33ff, 0xffcc33ff, 0xff9933ff, 0xff6633ff, 0xff3333ff, 0xff0033ff, 0xffff00ff, 249 | 0xffcc00ff, 0xff9900ff, 0xff6600ff, 0xff3300ff, 0xff0000ff, 0xffffffcc, 0xffccffcc, 0xff99ffcc, 0xff66ffcc, 0xff33ffcc, 0xff00ffcc, 0xffffcccc, 0xffcccccc, 0xff99cccc, 0xff66cccc, 0xff33cccc, 250 | 0xff00cccc, 0xffff99cc, 0xffcc99cc, 0xff9999cc, 0xff6699cc, 0xff3399cc, 0xff0099cc, 0xffff66cc, 0xffcc66cc, 0xff9966cc, 0xff6666cc, 0xff3366cc, 0xff0066cc, 0xffff33cc, 0xffcc33cc, 0xff9933cc, 251 | 0xff6633cc, 0xff3333cc, 0xff0033cc, 0xffff00cc, 0xffcc00cc, 0xff9900cc, 0xff6600cc, 0xff3300cc, 0xff0000cc, 0xffffff99, 0xffccff99, 0xff99ff99, 0xff66ff99, 0xff33ff99, 0xff00ff99, 0xffffcc99, 252 | 0xffcccc99, 0xff99cc99, 0xff66cc99, 0xff33cc99, 0xff00cc99, 0xffff9999, 0xffcc9999, 0xff999999, 0xff669999, 0xff339999, 0xff009999, 0xffff6699, 0xffcc6699, 0xff996699, 0xff666699, 0xff336699, 253 | 0xff006699, 0xffff3399, 0xffcc3399, 0xff993399, 0xff663399, 0xff333399, 0xff003399, 0xffff0099, 0xffcc0099, 0xff990099, 0xff660099, 0xff330099, 0xff000099, 0xffffff66, 0xffccff66, 0xff99ff66, 254 | 0xff66ff66, 0xff33ff66, 0xff00ff66, 0xffffcc66, 0xffcccc66, 0xff99cc66, 0xff66cc66, 0xff33cc66, 0xff00cc66, 0xffff9966, 0xffcc9966, 0xff999966, 0xff669966, 0xff339966, 0xff009966, 0xffff6666, 255 | 0xffcc6666, 0xff996666, 0xff666666, 0xff336666, 0xff006666, 0xffff3366, 0xffcc3366, 0xff993366, 0xff663366, 0xff333366, 0xff003366, 0xffff0066, 0xffcc0066, 0xff990066, 0xff660066, 0xff330066, 256 | 0xff000066, 0xffffff33, 0xffccff33, 0xff99ff33, 0xff66ff33, 0xff33ff33, 0xff00ff33, 0xffffcc33, 0xffcccc33, 0xff99cc33, 0xff66cc33, 0xff33cc33, 0xff00cc33, 0xffff9933, 0xffcc9933, 0xff999933, 257 | 0xff669933, 0xff339933, 0xff009933, 0xffff6633, 0xffcc6633, 0xff996633, 0xff666633, 0xff336633, 0xff006633, 0xffff3333, 0xffcc3333, 0xff993333, 0xff663333, 0xff333333, 0xff003333, 0xffff0033, 258 | 0xffcc0033, 0xff990033, 0xff660033, 0xff330033, 0xff000033, 0xffffff00, 0xffccff00, 0xff99ff00, 0xff66ff00, 0xff33ff00, 0xff00ff00, 0xffffcc00, 0xffcccc00, 0xff99cc00, 0xff66cc00, 0xff33cc00, 259 | 0xff00cc00, 0xffff9900, 0xffcc9900, 0xff999900, 0xff669900, 0xff339900, 0xff009900, 0xffff6600, 0xffcc6600, 0xff996600, 0xff666600, 0xff336600, 0xff006600, 0xffff3300, 0xffcc3300, 0xff993300, 260 | 0xff663300, 0xff333300, 0xff003300, 0xffff0000, 0xffcc0000, 0xff990000, 0xff660000, 0xff330000, 0xff0000ee, 0xff0000dd, 0xff0000bb, 0xff0000aa, 0xff000088, 0xff000077, 0xff000055, 0xff000044, 261 | 0xff000022, 0xff000011, 0xff00ee00, 0xff00dd00, 0xff00bb00, 0xff00aa00, 0xff008800, 0xff007700, 0xff005500, 0xff004400, 0xff002200, 0xff001100, 0xffee0000, 0xffdd0000, 0xffbb0000, 0xffaa0000, 262 | 0xff880000, 0xff770000, 0xff550000, 0xff440000, 0xff220000, 0xff110000, 0xffeeeeee, 0xffdddddd, 0xffbbbbbb, 0xffaaaaaa, 0xff888888, 0xff777777, 0xff555555, 0xff444444, 0xff222222, 0xff111111, 263 | }; 264 | 265 | test "validateHeader: valid header accepted" { 266 | const valid_test_buffer: []const u8 = "VOX " ++ [_]u8{ 150, 0, 0, 0 } ++ "MAIN"; 267 | 268 | try validateHeader(valid_test_buffer); 269 | } 270 | 271 | test "validateHeader: invalid id detected" { 272 | const invalid_test_buffer: []const u8 = "!VOX" ++ [_]u8{ 150, 0, 0, 0 } ++ "MAIN"; 273 | 274 | try std.testing.expectError(ParseError.InvalidId, validateHeader(invalid_test_buffer)); 275 | } 276 | 277 | test "validateHeader: invalid version detected" { 278 | const invalid_test_buffer: []const u8 = "VOX " ++ [_]u8{ 169, 0, 0, 0 } ++ "MAIN"; 279 | 280 | try std.testing.expectError(ParseError.UnexpectedVersion, validateHeader(invalid_test_buffer)); 281 | } 282 | -------------------------------------------------------------------------------- /src/modules/render/Context.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | const ArrayList = std.ArrayList; 4 | 5 | const vk = @import("vulkan"); 6 | const zglfw = @import("zglfw"); 7 | const c = @import("c.zig"); 8 | 9 | const consts = @import("consts.zig"); 10 | const dispatch = @import("dispatch.zig"); 11 | const QueueFamilyIndices = @import("physical_device.zig").QueueFamilyIndices; 12 | const validation_layer = @import("validation_layer.zig"); 13 | const vk_utils = @import("vk_utils.zig"); 14 | 15 | // TODO: move command pool to context? 16 | 17 | /// Utilized to supply vulkan methods and common vulkan state to other 18 | /// renderer functions and structs 19 | const Context = @This(); 20 | 21 | allocator: Allocator, 22 | 23 | vkb: dispatch.Base, 24 | vki: dispatch.Instance, 25 | vkd: dispatch.Device, 26 | 27 | instance: vk.Instance, 28 | physical_device_limits: vk.PhysicalDeviceLimits, 29 | physical_device: vk.PhysicalDevice, 30 | logical_device: vk.Device, 31 | 32 | compute_queue: vk.Queue, 33 | graphics_queue: vk.Queue, 34 | 35 | surface: vk.SurfaceKHR, 36 | queue_indices: QueueFamilyIndices, 37 | 38 | // TODO: utilize comptime for this (emit from struct if we are in release mode) 39 | messenger: ?vk.DebugUtilsMessengerEXT, 40 | 41 | /// pointer to the window handle. Caution is adviced when using this pointer ... 42 | window_ptr: *zglfw.Window, 43 | 44 | // Caller should make sure to call deinit 45 | pub fn init(allocator: Allocator, application_name: []const u8, window: *zglfw.Window) !Context { 46 | const app_name: [:0]const u8 = app_name_blk: { 47 | var c_str = try allocator.allocSentinel(u8, application_name.len, 0); 48 | @memcpy(c_str[0..application_name.len], application_name); 49 | c_str[c_str.len - 1] = 0; 50 | break :app_name_blk @ptrCast(c_str); 51 | }; 52 | defer allocator.free(app_name); 53 | 54 | const app_info = vk.ApplicationInfo{ 55 | .p_next = null, 56 | .p_application_name = app_name, 57 | .application_version = @bitCast(consts.application_version), 58 | .p_engine_name = consts.engine_name, 59 | .engine_version = @bitCast(consts.engine_version), 60 | .api_version = @bitCast(consts.vulkan_version), 61 | }; 62 | 63 | // TODO: move to global scope (currently crashes the zig compiler :') ) 64 | const common_extensions = [_][*:0]const u8{vk.extensions.khr_surface.name}; 65 | const application_extensions = blk: { 66 | if (consts.enable_validation_layers) { 67 | const debug_extensions = [_][*:0]const u8{ 68 | vk.extensions.ext_debug_utils.name, 69 | } ++ common_extensions; 70 | break :blk debug_extensions[0..]; 71 | } 72 | break :blk common_extensions[0..]; 73 | }; 74 | 75 | const glfw_extensions_slice = try zglfw.getRequiredInstanceExtensions(); 76 | // Due to a zig bug we need arraylist to append instead of preallocate slice 77 | // in release it fail and length turns out to be 1 78 | var extensions = try ArrayList([*:0]const u8).initCapacity(allocator, glfw_extensions_slice.len + application_extensions.len); 79 | defer extensions.deinit(); 80 | 81 | for (glfw_extensions_slice) |extension| { 82 | try extensions.append(extension); 83 | } 84 | for (application_extensions) |extension| { 85 | try extensions.append(extension); 86 | } 87 | 88 | // Partially init a context so that we can use "self" even in init 89 | var self: Context = undefined; 90 | self.allocator = allocator; 91 | 92 | // load base dispatch wrapper 93 | self.vkb = dispatch.Base.load(c.glfwGetInstanceProcAddress); 94 | if (!(try vk_utils.isInstanceExtensionsPresent(allocator, self.vkb, extensions.items))) { 95 | return error.InstanceExtensionNotPresent; 96 | } 97 | 98 | const validation_layer_info = try validation_layer.Info.init(allocator, self.vkb); 99 | 100 | const debug_create_info: ?*const vk.DebugUtilsMessengerCreateInfoEXT = blk: { 101 | if (consts.enable_validation_layers) { 102 | break :blk &createDefaultDebugCreateInfo(); 103 | } else { 104 | break :blk null; 105 | } 106 | }; 107 | 108 | const debug_features = [_]vk.ValidationFeatureEnableEXT{ 109 | .best_practices_ext, // .synchronization_validation_ext, 110 | }; 111 | const features: ?*const vk.ValidationFeaturesEXT = blk: { 112 | if (consts.enable_validation_layers) { 113 | break :blk &vk.ValidationFeaturesEXT{ 114 | .p_next = @ptrCast(debug_create_info), 115 | .enabled_validation_feature_count = debug_features.len, 116 | .p_enabled_validation_features = &debug_features, 117 | .disabled_validation_feature_count = 0, 118 | .p_disabled_validation_features = undefined, 119 | }; 120 | } 121 | break :blk null; 122 | }; 123 | 124 | self.instance = blk: { 125 | const instance_info = vk.InstanceCreateInfo{ 126 | .p_next = @ptrCast(features), 127 | .flags = .{}, 128 | .p_application_info = &app_info, 129 | .enabled_layer_count = validation_layer_info.enabled_layer_count, 130 | .pp_enabled_layer_names = validation_layer_info.enabled_layer_names, 131 | .enabled_extension_count = @intCast(extensions.items.len), 132 | .pp_enabled_extension_names = @ptrCast(extensions.items.ptr), 133 | }; 134 | break :blk try self.vkb.createInstance(&instance_info, null); 135 | }; 136 | 137 | self.vki = dispatch.Instance.load(self.instance, self.vkb.dispatch.vkGetInstanceProcAddr.?); 138 | errdefer self.vki.destroyInstance(self.instance, null); 139 | 140 | const result: vk.Result = c.glfwCreateWindowSurface(self.instance, window, null, &self.surface); 141 | if (result != .success) { 142 | return error.FailedToCreateSurface; 143 | } 144 | errdefer self.vki.destroySurfaceKHR(self.instance, self.surface, null); 145 | 146 | self.physical_device = try @import("physical_device.zig").selectPrimary(allocator, self.vki, self.instance, self.surface); 147 | self.queue_indices = try QueueFamilyIndices.init(allocator, self.vki, self.physical_device, self.surface); 148 | 149 | self.messenger = blk: { 150 | if (!consts.enable_validation_layers) break :blk null; 151 | break :blk self.vki.createDebugUtilsMessengerEXT(self.instance, debug_create_info.?, null) catch { 152 | std.debug.panic("failed to create debug messenger", .{}); 153 | }; 154 | }; 155 | self.logical_device = try @import("physical_device.zig").createLogicalDevice(allocator, self); 156 | 157 | self.vkd = dispatch.Device.load(self.logical_device, self.vki.dispatch.vkGetDeviceProcAddr.?); 158 | self.compute_queue = self.vkd.getDeviceQueue(self.logical_device, self.queue_indices.compute, 0); 159 | self.graphics_queue = self.vkd.getDeviceQueue(self.logical_device, self.queue_indices.graphics, 0); 160 | 161 | self.physical_device_limits = self.getPhysicalDeviceProperties().limits; 162 | 163 | // possibly a bit wasteful, but to get compile errors when forgetting to 164 | // init a variable the partial context variables are moved to a new context which we return 165 | return Context{ 166 | .allocator = self.allocator, 167 | .vkb = self.vkb, 168 | .vki = self.vki, 169 | .vkd = self.vkd, 170 | .instance = self.instance, 171 | .physical_device = self.physical_device, 172 | .logical_device = self.logical_device, 173 | .compute_queue = self.compute_queue, 174 | .graphics_queue = self.graphics_queue, 175 | .physical_device_limits = self.physical_device_limits, 176 | .surface = self.surface, 177 | .queue_indices = self.queue_indices, 178 | .messenger = self.messenger, 179 | .window_ptr = window, 180 | }; 181 | } 182 | 183 | pub fn destroyShaderModule(self: Context, module: vk.ShaderModule) void { 184 | self.vkd.destroyShaderModule(self.logical_device, module, null); 185 | } 186 | 187 | /// caller must destroy returned module 188 | pub fn createPipelineLayout(self: Context, create_info: vk.PipelineLayoutCreateInfo) !vk.PipelineLayout { 189 | return self.vkd.createPipelineLayout(self.logical_device, &create_info, null); 190 | } 191 | 192 | pub fn destroyPipelineLayout(self: Context, pipeline_layout: vk.PipelineLayout) void { 193 | self.vkd.destroyPipelineLayout(self.logical_device, pipeline_layout, null); 194 | } 195 | 196 | /// caller must destroy pipeline from vulkan 197 | pub inline fn createGraphicsPipeline(self: Context, create_info: vk.GraphicsPipelineCreateInfo) !vk.Pipeline { 198 | var pipeline: vk.Pipeline = undefined; 199 | const result = try self.vkd.createGraphicsPipelines( 200 | self.logical_device, 201 | .null_handle, 202 | 1, 203 | @ptrCast(&create_info), 204 | null, 205 | @ptrCast(&pipeline), 206 | ); 207 | if (result != vk.Result.success) { 208 | // TODO: not panic? 209 | std.debug.panic("failed to initialize pipeline!", .{}); 210 | } 211 | return pipeline; 212 | } 213 | 214 | /// caller must both destroy pipeline from the heap and in vulkan 215 | pub fn createComputePipeline(self: Context, create_info: vk.ComputePipelineCreateInfo) !vk.Pipeline { 216 | var pipeline: vk.Pipeline = undefined; 217 | const result = try self.vkd.createComputePipelines( 218 | self.logical_device, 219 | .null_handle, 220 | 1, 221 | @ptrCast(&create_info), 222 | null, 223 | @ptrCast(&pipeline), 224 | ); 225 | if (result != vk.Result.success) { 226 | // TODO: not panic? 227 | std.debug.panic("failed to initialize pipeline!", .{}); 228 | } 229 | 230 | return pipeline; 231 | } 232 | 233 | /// destroy pipeline from vulkan *not* from the application memory 234 | pub fn destroyPipeline(self: Context, pipeline: *vk.Pipeline) void { 235 | self.vkd.destroyPipeline(self.logical_device, pipeline.*, null); 236 | } 237 | 238 | pub fn getPhysicalDeviceProperties(self: Context) vk.PhysicalDeviceProperties { 239 | return self.vki.getPhysicalDeviceProperties(self.physical_device); 240 | } 241 | 242 | /// caller must destroy returned render pass 243 | pub fn createRenderPass(self: Context, format: vk.Format) !vk.RenderPass { 244 | const color_attachment = [_]vk.AttachmentDescription{ 245 | .{ 246 | .flags = .{}, 247 | .format = format, 248 | .samples = .{ 249 | .@"1_bit" = true, 250 | }, 251 | .load_op = .dont_care, 252 | .store_op = .store, 253 | .stencil_load_op = .dont_care, 254 | .stencil_store_op = .dont_care, 255 | .initial_layout = .present_src_khr, 256 | .final_layout = .present_src_khr, 257 | }, 258 | }; 259 | const color_attachment_refs = [_]vk.AttachmentReference{ 260 | .{ 261 | .attachment = 0, 262 | .layout = .color_attachment_optimal, 263 | }, 264 | }; 265 | const subpass = [_]vk.SubpassDescription{ 266 | .{ 267 | .flags = .{}, 268 | .pipeline_bind_point = .graphics, 269 | .input_attachment_count = 0, 270 | .p_input_attachments = undefined, 271 | .color_attachment_count = color_attachment_refs.len, 272 | .p_color_attachments = &color_attachment_refs, 273 | .p_resolve_attachments = null, 274 | .p_depth_stencil_attachment = null, 275 | .preserve_attachment_count = 0, 276 | .p_preserve_attachments = undefined, 277 | }, 278 | }; 279 | const subpass_dependency = vk.SubpassDependency{ 280 | .src_subpass = vk.SUBPASS_EXTERNAL, 281 | .dst_subpass = 0, 282 | .src_stage_mask = .{ 283 | .color_attachment_output_bit = true, 284 | }, 285 | .dst_stage_mask = .{ 286 | .color_attachment_output_bit = true, 287 | }, 288 | .src_access_mask = .{}, 289 | .dst_access_mask = .{ 290 | .color_attachment_write_bit = true, 291 | }, 292 | .dependency_flags = .{}, 293 | }; 294 | const render_pass_info = vk.RenderPassCreateInfo{ 295 | .flags = .{}, 296 | .attachment_count = color_attachment.len, 297 | .p_attachments = &color_attachment, 298 | .subpass_count = subpass.len, 299 | .p_subpasses = &subpass, 300 | .dependency_count = 1, 301 | .p_dependencies = @ptrCast(&subpass_dependency), 302 | }; 303 | return try self.vkd.createRenderPass(self.logical_device, &render_pass_info, null); 304 | } 305 | 306 | pub fn destroyRenderPass(self: Context, render_pass: vk.RenderPass) void { 307 | self.vkd.destroyRenderPass(self.logical_device, render_pass, null); 308 | } 309 | 310 | pub fn deinit(self: Context) void { 311 | self.vki.destroySurfaceKHR(self.instance, self.surface, null); 312 | self.vkd.destroyDevice(self.logical_device, null); 313 | 314 | if (consts.enable_validation_layers) { 315 | self.vki.destroyDebugUtilsMessengerEXT(self.instance, self.messenger.?, null); 316 | } 317 | self.vki.destroyInstance(self.instance, null); 318 | } 319 | 320 | // TODO: can probably drop function and inline it in init 321 | fn createDefaultDebugCreateInfo() vk.DebugUtilsMessengerCreateInfoEXT { 322 | const message_severity = vk.DebugUtilsMessageSeverityFlagsEXT{ 323 | .verbose_bit_ext = false, 324 | .info_bit_ext = false, 325 | .warning_bit_ext = true, 326 | .error_bit_ext = true, 327 | }; 328 | 329 | const message_type = vk.DebugUtilsMessageTypeFlagsEXT{ 330 | .general_bit_ext = true, 331 | .validation_bit_ext = true, 332 | .performance_bit_ext = true, 333 | }; 334 | 335 | return vk.DebugUtilsMessengerCreateInfoEXT{ 336 | .p_next = null, 337 | .flags = .{}, 338 | .message_severity = message_severity, 339 | .message_type = message_type, 340 | .pfn_user_callback = &validation_layer.messageCallback, 341 | .p_user_data = null, 342 | }; 343 | } 344 | -------------------------------------------------------------------------------- /src/modules/voxel_rt/ImguiGui.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const zgui = @import("zgui"); 4 | const vk = @import("vulkan"); 5 | 6 | const render = @import("../render.zig"); 7 | const Context = render.Context; 8 | 9 | const Camera = @import("Camera.zig"); 10 | const Sun = @import("Sun.zig"); 11 | const BrickState = @import("brick/State.zig"); 12 | const Pipeline = @import("Pipeline.zig"); 13 | const GraphicsPipeline = @import("GraphicsPipeline.zig"); 14 | const Benchmark = @import("Benchmark.zig"); 15 | 16 | pub const StateBinding = struct { 17 | camera_ptr: *Camera, 18 | /// used in benchmark report 19 | grid_state: BrickState, 20 | sun_ptr: *Sun, 21 | gfx_pipeline_shader_constants: *GraphicsPipeline.PushConstant, 22 | }; 23 | 24 | pub const Config = struct { 25 | camera_window_active: bool = true, 26 | metrics_window_active: bool = true, 27 | post_process_window_active: bool = true, 28 | sun_window_active: bool = true, 29 | update_frame_timings: bool = true, 30 | }; 31 | 32 | const MetricState = struct { 33 | update_frame_timings: bool, 34 | frame_times: [128]f32, 35 | min_frame_time: f32, 36 | max_frame_time: f32, 37 | }; 38 | 39 | // build voxel_rt gui for the ImguiPipeline and handle state propagation 40 | const ImguiGui = @This(); 41 | 42 | state_binding: StateBinding, 43 | 44 | camera_window_active: bool, 45 | metrics_window_active: bool, 46 | post_process_window_active: bool, 47 | sun_window_active: bool, 48 | 49 | device_properties: vk.PhysicalDeviceProperties, 50 | 51 | metrics_state: MetricState, 52 | 53 | benchmark: ?Benchmark = null, 54 | 55 | pub fn init(ctx: Context, gui_width: f32, gui_height: f32, state_binding: StateBinding, config: Config) ImguiGui { 56 | // Color scheme 57 | const StyleCol = zgui.StyleCol; 58 | const style = zgui.getStyle(); 59 | style.setColor(StyleCol.title_bg, [4]f32{ 0.1, 0.1, 0.1, 0.85 }); 60 | style.setColor(StyleCol.title_bg_active, [4]f32{ 0.15, 0.15, 0.15, 0.9 }); 61 | style.setColor(StyleCol.menu_bar_bg, [4]f32{ 0.1, 0.1, 0.1, 0.8 }); 62 | style.setColor(StyleCol.header, [4]f32{ 0.1, 0.1, 0.1, 0.8 }); 63 | style.setColor(StyleCol.check_mark, [4]f32{ 0, 1, 0, 1 }); 64 | 65 | // Dimensions 66 | zgui.io.setDisplaySize(gui_width, gui_height); 67 | zgui.io.setDisplayFramebufferScale(1.0, 1.0); 68 | 69 | return ImguiGui{ 70 | .state_binding = state_binding, 71 | .camera_window_active = config.camera_window_active, 72 | .metrics_window_active = config.metrics_window_active, 73 | .post_process_window_active = config.post_process_window_active, 74 | .sun_window_active = config.sun_window_active, 75 | .device_properties = ctx.getPhysicalDeviceProperties(), 76 | .metrics_state = .{ 77 | .update_frame_timings = config.update_frame_timings, 78 | .frame_times = [_]f32{0} ** 128, 79 | .min_frame_time = std.math.floatMax(f32), 80 | .max_frame_time = std.math.floatMin(f32), 81 | }, 82 | }; 83 | } 84 | 85 | /// handle window resizing 86 | pub fn handleRescale(self: ImguiGui, gui_width: f32, gui_height: f32) void { 87 | _ = self; 88 | 89 | zgui.io.setDisplaySize(gui_width, gui_height); 90 | } 91 | 92 | // Starts a new imGui frame and sets up windows and ui elements 93 | pub fn newFrame(self: *ImguiGui, ctx: Context, pipeline: *Pipeline, update_metrics: bool, dt: f32) void { 94 | _ = ctx; 95 | zgui.newFrame(); 96 | 97 | const style = zgui.getStyle(); 98 | const rounding = style.window_rounding; 99 | style.window_rounding = 0; // no rounding for top menu 100 | 101 | zgui.setNextWindowSize( 102 | .{ 103 | .w = @floatFromInt(pipeline.swapchain.extent.width), 104 | .h = 0, 105 | .cond = .always, 106 | }, 107 | ); 108 | zgui.setNextWindowPos(.{ .x = 0, .y = 0, .cond = .always }); 109 | _ = zgui.begin("Main menu", .{ .flags = .{ 110 | .menu_bar = true, 111 | .no_move = true, 112 | .no_resize = true, 113 | .no_title_bar = true, 114 | .no_scrollbar = true, 115 | .no_scroll_with_mouse = true, 116 | .no_collapse = true, 117 | .no_background = true, 118 | } }); 119 | style.window_rounding = rounding; 120 | 121 | blk: { 122 | if (zgui.beginMenuBar() == false) break :blk; 123 | defer zgui.endMenuBar(); 124 | 125 | if (zgui.beginMenu("Windows", true) == false) break :blk; 126 | defer zgui.endMenu(); 127 | 128 | if (zgui.menuItem("Camera", .{ .selected = self.camera_window_active, .enabled = true })) { 129 | self.camera_window_active = !self.camera_window_active; 130 | } 131 | if (zgui.menuItem("Metrics", .{ .selected = self.metrics_window_active, .enabled = true })) { 132 | self.metrics_window_active = !self.metrics_window_active; 133 | } 134 | if (zgui.menuItem("Post process", .{ .selected = self.post_process_window_active, .enabled = true })) { 135 | self.post_process_window_active = !self.post_process_window_active; 136 | } 137 | if (zgui.menuItem("Sun", .{ .selected = self.sun_window_active, .enabled = true })) { 138 | self.sun_window_active = !self.sun_window_active; 139 | } 140 | } 141 | zgui.end(); 142 | 143 | blk: { 144 | if (!self.metrics_state.update_frame_timings or !update_metrics) { 145 | break :blk; 146 | } 147 | std.mem.rotate(f32, self.metrics_state.frame_times[0..], 1); 148 | const frame_time = dt * std.time.ms_per_s; 149 | self.metrics_state.frame_times[self.metrics_state.frame_times.len - 1] = frame_time; 150 | self.metrics_state.min_frame_time = @min(self.metrics_state.min_frame_time, frame_time); 151 | self.metrics_state.max_frame_time = @max(self.metrics_state.max_frame_time, frame_time); 152 | } 153 | 154 | self.benchmark = blk: { 155 | if (self.benchmark) |*b| { 156 | if (b.update(dt)) { 157 | self.state_binding.camera_ptr.reset(); 158 | b.printReport(self.device_properties.device_name[0..]); 159 | break :blk null; 160 | } 161 | } 162 | break :blk self.benchmark; 163 | }; 164 | 165 | self.drawCameraWindowIfEnabled(); 166 | self.drawMetricsWindowIfEnabled(); 167 | self.drawPostProcessWindowIfEnabled(); 168 | self.drawPostSunWindowIfEnabled(); 169 | 170 | // imgui.igSetNextWindowPos(.{ .x = 650, .y = 20 }, imgui.ImGuiCond_FirstUseEver, .{ .x = 0, .y = 0 }); 171 | // imgui.igShowDemoWindow(null); 172 | 173 | zgui.render(); 174 | } 175 | 176 | inline fn drawCameraWindowIfEnabled(self: *ImguiGui) void { 177 | if (self.camera_window_active == false) return; 178 | 179 | zgui.setNextWindowSize(.{ 180 | .w = 400, 181 | .h = 500, 182 | .cond = .first_use_ever, 183 | }); 184 | const camera_open = zgui.begin("Camera", .{ .popen = &self.camera_window_active }); 185 | defer zgui.end(); 186 | if (camera_open == false) return; 187 | 188 | _ = zgui.sliderInt("max bounces", .{ 189 | .v = &self.state_binding.camera_ptr.d_camera.max_bounce, 190 | .min = 1, 191 | .max = 32, 192 | }); 193 | imguiToolTip("how many times a ray is allowed to bounce before terminating", .{}); 194 | _ = zgui.sliderInt("samples per pixel", .{ 195 | .v = &self.state_binding.camera_ptr.d_camera.samples_per_pixel, 196 | .min = 1, 197 | .max = 32, 198 | }); 199 | imguiToolTip("how many rays per pixel", .{}); 200 | 201 | _ = zgui.inputFloat("move speed", .{ .v = &self.state_binding.camera_ptr.normal_speed }); 202 | _ = zgui.inputFloat("turn rate", .{ .v = &self.state_binding.camera_ptr.turn_rate }); 203 | 204 | var camera_origin: [3]f32 = self.state_binding.camera_ptr.d_camera.origin; 205 | const camera_origin_changed = zgui.inputFloat3("position", .{ .v = &camera_origin }); 206 | if (camera_origin_changed) { 207 | self.state_binding.camera_ptr.setOrigin(camera_origin); 208 | } 209 | } 210 | 211 | inline fn drawMetricsWindowIfEnabled(self: *ImguiGui) void { 212 | if (self.metrics_window_active == false) return; 213 | 214 | zgui.setNextWindowSize(.{ 215 | .w = 400, 216 | .h = 500, 217 | .cond = .first_use_ever, 218 | }); 219 | const metrics_open = zgui.begin("Metrics", .{ .popen = &self.metrics_window_active }); 220 | defer zgui.end(); 221 | if (metrics_open == false) return; 222 | 223 | const zero_index = std.mem.indexOf(u8, &self.device_properties.device_name, &[_]u8{0}); 224 | zgui.textUnformatted(self.device_properties.device_name[0..zero_index.?]); 225 | 226 | if (zgui.plot.beginPlot("Frame times", .{})) { 227 | defer zgui.plot.endPlot(); 228 | 229 | // x axis 230 | zgui.plot.setupAxis(.x1, .{ .label = "frame" }); 231 | zgui.plot.setupAxisLimits(.x1, .{ .min = 0, .max = self.metrics_state.frame_times.len }); 232 | 233 | // y axis 234 | zgui.plot.setupAxis(.y1, .{ .label = "time (ms)" }); 235 | zgui.plot.setupAxisLimits(.y1, .{ .min = 0, .max = @floatCast(30) }); 236 | 237 | zgui.plot.setupFinish(); 238 | 239 | zgui.plot.plotLineValues("Frame times", f32, .{ 240 | .v = &self.metrics_state.frame_times, 241 | }); 242 | } 243 | 244 | zgui.text("Recent frame time: {d:>8.3}", .{self.metrics_state.frame_times[self.metrics_state.frame_times.len - 1]}); 245 | zgui.text("Minimum frame time: {d:>8.3}", .{self.metrics_state.min_frame_time}); 246 | zgui.text("Maximum frame time: {d:>8.3}", .{self.metrics_state.max_frame_time}); 247 | 248 | if (zgui.collapsingHeader("Benchmark", .{})) { 249 | const benchmark_active = self.benchmark != null; 250 | if (benchmark_active) { 251 | // imgui.igPushItemFlag(ImGuiButtonFlags_Disabled, true); 252 | zgui.pushStyleVar1f(.{ .idx = .alpha, .v = zgui.getStyle().alpha * 0.5 }); 253 | } 254 | if (zgui.button("Start benchmark", .{ .w = 200, .h = 80 })) { 255 | if (benchmark_active == false) { 256 | // reset sun to avoid any difference in lighting affecting performance 257 | if (self.state_binding.sun_ptr.device_data.enabled > 0 and self.state_binding.sun_ptr.animate) { 258 | self.state_binding.sun_ptr.* = Sun.init(.{}); 259 | } 260 | self.benchmark = Benchmark.init( 261 | self.state_binding.camera_ptr, 262 | self.state_binding.grid_state, 263 | (self.state_binding.sun_ptr.device_data.enabled > 0), 264 | ); 265 | } 266 | } 267 | imguiToolTip("benchmark will control camera and create a report to stdout", .{}); 268 | if (benchmark_active) { 269 | // imgui.igPopItemFlag(); 270 | zgui.popStyleVar(.{}); 271 | } 272 | } 273 | } 274 | 275 | inline fn drawPostProcessWindowIfEnabled(self: *ImguiGui) void { 276 | if (self.post_process_window_active == false) return; 277 | 278 | zgui.setNextWindowSize(.{ 279 | .w = 400, 280 | .h = 500, 281 | .cond = .first_use_ever, 282 | }); 283 | const post_window_open = zgui.begin("Post process", .{ .popen = &self.post_process_window_active }); 284 | defer zgui.end(); 285 | if (post_window_open == false) return; 286 | 287 | _ = zgui.inputInt("Samples", .{ .v = &self.state_binding.gfx_pipeline_shader_constants.samples, .step_fast = 2 }); 288 | imguiToolTip("Higher sample count result in less noise\nThis comes at the cost of performance", .{}); 289 | 290 | _ = zgui.sliderFloat("Distribution bias", .{ 291 | .v = &self.state_binding.gfx_pipeline_shader_constants.distribution_bias, 292 | .min = 0, 293 | .max = 1, 294 | }); 295 | _ = zgui.sliderFloat("Pixel Multiplier", .{ 296 | .v = &self.state_binding.gfx_pipeline_shader_constants.pixel_multiplier, 297 | .min = 1, 298 | .max = 3, 299 | }); 300 | imguiToolTip("should be kept low", .{}); 301 | _ = zgui.sliderFloat("Inverse Hue Tolerance", .{ 302 | .v = &self.state_binding.gfx_pipeline_shader_constants.inverse_hue_tolerance, 303 | .min = 2, 304 | .max = 30, 305 | }); 306 | } 307 | 308 | inline fn drawPostSunWindowIfEnabled(self: *ImguiGui) void { 309 | if (self.sun_window_active == false) return; 310 | 311 | zgui.setNextWindowSize(.{ 312 | .w = 400, 313 | .h = 500, 314 | .cond = .first_use_ever, 315 | }); 316 | 317 | const sun_open = zgui.begin("Sun", .{ .popen = &self.sun_window_active }); 318 | defer zgui.end(); 319 | if (sun_open == false) return; 320 | 321 | var enabled = (self.state_binding.sun_ptr.device_data.enabled > 0); 322 | _ = zgui.checkbox("enabled", .{ .v = &enabled }); 323 | self.state_binding.sun_ptr.device_data.enabled = if (enabled) 1 else 0; 324 | 325 | _ = zgui.dragFloat3("position", .{ 326 | .v = &self.state_binding.sun_ptr.device_data.position, 327 | .speed = 1, 328 | .min = -10000, 329 | .max = 10000, 330 | }); 331 | _ = zgui.colorEdit3("color", .{ .col = &self.state_binding.sun_ptr.device_data.color }); 332 | _ = zgui.dragFloat("radius", .{ .v = &self.state_binding.sun_ptr.device_data.radius, .speed = 1, .min = 0, .max = 20 }); 333 | 334 | if (zgui.collapsingHeader("Animation", .{})) { 335 | _ = zgui.checkbox("animate", .{ .v = &self.state_binding.sun_ptr.animate }); 336 | var speed: f32 = self.state_binding.sun_ptr.animate_speed / 3; 337 | const speed_changed = zgui.inputFloat("speed", .{ .v = &speed }); 338 | imguiToolTip("how long a day and night last in seconds", .{}); 339 | if (speed_changed) { 340 | self.state_binding.sun_ptr.animate_speed = speed * 3; 341 | } 342 | // TODO: allow these to be changed: (?) 343 | // slerp_orientations: [3]za.Quat, 344 | // lerp_color: [3]za.Vec3, 345 | // static_pos_vec: za.Vec3, 346 | } 347 | } 348 | 349 | const ToolTipConfig = struct { 350 | offset_from_start: f32 = 0, 351 | spacing: f32 = 10, 352 | }; 353 | fn imguiToolTip(comptime tip: []const u8, config: ToolTipConfig) void { 354 | zgui.sameLine(.{ 355 | .offset_from_start_x = config.offset_from_start, 356 | .spacing = config.spacing, 357 | }); 358 | zgui.textDisabled("(?)", .{}); 359 | if (zgui.isItemHovered(.{})) { 360 | _ = zgui.beginTooltip(); 361 | zgui.pushTextWrapPos(450); 362 | zgui.textUnformatted(tip); 363 | zgui.popTextWrapPos(); 364 | zgui.endTooltip(); 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /src/modules/Input.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | 4 | const zglfw = @import("zglfw"); 5 | const zgui = @import("zgui"); 6 | 7 | pub const WindowHandle = zglfw.Window; 8 | pub const Key = zglfw.Key; 9 | pub const Action = zglfw.Action; 10 | pub const Mods = zglfw.Mods; 11 | pub const MouseButton = zglfw.MouseButton; 12 | 13 | // TODO: use callbacks for easier key binding 14 | // const int scancode = glfwGetKeyScancode(GLFW_KEY_X); 15 | // set_key_mapping(scancode, swap_weapons); 16 | 17 | // TODO: imgui should be optional 18 | 19 | // TODO: thread safety 20 | 21 | pub const KeyEvent = struct { 22 | key: Key, 23 | action: Action, 24 | mods: Mods, 25 | }; 26 | 27 | pub const MouseButtonEvent = struct { 28 | button: MouseButton, 29 | action: Action, 30 | mods: Mods, 31 | }; 32 | 33 | pub const CursorPosEvent = struct { 34 | x: f64, 35 | y: f64, 36 | }; 37 | 38 | pub const KeyHandleFn = *const fn (KeyEvent) void; 39 | pub const MouseButtonHandleFn = *const fn (MouseButtonEvent) void; 40 | pub const CursorPosHandleFn = *const fn (CursorPosEvent) void; 41 | 42 | const WindowContext = struct { 43 | allocator: Allocator, 44 | imgui_want_input: bool, 45 | key_handle_fn: KeyHandleFn, 46 | mouse_btn_handle_fn: MouseButtonHandleFn, 47 | cursor_pos_handle_fn: CursorPosHandleFn, 48 | }; 49 | 50 | const ImguiContext = struct { 51 | hand: *zglfw.Cursor, 52 | arrow: *zglfw.Cursor, 53 | ibeam: *zglfw.Cursor, 54 | crosshair: *zglfw.Cursor, 55 | resize_ns: *zglfw.Cursor, 56 | resize_ew: *zglfw.Cursor, 57 | resize_nesw: *zglfw.Cursor, 58 | resize_nwse: *zglfw.Cursor, 59 | not_allowed: *zglfw.Cursor, 60 | }; 61 | 62 | const Input = @This(); 63 | 64 | window: *zglfw.Window, 65 | window_context: *WindowContext, 66 | imgui_context: ImguiContext, 67 | 68 | /// !This will set the glfw window user context! 69 | /// create a input module. 70 | pub fn init( 71 | allocator: Allocator, 72 | input_window: *zglfw.Window, 73 | input_handle_fn: KeyHandleFn, 74 | input_mouse_btn_handle_fn: MouseButtonHandleFn, 75 | input_cursor_pos_handle_fn: CursorPosHandleFn, 76 | ) !Input { 77 | const window = input_window; 78 | const window_context = try allocator.create(WindowContext); 79 | window_context.* = .{ 80 | .allocator = allocator, 81 | .imgui_want_input = false, 82 | .key_handle_fn = input_handle_fn, 83 | .mouse_btn_handle_fn = input_mouse_btn_handle_fn, 84 | .cursor_pos_handle_fn = input_cursor_pos_handle_fn, 85 | }; 86 | 87 | try window.setInputMode(.cursor, .normal); 88 | 89 | _ = window.setKeyCallback(keyCallback); 90 | _ = window.setCharCallback(charCallback); 91 | _ = window.setMouseButtonCallback(mouseBtnCallback); 92 | _ = window.setCursorPosCallback(cursorPosCallback); 93 | _ = window.setScrollCallback(scrollCallback); 94 | window.setUserPointer(@ptrCast(window_context)); 95 | 96 | const imgui_context = try linkImguiCodes(); 97 | 98 | return Input{ 99 | .window = window, 100 | .window_context = window_context, 101 | .imgui_context = imgui_context, 102 | }; 103 | } 104 | 105 | /// kill input module 106 | pub fn deinit(self: Input, allocator: Allocator) void { 107 | self.window.setUserPointer(null); 108 | allocator.destroy(self.window_context); 109 | 110 | // unregister callback functions 111 | _ = self.window.setKeyCallback(null); 112 | _ = self.window.setCharCallback(null); 113 | _ = self.window.setMouseButtonCallback(null); 114 | _ = self.window.setCursorPosCallback(null); 115 | _ = self.window.setScrollCallback(null); 116 | } 117 | 118 | pub fn setImguiWantInput(self: Input, want_input: bool) void { 119 | self.window_context.imgui_want_input = want_input; 120 | } 121 | 122 | pub fn setInputModeCursor(self: Input, mode: zglfw.Cursor.Mode) !void { 123 | try self.window.setInputMode(.cursor, mode); 124 | } 125 | 126 | pub fn setCursorPosCallback(self: Input, input_cursor_pos_handle_fn: CursorPosHandleFn) void { 127 | self.window_context.cursor_pos_handle_fn = input_cursor_pos_handle_fn; 128 | } 129 | 130 | pub fn setKeyCallback(self: Input, input_key_handle_fn: KeyHandleFn) void { 131 | self.window_context.key_handle_fn = input_key_handle_fn; 132 | } 133 | 134 | /// update cursor based on imgui 135 | pub fn updateCursor(self: *Input) !void { 136 | const context = if (self.window.getUserPointer(WindowContext)) |some| some else return; 137 | if (context.imgui_want_input == false) { 138 | return; 139 | } 140 | 141 | try self.window.setInputMode(.cursor, .normal); 142 | switch (zgui.getMouseCursor()) { 143 | .none => try self.window.setInputMode(.cursor, .hidden), 144 | .arrow => self.window.setCursor(self.imgui_context.arrow), 145 | .text_input => self.window.setCursor(self.imgui_context.ibeam), 146 | .resize_all => self.window.setCursor(self.imgui_context.crosshair), 147 | .resize_ns => self.window.setCursor(self.imgui_context.resize_ns), 148 | .resize_ew => self.window.setCursor(self.imgui_context.resize_ew), 149 | .resize_nesw => self.window.setCursor(self.imgui_context.resize_nesw), 150 | .resize_nwse => self.window.setCursor(self.imgui_context.resize_nwse), 151 | .hand => self.window.setCursor(self.imgui_context.hand), 152 | .not_allowed => self.window.setCursor(self.imgui_context.not_allowed), 153 | .count => self.window.setCursor(self.imgui_context.ibeam), 154 | } 155 | } 156 | 157 | fn keyCallback(window: *zglfw.Window, key: Key, scan_code: c_int, action: Action, mods: Mods) callconv(.c) void { 158 | _ = scan_code; 159 | 160 | var owned_mods = mods; 161 | const parsed_mods: *Mods = @ptrCast(&owned_mods); 162 | const event = KeyEvent{ 163 | .key = key, 164 | .action = action, 165 | .mods = parsed_mods.*, 166 | }; 167 | const context = if (window.getUserPointer(WindowContext)) |some| some else return; 168 | context.key_handle_fn(event); 169 | 170 | if (context.imgui_want_input) { 171 | zgui.io.addKeyEvent(zgui.Key.mod_shift, mods.shift); 172 | zgui.io.addKeyEvent(zgui.Key.mod_ctrl, mods.control); 173 | zgui.io.addKeyEvent(zgui.Key.mod_alt, mods.alt); 174 | zgui.io.addKeyEvent(zgui.Key.mod_super, mods.super); 175 | // zgui.addKeyEvent(zgui.Key.mod_caps_lock, mod.caps_lock); 176 | // zgui.addKeyEvent(zgui.Key.mod_num_lock, mod.num_lock); 177 | 178 | zgui.io.addKeyEvent(mapGlfwKeyToImgui(key), action == .press); 179 | } 180 | } 181 | 182 | fn charCallback(window: *zglfw.Window, codepoint: u32) callconv(.c) void { 183 | const context = if (window.getUserPointer(WindowContext)) |some| some else return; 184 | if (context.imgui_want_input) { 185 | var buffer: [8]u8 = undefined; 186 | const code: u21 = @intCast(codepoint); 187 | const len = std.unicode.utf8Encode(code, buffer[0..]) catch return; 188 | const cstr = buffer[0 .. len + 1]; 189 | cstr[len] = 0; // null terminator 190 | zgui.io.addInputCharactersUTF8(@ptrCast(cstr.ptr)); 191 | } 192 | } 193 | 194 | fn mouseBtnCallback(window: *zglfw.Window, button: MouseButton, action: Action, mods: Mods) callconv(.c) void { 195 | var owned_mods = mods; 196 | const parsed_mods: *Mods = @ptrCast(&owned_mods); 197 | const event = MouseButtonEvent{ 198 | .button = button, 199 | .action = action, 200 | .mods = parsed_mods.*, 201 | }; 202 | const context = if (window.getUserPointer(WindowContext)) |some| some else return; 203 | context.mouse_btn_handle_fn(event); 204 | 205 | if (context.imgui_want_input) { 206 | if (switch (button) { 207 | .left => zgui.MouseButton.left, 208 | .right => zgui.MouseButton.right, 209 | .middle => zgui.MouseButton.middle, 210 | .four, .five, .six, .seven, .eight => null, 211 | }) |zgui_button| { 212 | // apply modifiers 213 | zgui.io.addKeyEvent(zgui.Key.mod_shift, mods.shift); 214 | zgui.io.addKeyEvent(zgui.Key.mod_ctrl, mods.control); 215 | zgui.io.addKeyEvent(zgui.Key.mod_alt, mods.alt); 216 | zgui.io.addKeyEvent(zgui.Key.mod_super, mods.super); 217 | 218 | zgui.io.addMouseButtonEvent(zgui_button, action == .press); 219 | } 220 | } 221 | } 222 | 223 | fn cursorPosCallback(window: *zglfw.Window, x_pos: f64, y_pos: f64) callconv(.c) void { 224 | const event = CursorPosEvent{ 225 | .x = x_pos, 226 | .y = y_pos, 227 | }; 228 | const context = if (window.getUserPointer(WindowContext)) |some| some else return; 229 | context.cursor_pos_handle_fn(event); 230 | 231 | if (context.imgui_want_input) { 232 | zgui.io.addMousePositionEvent(@floatCast(x_pos), @floatCast(y_pos)); 233 | } 234 | } 235 | 236 | fn scrollCallback(window: *zglfw.Window, xoffset: f64, yoffset: f64) callconv(.c) void { 237 | const context = if (window.getUserPointer(WindowContext)) |some| some else return; 238 | 239 | if (context.imgui_want_input) { 240 | zgui.io.addMouseWheelEvent(@floatCast(xoffset), @floatCast(yoffset)); 241 | } 242 | } 243 | 244 | /// link imgui and glfw codes 245 | fn linkImguiCodes() !ImguiContext { 246 | const hand = try zglfw.Cursor.createStandard(.hand); 247 | errdefer hand.destroy(); 248 | const arrow = try zglfw.Cursor.createStandard(.arrow); 249 | errdefer arrow.destroy(); 250 | const ibeam = try zglfw.Cursor.createStandard(.ibeam); 251 | errdefer ibeam.destroy(); 252 | const crosshair = try zglfw.Cursor.createStandard(.crosshair); 253 | errdefer crosshair.destroy(); 254 | const resize_ns = try zglfw.Cursor.createStandard(.resize_ns); 255 | errdefer resize_ns.destroy(); 256 | const resize_ew = try zglfw.Cursor.createStandard(.resize_ew); 257 | errdefer resize_ew.destroy(); 258 | const resize_nesw = try zglfw.Cursor.createStandard(.resize_nesw); 259 | errdefer resize_nesw.destroy(); 260 | const resize_nwse = try zglfw.Cursor.createStandard(.resize_nwse); 261 | errdefer resize_nwse.destroy(); 262 | const not_allowed = try zglfw.Cursor.createStandard(.not_allowed); 263 | errdefer not_allowed.destroy(); 264 | 265 | return ImguiContext{ 266 | .hand = hand, 267 | .arrow = arrow, 268 | .ibeam = ibeam, 269 | .crosshair = crosshair, 270 | .resize_ns = resize_ns, 271 | .resize_ew = resize_ew, 272 | .resize_nesw = resize_nesw, 273 | .resize_nwse = resize_nwse, 274 | .not_allowed = not_allowed, 275 | }; 276 | } 277 | 278 | fn getClipboardTextFn(ctx: ?*anyopaque) callconv(.C) [*c]const u8 { 279 | _ = ctx; 280 | 281 | const clipboard_string = zglfw.getClipboardString() catch blk: { 282 | break :blk ""; 283 | }; 284 | return clipboard_string; 285 | } 286 | 287 | fn setClipboardTextFn(ctx: ?*anyopaque, text: [*c]const u8) callconv(.C) void { 288 | _ = ctx; 289 | zglfw.setClipboardString(text) catch {}; 290 | } 291 | 292 | inline fn mapGlfwKeyToImgui(key: zglfw.Key) zgui.Key { 293 | return switch (key) { 294 | .unknown => zgui.Key.none, 295 | .space => zgui.Key.space, 296 | .apostrophe => zgui.Key.apostrophe, 297 | .comma => zgui.Key.comma, 298 | .minus => zgui.Key.minus, 299 | .period => zgui.Key.period, 300 | .slash => zgui.Key.slash, 301 | .zero => zgui.Key.zero, 302 | .one => zgui.Key.one, 303 | .two => zgui.Key.two, 304 | .three => zgui.Key.three, 305 | .four => zgui.Key.four, 306 | .five => zgui.Key.five, 307 | .six => zgui.Key.six, 308 | .seven => zgui.Key.seven, 309 | .eight => zgui.Key.eight, 310 | .nine => zgui.Key.nine, 311 | .semicolon => zgui.Key.semicolon, 312 | .equal => zgui.Key.equal, 313 | .a => zgui.Key.a, 314 | .b => zgui.Key.b, 315 | .c => zgui.Key.c, 316 | .d => zgui.Key.d, 317 | .e => zgui.Key.e, 318 | .f => zgui.Key.f, 319 | .g => zgui.Key.g, 320 | .h => zgui.Key.h, 321 | .i => zgui.Key.i, 322 | .j => zgui.Key.j, 323 | .k => zgui.Key.k, 324 | .l => zgui.Key.l, 325 | .m => zgui.Key.m, 326 | .n => zgui.Key.n, 327 | .o => zgui.Key.o, 328 | .p => zgui.Key.p, 329 | .q => zgui.Key.q, 330 | .r => zgui.Key.r, 331 | .s => zgui.Key.s, 332 | .t => zgui.Key.t, 333 | .u => zgui.Key.u, 334 | .v => zgui.Key.v, 335 | .w => zgui.Key.w, 336 | .x => zgui.Key.x, 337 | .y => zgui.Key.y, 338 | .z => zgui.Key.z, 339 | .left_bracket => zgui.Key.left_bracket, 340 | .backslash => zgui.Key.back_slash, 341 | .right_bracket => zgui.Key.right_bracket, 342 | .grave_accent => zgui.Key.grave_accent, 343 | .world_1 => zgui.Key.none, // ???? 344 | .world_2 => zgui.Key.none, // ???? 345 | .escape => zgui.Key.escape, 346 | .enter => zgui.Key.enter, 347 | .tab => zgui.Key.tab, 348 | .backspace => zgui.Key.back_space, 349 | .insert => zgui.Key.insert, 350 | .delete => zgui.Key.delete, 351 | .right => zgui.Key.right_arrow, 352 | .left => zgui.Key.left_arrow, 353 | .down => zgui.Key.down_arrow, 354 | .up => zgui.Key.up_arrow, 355 | .page_up => zgui.Key.page_up, 356 | .page_down => zgui.Key.page_down, 357 | .home => zgui.Key.home, 358 | .end => zgui.Key.end, 359 | .caps_lock => zgui.Key.caps_lock, 360 | .scroll_lock => zgui.Key.scroll_lock, 361 | .num_lock => zgui.Key.num_lock, 362 | .print_screen => zgui.Key.print_screen, 363 | .pause => zgui.Key.pause, 364 | .F1 => zgui.Key.f1, 365 | .F2 => zgui.Key.f2, 366 | .F3 => zgui.Key.f3, 367 | .F4 => zgui.Key.f4, 368 | .F5 => zgui.Key.f5, 369 | .F6 => zgui.Key.f6, 370 | .F7 => zgui.Key.f7, 371 | .F8 => zgui.Key.f8, 372 | .F9 => zgui.Key.f9, 373 | .F10 => zgui.Key.f10, 374 | .F11 => zgui.Key.f11, 375 | .F12 => zgui.Key.f12, 376 | .F13, 377 | .F14, 378 | .F15, 379 | .F16, 380 | .F17, 381 | .F18, 382 | .F19, 383 | .F20, 384 | .F21, 385 | .F22, 386 | .F23, 387 | .F24, 388 | .F25, 389 | => zgui.Key.none, 390 | .kp_0 => zgui.Key.keypad_0, 391 | .kp_1 => zgui.Key.keypad_1, 392 | .kp_2 => zgui.Key.keypad_2, 393 | .kp_3 => zgui.Key.keypad_3, 394 | .kp_4 => zgui.Key.keypad_4, 395 | .kp_5 => zgui.Key.keypad_5, 396 | .kp_6 => zgui.Key.keypad_6, 397 | .kp_7 => zgui.Key.keypad_7, 398 | .kp_8 => zgui.Key.keypad_8, 399 | .kp_9 => zgui.Key.keypad_9, 400 | .kp_decimal => zgui.Key.keypad_decimal, 401 | .kp_divide => zgui.Key.keypad_divide, 402 | .kp_multiply => zgui.Key.keypad_multiply, 403 | .kp_subtract => zgui.Key.keypad_subtract, 404 | .kp_add => zgui.Key.keypad_add, 405 | .kp_enter => zgui.Key.keypad_enter, 406 | .kp_equal => zgui.Key.keypad_equal, 407 | .left_shift => zgui.Key.left_shift, 408 | .left_control => zgui.Key.left_ctrl, 409 | .left_alt => zgui.Key.left_alt, 410 | .left_super => zgui.Key.left_super, 411 | .right_shift => zgui.Key.right_shift, 412 | .right_control => zgui.Key.right_ctrl, 413 | .right_alt => zgui.Key.right_alt, 414 | .right_super => zgui.Key.right_super, 415 | .menu => zgui.Key.menu, 416 | }; 417 | } 418 | -------------------------------------------------------------------------------- /src/modules/render/Texture.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const vk = @import("vulkan"); 3 | 4 | const vk_utils = @import("vk_utils.zig"); 5 | const GpuBufferMemory = @import("GpuBufferMemory.zig"); 6 | const Context = @import("Context.zig"); 7 | const Allocator = std.mem.Allocator; 8 | 9 | pub fn Config(comptime T: type) type { 10 | return struct { 11 | data: ?[]T, 12 | width: u32, 13 | height: u32, 14 | usage: vk.ImageUsageFlags, 15 | queue_family_indices: []const u32, 16 | format: vk.Format, 17 | }; 18 | } 19 | 20 | const Texture = @This(); 21 | 22 | image_size: vk.DeviceSize, 23 | image_extent: vk.Extent2D, 24 | 25 | image: vk.Image, 26 | image_view: vk.ImageView, 27 | image_memory: vk.DeviceMemory, 28 | format: vk.Format, 29 | sampler: vk.Sampler, 30 | layout: vk.ImageLayout, 31 | 32 | // TODO: comptime send_to_device: bool to disable all useless transfer 33 | pub fn init(ctx: Context, command_pool: vk.CommandPool, layout: vk.ImageLayout, comptime T: type, config: Config(T)) !Texture { 34 | const image_extent = vk.Extent2D{ 35 | .width = config.width, 36 | .height = config.height, 37 | }; 38 | 39 | const image = blk: { 40 | // TODO: make sure we use correct usage bits https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VkImageUsageFlagBits.html 41 | // TODO: VK_IMAGE_CREATE_SPARSE_BINDING_BIT 42 | const image_info = vk.ImageCreateInfo{ 43 | .flags = .{}, 44 | .image_type = .@"2d", 45 | .format = config.format, 46 | .extent = vk.Extent3D{ 47 | .width = config.width, 48 | .height = config.height, 49 | .depth = 1, 50 | }, 51 | .mip_levels = 1, 52 | .array_layers = 1, 53 | .samples = .{ 54 | .@"1_bit" = true, 55 | }, 56 | .tiling = .optimal, 57 | .usage = config.usage, 58 | .sharing_mode = .exclusive, // TODO: concurrent if (queue_family_indices.len > 1) 59 | .queue_family_index_count = @intCast(config.queue_family_indices.len), 60 | .p_queue_family_indices = config.queue_family_indices.ptr, 61 | .initial_layout = .undefined, 62 | }; 63 | break :blk try ctx.vkd.createImage(ctx.logical_device, &image_info, null); 64 | }; 65 | 66 | const image_memory = blk: { 67 | const memory_requirements = ctx.vkd.getImageMemoryRequirements(ctx.logical_device, image); 68 | const alloc_info = vk.MemoryAllocateInfo{ 69 | .allocation_size = memory_requirements.size, 70 | .memory_type_index = try vk_utils.findMemoryTypeIndex(ctx, memory_requirements.memory_type_bits, .{ 71 | .device_local_bit = true, 72 | }), 73 | }; 74 | break :blk try ctx.vkd.allocateMemory(ctx.logical_device, &alloc_info, null); 75 | }; 76 | 77 | try ctx.vkd.bindImageMemory(ctx.logical_device, image, image_memory, 0); 78 | 79 | // TODO: set size according to image format 80 | const image_size: vk.DeviceSize = @intCast(config.width * config.height * @sizeOf(f32)); 81 | 82 | // transfer texture data to gpu 83 | if (config.data) |data| { 84 | var staging_buffer = try GpuBufferMemory.init(ctx, image_size, .{ 85 | .transfer_src_bit = true, 86 | }, .{ 87 | .host_visible_bit = true, 88 | .host_coherent_bit = true, 89 | }); 90 | defer staging_buffer.deinit(ctx); 91 | try staging_buffer.transferToDevice(ctx, T, 0, data); 92 | 93 | const transition_1 = [_]TransitionConfig{.{ 94 | .image = image, 95 | .old_layout = .undefined, 96 | .new_layout = .transfer_dst_optimal, 97 | }}; 98 | try transitionImageLayouts(ctx, command_pool, &transition_1); 99 | 100 | try copyBufferToImage(ctx, command_pool, image, staging_buffer.buffer, image_extent); 101 | 102 | const transition_2 = [_]TransitionConfig{.{ 103 | .image = image, 104 | .old_layout = .transfer_dst_optimal, 105 | .new_layout = .layout, 106 | }}; 107 | try transitionImageLayouts(ctx, command_pool, &transition_2); 108 | } else { 109 | const transition = [_]TransitionConfig{.{ 110 | .image = image, 111 | .old_layout = .undefined, 112 | .new_layout = layout, 113 | }}; 114 | try transitionImageLayouts(ctx, command_pool, &transition); 115 | } 116 | 117 | const image_view = blk: { 118 | // TODO: evaluate if this and swapchain should share logic (probably no) 119 | const image_view_info = vk.ImageViewCreateInfo{ 120 | .flags = .{}, 121 | .image = image, 122 | .view_type = .@"2d", 123 | .format = config.format, 124 | .components = .{ 125 | .r = .identity, 126 | .g = .identity, 127 | .b = .identity, 128 | .a = .identity, 129 | }, 130 | .subresource_range = .{ 131 | .aspect_mask = .{ 132 | .color_bit = true, 133 | }, 134 | .base_mip_level = 0, 135 | .level_count = 1, 136 | .base_array_layer = 0, 137 | .layer_count = 1, 138 | }, 139 | }; 140 | break :blk try ctx.vkd.createImageView(ctx.logical_device, &image_view_info, null); 141 | }; 142 | 143 | const sampler = blk: { 144 | // const device_properties = ctx.vki.getPhysicalDeviceProperties(ctx.physical_device); 145 | const sampler_info = vk.SamplerCreateInfo{ 146 | .flags = .{}, 147 | .mag_filter = .linear, // not sure what the application would need 148 | .min_filter = .linear, // RT should use linear, pixel sim should be nearest 149 | .mipmap_mode = .linear, 150 | .address_mode_u = .repeat, 151 | .address_mode_v = .repeat, 152 | .address_mode_w = .repeat, 153 | .mip_lod_bias = 0.0, 154 | .anisotropy_enable = vk.FALSE, // TODO: test with, and without 155 | .max_anisotropy = 1.0, // device_properties.limits.max_sampler_anisotropy, 156 | .compare_enable = vk.FALSE, 157 | .compare_op = .always, 158 | .min_lod = 0.0, 159 | .max_lod = 0.0, 160 | .border_color = .int_opaque_black, 161 | .unnormalized_coordinates = vk.FALSE, // TODO: might be good for pixel sim to use true 162 | }; 163 | break :blk try ctx.vkd.createSampler(ctx.logical_device, &sampler_info, null); 164 | }; 165 | 166 | return Texture{ 167 | .image_size = image_size, 168 | .image_extent = image_extent, 169 | .image = image, 170 | .image_view = image_view, 171 | .image_memory = image_memory, 172 | .format = config.format, 173 | .sampler = sampler, 174 | .layout = layout, 175 | }; 176 | } 177 | 178 | pub fn deinit(self: Texture, ctx: Context) void { 179 | ctx.vkd.destroySampler(ctx.logical_device, self.sampler, null); 180 | ctx.vkd.destroyImageView(ctx.logical_device, self.image_view, null); 181 | ctx.vkd.destroyImage(ctx.logical_device, self.image, null); 182 | ctx.vkd.freeMemory(ctx.logical_device, self.image_memory, null); 183 | } 184 | 185 | pub fn copyToHost(self: Texture, command_pool: vk.CommandPool, ctx: Context, comptime T: type, buffer: []T) !void { 186 | var staging_buffer = try GpuBufferMemory.init(ctx, self.image_size, .{ 187 | .transfer_dst_bit = true, 188 | }, .{ 189 | .host_visible_bit = true, 190 | .host_coherent_bit = true, 191 | }); 192 | defer staging_buffer.deinit(ctx); 193 | 194 | const transition_1 = [_]TransitionConfig{.{ 195 | .image = self.image, 196 | .old_layout = self.layout, 197 | .new_layout = .transfer_src_optimal, 198 | }}; 199 | try transitionImageLayouts(ctx, command_pool, &transition_1); 200 | const command_buffer = try vk_utils.beginOneTimeCommandBuffer(ctx, command_pool); 201 | { 202 | const region = vk.BufferImageCopy{ 203 | .buffer_offset = 0, 204 | .buffer_row_length = 0, 205 | .buffer_image_height = 0, 206 | .image_subresource = vk.ImageSubresourceLayers{ 207 | .aspect_mask = .{ 208 | .color_bit = true, 209 | }, 210 | .mip_level = 0, 211 | .base_array_layer = 0, 212 | .layer_count = 1, 213 | }, 214 | .image_offset = .{ 215 | .x = 0, 216 | .y = 0, 217 | .z = 0, 218 | }, 219 | .image_extent = .{ 220 | .width = self.image_extent.width, 221 | .height = self.image_extent.height, 222 | .depth = 1, 223 | }, 224 | }; 225 | ctx.vkd.cmdCopyImageToBuffer(command_buffer, self.image, .transfer_src_optimal, staging_buffer.buffer, 1, @ptrCast(®ion)); 226 | } 227 | try vk_utils.endOneTimeCommandBuffer(ctx, command_pool, command_buffer); 228 | 229 | const transition_2 = [_]TransitionConfig{.{ 230 | .image = self.image, 231 | .old_layout = .transfer_src_optimal, 232 | .new_layout = self.layout, 233 | }}; 234 | try transitionImageLayouts(ctx, command_pool, &transition_2); 235 | 236 | try staging_buffer.transferFromDevice(ctx, T, buffer); 237 | } 238 | 239 | const TransitionBits = struct { 240 | src_mask: vk.AccessFlags, 241 | dst_mask: vk.AccessFlags, 242 | src_stage: vk.PipelineStageFlags, 243 | dst_stage: vk.PipelineStageFlags, 244 | }; 245 | pub fn getTransitionBits(old_layout: vk.ImageLayout, new_layout: vk.ImageLayout) TransitionBits { 246 | var transition_bits: TransitionBits = undefined; 247 | switch (old_layout) { 248 | .undefined => { 249 | transition_bits.src_mask = .{}; 250 | transition_bits.src_stage = .{ 251 | .top_of_pipe_bit = true, 252 | }; 253 | }, 254 | .general => { 255 | transition_bits.src_mask = .{ 256 | .shader_read_bit = true, 257 | .shader_write_bit = true, 258 | }; 259 | transition_bits.src_stage = .{ 260 | .compute_shader_bit = true, 261 | }; 262 | }, 263 | .shader_read_only_optimal => { 264 | transition_bits.src_mask = .{ 265 | .shader_read_bit = true, 266 | }; 267 | transition_bits.src_stage = .{ 268 | .fragment_shader_bit = true, 269 | }; 270 | }, 271 | .transfer_dst_optimal => { 272 | transition_bits.src_mask = .{ 273 | .transfer_write_bit = true, 274 | }; 275 | transition_bits.src_stage = .{ 276 | .transfer_bit = true, 277 | }; 278 | }, 279 | .transfer_src_optimal => { 280 | transition_bits.src_mask = .{ 281 | .transfer_read_bit = true, 282 | }; 283 | transition_bits.src_stage = .{ 284 | .transfer_bit = true, 285 | }; 286 | }, 287 | else => { 288 | // TODO return error 289 | std.debug.panic("illegal old layout", .{}); 290 | }, 291 | } 292 | switch (new_layout) { 293 | .undefined => { 294 | transition_bits.dst_mask = .{}; 295 | transition_bits.dst_stage = .{ 296 | .top_of_pipe_bit = true, 297 | }; 298 | }, 299 | .present_src_khr => { 300 | transition_bits.dst_mask = .{}; 301 | transition_bits.dst_stage = .{ 302 | .fragment_shader_bit = true, 303 | }; 304 | }, 305 | .general => { 306 | transition_bits.dst_mask = .{ 307 | .shader_read_bit = true, 308 | }; 309 | transition_bits.dst_stage = .{ 310 | .fragment_shader_bit = true, 311 | }; 312 | }, 313 | .shader_read_only_optimal => { 314 | transition_bits.dst_mask = .{ 315 | .shader_read_bit = true, 316 | }; 317 | transition_bits.dst_stage = .{ 318 | .fragment_shader_bit = true, 319 | }; 320 | }, 321 | .transfer_dst_optimal => { 322 | transition_bits.dst_mask = .{ 323 | .transfer_write_bit = true, 324 | }; 325 | transition_bits.dst_stage = .{ 326 | .transfer_bit = true, 327 | }; 328 | }, 329 | .transfer_src_optimal => { 330 | transition_bits.dst_mask = .{ 331 | .transfer_read_bit = true, 332 | }; 333 | transition_bits.dst_stage = .{ 334 | .transfer_bit = true, 335 | }; 336 | }, 337 | else => { 338 | // TODO return error 339 | std.debug.panic("illegal new layout", .{}); 340 | }, 341 | } 342 | return transition_bits; 343 | } 344 | 345 | pub const TransitionConfig = struct { 346 | image: vk.Image, 347 | old_layout: vk.ImageLayout, 348 | new_layout: vk.ImageLayout, 349 | src_queue_family_index: u32 = vk.QUEUE_FAMILY_IGNORED, 350 | dst_queue_family_index: u32 = vk.QUEUE_FAMILY_IGNORED, 351 | }; 352 | pub inline fn transitionImageLayouts( 353 | ctx: Context, 354 | command_pool: vk.CommandPool, 355 | configs: []const TransitionConfig, 356 | ) !void { 357 | const commmand_buffer = try vk_utils.beginOneTimeCommandBuffer(ctx, command_pool); 358 | 359 | for (configs) |config| { 360 | const transition = getTransitionBits(config.old_layout, config.new_layout); 361 | const barrier = vk.ImageMemoryBarrier{ 362 | .src_access_mask = transition.src_mask, 363 | .dst_access_mask = transition.dst_mask, 364 | .old_layout = config.old_layout, 365 | .new_layout = config.new_layout, 366 | .src_queue_family_index = config.src_queue_family_index, 367 | .dst_queue_family_index = config.dst_queue_family_index, 368 | .image = config.image, 369 | .subresource_range = vk.ImageSubresourceRange{ 370 | .aspect_mask = .{ 371 | .color_bit = true, 372 | }, 373 | .base_mip_level = 0, 374 | .level_count = 1, 375 | .base_array_layer = 0, 376 | .layer_count = 1, 377 | }, 378 | }; 379 | ctx.vkd.cmdPipelineBarrier(commmand_buffer, transition.src_stage, transition.dst_stage, vk.DependencyFlags{}, 0, undefined, 0, undefined, 1, @ptrCast(&barrier)); 380 | } 381 | try vk_utils.endOneTimeCommandBuffer(ctx, command_pool, commmand_buffer); 382 | } 383 | 384 | pub fn copyBufferToImage(ctx: Context, command_pool: vk.CommandPool, image: vk.Image, buffer: vk.Buffer, image_extent: vk.Extent2D) !void { 385 | const command_buffer = try vk_utils.beginOneTimeCommandBuffer(ctx, command_pool); 386 | { 387 | const region = vk.BufferImageCopy{ 388 | .buffer_offset = 0, 389 | .buffer_row_length = 0, 390 | .buffer_image_height = 0, 391 | .image_subresource = vk.ImageSubresourceLayers{ 392 | .aspect_mask = .{ 393 | .color_bit = true, 394 | }, 395 | .mip_level = 0, 396 | .base_array_layer = 0, 397 | .layer_count = 1, 398 | }, 399 | .image_offset = .{ 400 | .x = 0, 401 | .y = 0, 402 | .z = 0, 403 | }, 404 | .image_extent = .{ 405 | .width = image_extent.width, 406 | .height = image_extent.height, 407 | .depth = 1, 408 | }, 409 | }; 410 | ctx.vkd.cmdCopyBufferToImage(command_buffer, buffer, image, .transfer_dst_optimal, 1, @ptrCast(®ion)); 411 | } 412 | try vk_utils.endOneTimeCommandBuffer(ctx, command_pool, command_buffer); 413 | } 414 | --------------------------------------------------------------------------------