zgpu

git clone git://git.electrosoup.com/zgpu
Log | Files | Refs | Submodules | README

commit e0dffa92a68c08dda96c7b8974f8b4b6f0c746a1
parent f3728633b9437471650816d0620fd71f8dcb2fb6
Author: Christian Ermann <christian.ermann@joescan.com>
Date:   Tue, 15 Jul 2025 19:25:57 -0700

Add shader sandbox to develop more complex examples

Diffstat:
Mbuild.zig | 32++++++++++++++++++++++++++------
Msrc/math.zig | 4++--
Asrc/shader-sandbox/main.zig | 250+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/shader-sandbox/render_pipeline.zig | 229+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 507 insertions(+), 8 deletions(-)

diff --git a/build.zig b/build.zig @@ -27,27 +27,31 @@ pub fn build(b: *std.Build) void { glfw.linkFramework("Metal", .{}); } + const math = b.addModule("math", .{ + .root_source_file = b.path("src/math.zig"), + }); + const wgpu = b.addModule("wgpu-bindings", .{ .root_source_file = b.path("src/wgpu-bindings/root.zig"), }); wgpu.addIncludePath(b.path("include")); wgpu.addObjectFile(b.path("lib/libwgpu_native.a")); - const exe = b.addExecutable(.{ + const wgpu_bindings_exe = b.addExecutable(.{ .name = "wgpu-bindings-example", .root_source_file = b.path("src/wgpu-bindings/main.zig"), .target = target, .optimize = optimize, }); - exe.root_module.addImport("glfw", glfw); - exe.root_module.addImport("wgpu", wgpu); - exe.linkLibCpp(); - b.installArtifact(exe); + wgpu_bindings_exe.root_module.addImport("glfw", glfw); + wgpu_bindings_exe.root_module.addImport("wgpu", wgpu); + wgpu_bindings_exe.linkLibCpp(); + b.installArtifact(wgpu_bindings_exe); // This *creates* a Run step in the build graph, to be executed when another // step is evaluated that depends on it. The next line below will establish // such a dependency. - const run_cmd = b.addRunArtifact(exe); + const run_cmd = b.addRunArtifact(wgpu_bindings_exe); // By making the run step depend on the install step, it will be run from the // installation directory rather than directly from within the cache directory. @@ -67,6 +71,22 @@ pub fn build(b: *std.Build) void { const run_step = b.step("run", "Run the app"); run_step.dependOn(&run_cmd.step); + const sandbox_exe = b.addExecutable(.{ + .name = "shader-sandbox", + .root_source_file = b.path("src/shader-sandbox/main.zig"), + .target = target, + .optimize = optimize, + }); + sandbox_exe.root_module.addImport("glfw", glfw); + sandbox_exe.root_module.addImport("wgpu", wgpu); + sandbox_exe.root_module.addImport("math", math); + sandbox_exe.linkLibCpp(); + b.installArtifact(sandbox_exe); + const run_sandbox_cmd = b.addRunArtifact(sandbox_exe); + run_sandbox_cmd.step.dependOn(b.getInstallStep()); + const run_sandbox_step = b.step("sandbox", "Run shader sandbox"); + run_sandbox_step.dependOn(&run_sandbox_cmd.step); + const test_step = b.step("test", "Run unit tests"); const tests = b.addTest(.{ .root_source_file = b.path("src/math.zig"), diff --git a/src/math.zig b/src/math.zig @@ -1,7 +1,7 @@ const std = @import("std"); -const Vec4 = @Vector(4, f32); -const Mat4 = [4]Vec4; +pub const Vec4 = @Vector(4, f32); +pub const Mat4 = [4]Vec4; pub fn dot(u: Vec4, v: Vec4) f32 { return @reduce(.Add, u * v); diff --git a/src/shader-sandbox/main.zig b/src/shader-sandbox/main.zig @@ -0,0 +1,250 @@ +const builtin = @import("builtin"); +const std = @import("std"); + +const glfw = @import("glfw"); +const wgpu = @import("wgpu"); + +const Callback1 = wgpu.Callback1; +const DeviceCallback = wgpu.DeviceCallback; +const CommandBuffer = wgpu.CommandBuffer; +const Device = wgpu.Device; +const Instance = wgpu.Instance; +const StringView = wgpu.StringView; + +const RenderPipeline = @import("render_pipeline.zig").RenderPipeline; +const TriangleRenderPipeline = @import("render_pipeline.zig").TriangleRenderPipeline; + +fn zgpu_log_callback(level: wgpu.LogLevel, message: []const u8) void { + switch (level) { + .@"error" => { + std.log.err("[wgpu] {s}", .{message}); + }, + .warn => { + std.log.warn("[wgpu] {s}", .{message}); + }, + .info => { + std.log.debug("[wgpu] {s}", .{message}); + }, + .debug, .trace => { + std.log.debug("[wgpu] {s}", .{message}); + }, + .off => unreachable, + } +} + +fn zgpu_error_callback( + error_type: wgpu.ErrorType, + message: []const u8, +) void { + std.log.err("[wgpu - {}] {s}", .{ error_type, message }); +} + +fn zgpu_device_lost_callback( + reason: wgpu.DeviceLostReason, + message: []const u8, +) void { + std.log.err("[wgpu - {}] {s}", .{ reason, message }); +} + +pub fn main() !void { + try glfw.init(); + defer glfw.terminate(); + + glfw.windowHint(.client_api, glfw.ClientApi.no_api); + const window = try glfw.Window.create(640, 480, "Example", null, null); + defer window.destroy(); + + wgpu.setLogCallback(Callback1(zgpu_log_callback)); + wgpu.setLogLevel(.warn); + + const desc = Instance.Descriptor{ + .next = .{ + .extras = &.{ + .backends = .primary, + .flags = .validation, + .dx12_compiler = .undefined, + .gles3_minor_version = .automatic, + .gl_fence_behavior = .normal, + .dxil_path = null, + .dxc_path = null, + .dxc_max_shader_model = .dxc_max_shader_model_v6_0, + }, + }, + .features = .{ + .timed_wait_any_enable = false, + .timed_wait_any_max_count = 0, + }, + }; + + const instance = Instance.create(&desc) orelse { + std.log.err("failed to create GPU instance", .{}); + std.process.exit(1); + }; + defer instance.release(); + + const surface = switch (builtin.target.os.tag) { + .linux => instance.createSurface(&.{ + .next = .{ + .from_xlib_window = &.{ + .window = try glfw.native.getX11Window(window)(), + .display = try glfw.native.getX11Display(), + }, + }, + .label = StringView.fromSlice("Example Surface"), + }), + .macos => instance.createSurface(&.{ + .next = .{ + .from_metal_layer = &.{ + .layer = glfw.native.getMetalLayer(window), + }, + }, + .label = StringView.fromSlice("Example Surface"), + }), + else => @compileError("Unsupported OS"), + } orelse { + std.log.err("failed to create GPU surface", .{}); + std.process.exit(1); + }; + defer surface.release(); + + const adapter = switch (builtin.target.os.tag) { + .linux => try instance.requestAdapter(&.{ + .feature_level = .core, + .power_preference = .high_performance, + .backend_type = .vulkan, + .force_fallback_adapter = false, + .compatible_surface = surface, + }), + .macos => try instance.requestAdapter(&.{ + .feature_level = .core, + .power_preference = .high_performance, + .backend_type = .metal, + .force_fallback_adapter = false, + // the adapter seems to be compatible, but when I request that it + // is it can't find one + .compatible_surface = null, + }), + else => @compileError("Unsupported OS"), + }; + defer adapter.release(); + + const device = try adapter.requestDevice(&.{ + .label = StringView.fromSlice("Example Device"), + .required_feature_count = 0, + .required_features = null, + .required_limits = null, + .default_queue = .{ + .label = StringView.fromSlice("Example Default Queue"), + }, + .device_lost_callback_info = .{ + .callback = DeviceCallback(zgpu_device_lost_callback), + .mode = .allow_spontaneous, + .userdata1 = null, + .userdata2 = null, + }, + .uncaptured_error_callback_info = .{ + .callback = DeviceCallback(zgpu_error_callback), + .userdata1 = null, + .userdata2 = null, + }, + }); + defer device.release(); + + const queue = device.getQueue(); + defer queue.release(); + + surface.configure(&.{ + .device = device, + .format = .bgra8_unorm, + .usage = .{ .render_attachment = true }, + .width = 640, + .height = 480, + .view_format_count = 0, + .view_formats = null, + .alpha_mode = .auto, + .present_mode = .fifo, + }); + + var triangle_rp = try TriangleRenderPipeline.init(device); + const uniform_data = TriangleRenderPipeline.UniformData{ + .view_matrix = .{ + .{0.5, 0, 0, 0}, + .{ 0, 0.5, 0, 0}, + .{ 0, 0, 1, 0}, + .{ 0, 0, 0, 1}, + }, + .proj_matrix = .{ + .{1, 0, 0, 0}, + .{0, 1, 0, 0}, + .{0, 0, 1, 0}, + .{0, 0, 0, 1}, + }, + }; + + triangle_rp.setUniform(queue, &uniform_data); + + const pipelines = [_]RenderPipeline{ + triangle_rp.renderPipeline(), + }; + + while (!window.shouldClose()) { + glfw.pollEvents(); + const current_texture = surface.getCurrentTexture(); + const texture = current_texture.texture; + defer texture.release(); + const view = texture.createView(&.{ + .label = StringView.fromSlice("Example View"), + .format = .bgra8_unorm, + .dimension = .@"2d", + .base_mip_level = 0, + .mip_level_count = 1, + .base_array_layer = 0, + .array_layer_count = 1, + .aspect = .all, + .usage = .{ .render_attachment = true }, + }).?; + defer view.release(); + { + const encoder = device.createCommandEncoder(&.{ + .label = StringView.fromSlice("Example Encoder"), + }).?; + defer encoder.release(); + { + const pass = encoder.beginRenderPass(&.{ + .label = StringView.fromSlice("Example Render Pass"), + .color_attachment_count = 1, + .color_attachments = &.{ + .{ + .view = view, + .resolve_target = null, + .load_op = .clear, + .store_op = .store, + .clear_value = .{ .r = 1, .g = 1, .b = 1, .a = 1 }, + }, + }, + .depth_stencil_attachment = null, + .occlusion_query_set = null, + .timestamp_writes = null, + }); + + for (pipelines) |pipeline| { + pipeline.frame(pass); + } + + defer pass.release(); + defer pass.end(); + } + { + var command = encoder.finish(&.{ + .label = StringView.fromSlice("Example Command Buffer"), + }).?; + defer command.release(); + queue.submit(&[_]*CommandBuffer{command}); + } + } + switch (surface.present()) { + .success => {}, + .@"error" => std.log.err("surface presentation failed", .{}), + } + } +} diff --git a/src/shader-sandbox/render_pipeline.zig b/src/shader-sandbox/render_pipeline.zig @@ -0,0 +1,229 @@ +const std = @import("std"); + +const wgpu = @import("wgpu"); +const math = @import("math"); + +pub const RenderPipeline = struct { + ptr: *anyopaque, + frameFn: *const fn ( + ptr: *anyopaque, + render_pass_encoder: *wgpu.RenderPassEncoder, + ) void, + + pub fn frame( + self: *const RenderPipeline, + render_pass_encoder: *wgpu.RenderPassEncoder, + ) void { + return self.frameFn(self.ptr, render_pass_encoder); + } +}; + +pub const TriangleRenderPipeline = struct { + render_pipeline: *wgpu.RenderPipeline, + bind_group: *wgpu.BindGroup, + uniform_buffer: *wgpu.Buffer, + + pub const UniformData = extern struct { + view_matrix: math.Mat4, + proj_matrix: math.Mat4, + }; + + pub fn init(device: *wgpu.Device) !TriangleRenderPipeline { + const vs = + \\ @group(0) @binding(0) + \\ var<uniform> uniform_data: UniformData; + \\ struct UniformData { + \\ view: mat4x4<f32>, + \\ proj: mat4x4<f32>, + \\ }; + \\ + \\ @vertex fn main( + \\ @builtin(vertex_index) vtx_idx : u32 + \\ ) -> @builtin(position) vec4<f32> { + \\ let camera_matrix = uniform_data.proj * uniform_data.view; + \\ let x = f32(i32(vtx_idx) - 1); + \\ let y = f32(i32(vtx_idx & 1u) * 2 - 1); + \\ let position_ws = vec4<f32>(x, y, 0.0, 1.0); + \\ let mat = mat4x4<f32>( 1, 0, 0, 0, + \\ 0, 1, 0, 0, + \\ 0, 0, 1, 0, + \\ 0, 0, 0, 1 ); + \\ let position_cs = camera_matrix * position_ws; + \\ return camera_matrix * position_ws; + \\ } + ; + const vs_module = device.createShaderModule(&.{ + .next = .{ .wgsl = &.{ .code = wgpu.StringView.fromSlice(vs) } }, + .label = wgpu.StringView.fromSlice("Triangle Vertex Shader"), + }) orelse { + return error.VertexShader; + }; + defer vs_module.release(); + + const fs = + \\ @fragment fn main() -> @location(0) vec4<f32> { + \\ return vec4<f32>(1.0, 0.0, 0.0, 1.0); + \\ } + ; + const fs_module = device.createShaderModule(&.{ + .next = .{ .wgsl = &.{ .code = wgpu.StringView.fromSlice(fs) } }, + .label = wgpu.StringView.fromSlice("Triangle Fragment Shader"), + }) orelse { + return error.FragmentShader; + }; + defer fs_module.release(); + + const uniform_buffer = device.createBuffer(&.{ + .label = wgpu.StringView.fromSlice("Triangle Uniform Buffer"), + .usage = .{ .uniform = true, .copy_dst = true }, + .size = @sizeOf(UniformData), + .mapped_at_creation = false, + }).?; + + const bind_group_layout_entries = &[_]wgpu.BindGroupLayout.Entry{ + .{ + .binding = 0, + .visibility = .{ .vertex = true }, + .buffer = .{ + .type = .uniform, + .has_dynamic_offset = false, + .min_binding_size = 0, + }, + .sampler = .{ + .type = .binding_not_used, + }, + .texture = .{ + .sample_type = .binding_not_used, + .view_dimension = .undefined, + .multisampled = false, + }, + .storage_texture = .{ + .access = .binding_not_used, + .format = .undefined, + .view_dimension = .undefined, + }, + }, + }; + const bind_group_layout = device.createBindGroupLayout(&.{ + .label = wgpu.StringView.fromSlice("Triangle Bind Group Layout"), + .entry_count = bind_group_layout_entries.len, + .entries = bind_group_layout_entries.ptr, + }).?; + + const bind_group_entries = &[_]wgpu.BindGroup.Entry{ + .{ + .binding = 0, + .buffer = uniform_buffer, + .offset = 0, + .size = @sizeOf(UniformData), + .sampler = null, + .texture_view = null, + }, + }; + const bind_group = device.createBindGroup(&.{ + .label = wgpu.StringView.fromSlice("Triangle Pipeline Bind Group"), + .layout = bind_group_layout, + .entry_count = bind_group_entries.len, + .entries = bind_group_entries.ptr, + }).?; + + const bind_group_layouts = &[_]*wgpu.BindGroupLayout{ + bind_group_layout, + }; + const pipeline_layout = device.createPipelineLayout(&.{ + .label = wgpu.StringView.fromSlice("Triangle Pipeline Layout"), + .bind_group_layout_count = bind_group_layouts.len, + .bind_group_layouts = bind_group_layouts.ptr, + }); + + const pipeline = device.createRenderPipeline(&.{ + .label = wgpu.StringView.fromSlice("Triangle Render Pipeline"), + .layout = pipeline_layout, + .vertex = .{ + .module = vs_module, + .entry_point = wgpu.StringView.fromSlice("main"), + .constant_count = 0, + .constants = null, + .buffer_count = 0, + .buffers = null, + }, + .primitive = .{ + .topology = .triangle_list, + .strip_index_format = .undefined, + .front_face = .ccw, + .cull_mode = .none, + .unclipped_depth = false, + }, + .depth_stencil = null, + .multisample = .{ + .count = 1, + .mask = 0xFFFFFFFF, + .alpha_to_coverage_enabled = false, + }, + .fragment_state = &.{ + .module = fs_module, + .entry_point = wgpu.StringView.fromSlice("main"), + .constant_count = 0, + .constants = null, + .target_count = 1, + .targets = &.{ + .{ + .format = .bgra8_unorm, + .blend = &.{ + .color = .{ + .operation = .add, + .src_factor = .one, + .dst_factor = .zero, + }, + .alpha = .{ + .operation = .add, + .src_factor = .one, + .dst_factor = .zero, + }, + }, + .write_mask = .{ + .red = true, + .green = true, + .blue = true, + .alpha = true, + }, + }, + }, + }, + }) orelse { + return error.RenderPipeline; + }; + + return .{ + .render_pipeline = pipeline, + .bind_group = bind_group, + .uniform_buffer = uniform_buffer, + }; + } + + pub fn setUniform( + self: *TriangleRenderPipeline, + queue: *wgpu.Queue, + uniform_data: *const UniformData, + ) void { + const bytes = std.mem.asBytes(uniform_data); + queue.writeBuffer(self.uniform_buffer, 0, bytes.ptr, bytes.len); + } + + pub fn frame( + ptr: *anyopaque, + render_pass_encoder: *wgpu.RenderPassEncoder, + ) void { + const self: *TriangleRenderPipeline = @ptrCast(@alignCast(ptr)); + render_pass_encoder.setPipeline(self.render_pipeline); + render_pass_encoder.setBindGroup(0, self.bind_group, 0, null); + render_pass_encoder.draw(3, 1, 0, 0); + } + + pub fn renderPipeline(self: *TriangleRenderPipeline) RenderPipeline { + return .{ + .ptr = self, + .frameFn = frame, + }; + } +};