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:
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,
+ };
+ }
+};