diff --git a/README.md b/README.md index f5caa71..6d9a6a0 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,10 @@ pub fn build(b: *std.Build) void { const zgpu = b.dependency("zgpu", .{}); exe.root_module.addImport("zgpu", zgpu.module("root")); - exe.linkLibrary(zgpu.artifact("zdawn")); + + if (target.result.os.tag != .emscripten) { + exe.linkLibrary(zgpu.artifact("zdawn")); + } } ``` diff --git a/src/wgpu.zig b/src/wgpu.zig index d615e50..7bf50e2 100644 --- a/src/wgpu.zig +++ b/src/wgpu.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const emscripten = @import("builtin").target.os.tag == .emscripten; test "extern struct ABI compatibility" { @setEvalBranchQuota(10_000); @@ -66,28 +67,56 @@ pub const BackendType = enum(u32) { opengles, }; -pub const BlendFactor = enum(u32) { - zero = 0x00000000, - one = 0x00000001, - src = 0x00000002, - one_minus_src = 0x00000003, - src_alpha = 0x00000004, - one_minus_src_alpha = 0x00000005, - dst = 0x00000006, - one_minus_dst = 0x00000007, - dst_alpha = 0x00000008, - one_minus_dst_alpha = 0x00000009, - src_alpha_saturated = 0x0000000A, - constant = 0x0000000B, - one_minus_constant = 0x0000000C, -}; - -pub const BlendOperation = enum(u32) { - add = 0x00000000, - subtract = 0x00000001, - reverse_subtract = 0x00000002, - min = 0x00000003, - max = 0x00000004, +pub const BlendFactor = switch (emscripten) { + true => enum(u32) { + undef = 0x00000000, + zero = 0x00000001, + one = 0x00000002, + src = 0x00000003, + one_minus_src = 0x00000004, + src_alpha = 0x00000005, + one_minus_src_alpha = 0x00000006, + dst = 0x00000007, + one_minus_dst = 0x00000008, + dst_alpha = 0x00000009, + one_minus_dst_alpha = 0x0000000A, + src_alpha_saturated = 0x0000000B, + constant = 0x0000000C, + one_minus_constant = 0x0000000D, + }, + false => enum(u32) { + zero = 0x00000000, + one = 0x00000001, + src = 0x00000002, + one_minus_src = 0x00000003, + src_alpha = 0x00000004, + one_minus_src_alpha = 0x00000005, + dst = 0x00000006, + one_minus_dst = 0x00000007, + dst_alpha = 0x00000008, + one_minus_dst_alpha = 0x00000009, + src_alpha_saturated = 0x0000000A, + constant = 0x0000000B, + one_minus_constant = 0x0000000C, + }, +}; + +pub const BlendOperation = switch (emscripten) { + true => enum(u32) { + undef = 0x00000000, + add = 0x00000001, + subtract = 0x00000002, + reverse_subtract = 0x00000003, + min = 0x00000008, + max = 0x00000004, + }, + false => enum(u32) { + add = 0x00000000, + subtract = 0x00000001, + reverse_subtract = 0x00000002, + min = 0x00000003, + max = 0x00000004, + }, }; pub const BufferBindingType = enum(u32) { @@ -109,10 +138,17 @@ pub const BufferMapAsyncStatus = enum(u32) { size_out_of_range = 0x00000008, }; -pub const BufferMapState = enum(u32) { - unmapped = 0x00000000, - pending = 0x00000001, - mapped = 0x00000002, +pub const BufferMapState = switch (emscripten) { + true => enum(u32) { + unmapped = 0x00000001, + pending = 0x00000002, + mapped = 0x00000003, + }, + false => enum(u32) { + unmapped = 0x00000000, + pending = 0x00000001, + mapped = 0x00000002, + }, }; pub const CompareFunction = enum(u32) { @@ -262,18 +298,35 @@ pub const PowerPreference = enum(u32) { high_performance = 0x00000002, }; -pub const PresentMode = enum(u32) { - immediate = 0x00000000, - mailbox = 0x00000001, - fifo = 0x00000002, -}; - -pub const PrimitiveTopology = enum(u32) { - point_list = 0x00000000, - line_list = 0x00000001, - line_strip = 0x00000002, - triangle_list = 0x00000003, - triangle_strip = 0x00000004, +pub const PresentMode = switch (emscripten) { + true => enum(u32) { + fifo = 0x00000001, + immediate = 0x00000003, + mailbox = 0x00000004, + }, + false => enum(u32) { + immediate = 0x00000000, + mailbox = 0x00000001, + fifo = 0x00000002, + }, +}; + +pub const PrimitiveTopology = switch (emscripten) { + true => enum(u32) { + undefined = 0x00000000, + point_list = 0x00000001, + line_list = 0x00000002, + line_strip = 0x00000003, + triangle_list = 0x00000004, + triangle_strip = 0x00000005, + }, + false => enum(u32) { + point_list = 0x00000000, + line_list = 0x00000001, + line_strip = 0x00000002, + triangle_list = 0x00000003, + triangle_strip = 0x00000004, + }, }; pub const QueryType = enum(u32) { @@ -410,10 +463,18 @@ pub const TextureAspect = enum(u32) { plane1_only = 0x00000004, }; -pub const TextureDimension = enum(u32) { - tdim_1d = 0x00000000, - tdim_2d = 0x00000001, - tdim_3d = 0x00000002, +pub const TextureDimension = switch (emscripten) { + true => enum(u32) { + undef = 0x00000000, + tdim_1d = 0x00000001, + tdim_2d = 0x00000002, + tdim_3d = 0x00000003, + }, + false => enum(u32) { + tdim_1d = 0x00000000, + tdim_2d = 0x00000001, + tdim_3d = 0x00000002, + }, }; pub const TextureFormat = enum(u32) { @@ -568,10 +629,18 @@ pub const VertexFormat = enum(u32) { sint32x4 = 0x0000001E, }; -pub const VertexStepMode = enum(u32) { - vertex = 0x00000000, - instance = 0x00000001, - vertex_buffer_not_used = 0x00000002, +pub const VertexStepMode = switch (emscripten) { + true => enum(u32) { + undefined = 0x00000000, + vertex_buffer_not_used = 0x00000001, + vertex = 0x00000002, + instance = 0x00000003, + }, + false => enum(u32) { + vertex = 0x00000000, + instance = 0x00000001, + vertex_buffer_not_used = 0x00000002, + }, }; pub const BufferUsage = packed struct(u32) { @@ -665,7 +734,7 @@ pub const BindGroupDescriptor = extern struct { pub const BufferBindingLayout = extern struct { next_in_chain: ?*const ChainedStruct = null, binding_type: BufferBindingType = .uniform, - has_dynamic_offset: bool = false, + has_dynamic_offset: U32Bool = .false, min_binding_size: u64 = 0, }; @@ -705,12 +774,17 @@ pub const BindGroupLayoutDescriptor = extern struct { entries: ?[*]const BindGroupLayoutEntry, }; +pub const U32Bool = enum(u32) { + false = 0, + true = 1, +}; + pub const BufferDescriptor = extern struct { next_in_chain: ?*const ChainedStruct = null, label: ?[*:0]const u8 = null, usage: BufferUsage, size: u64, - mapped_at_creation: bool = false, + mapped_at_creation: U32Bool = .false, }; pub const CommandEncoderDescriptor = extern struct { @@ -1059,13 +1133,24 @@ pub const Color = extern struct { a: f64, }; -pub const RenderPassColorAttachment = extern struct { - next_in_chain: ?*const ChainedStruct = null, - view: ?TextureView, - resolve_target: ?TextureView = null, - load_op: LoadOp, - store_op: StoreOp, - clear_value: Color = .{ .r = 0.0, .g = 0.0, .b = 0.0, .a = 0.0 }, +pub const RenderPassColorAttachment = switch (emscripten) { + true => extern struct { + next_in_chain: ?*const ChainedStruct = null, + view: ?TextureView, + depth_slice: u32 = std.math.maxInt(u32), + resolve_target: ?TextureView = null, + load_op: LoadOp, + store_op: StoreOp, + clear_value: Color = .{ .r = 0.0, .g = 0.0, .b = 0.0, .a = 0.0 }, + }, + false => extern struct { + next_in_chain: ?*const ChainedStruct = null, + view: ?TextureView, + resolve_target: ?TextureView = null, + load_op: LoadOp, + store_op: StoreOp, + clear_value: Color = .{ .r = 0.0, .g = 0.0, .b = 0.0, .a = 0.0 }, + }, }; pub const RenderPassDepthStencilAttachment = extern struct { @@ -1073,11 +1158,11 @@ pub const RenderPassDepthStencilAttachment = extern struct { depth_load_op: LoadOp = .undef, depth_store_op: StoreOp = .undef, depth_clear_value: f32 = 0.0, - depth_read_only: bool = false, + depth_read_only: U32Bool = .false, stencil_load_op: LoadOp = .undef, stencil_store_op: StoreOp = .undef, stencil_clear_value: u32 = 0, - stencil_read_only: bool = false, + stencil_read_only: U32Bool = .false, }; pub const RenderPassDescriptor = extern struct { @@ -1467,10 +1552,10 @@ pub const CommandEncoder = *opaque { pub fn copyBufferToBuffer( command_encoder: CommandEncoder, source: Buffer, - source_offset: usize, + source_offset: u64, destination: Buffer, - destination_offset: usize, - size: usize, + destination_offset: u64, + size: u64, ) void { wgpuCommandEncoderCopyBufferToBuffer( command_encoder, @@ -1484,10 +1569,10 @@ pub const CommandEncoder = *opaque { extern fn wgpuCommandEncoderCopyBufferToBuffer( command_encoder: CommandEncoder, source: Buffer, - source_offset: usize, + source_offset: u64, destination: Buffer, - destination_offset: usize, - size: usize, + destination_offset: u64, + size: u64, ) void; pub fn copyBufferToTexture( @@ -2147,15 +2232,29 @@ pub const Queue = *opaque { callback: QueueWorkDoneCallback, userdata: ?*anyopaque, ) void { - wgpuQueueOnSubmittedWorkDone(queue, signal_value, callback, userdata); + if (emscripten) { + const oswd = @extern( + *const fn ( + queue: Queue, + callback: QueueWorkDoneCallback, + userdata: ?*anyopaque, + ) callconv(.C) void, + .{ .name = "wgpuQueueOnSubmittedWorkDone" }, + ); + oswd(queue, callback, userdata); + } else { + const oswd = @extern( + *const fn ( + queue: Queue, + signal_value: u64, + callback: QueueWorkDoneCallback, + userdata: ?*anyopaque, + ) callconv(.C) void, + .{ .name = "wgpuQueueOnSubmittedWorkDone" }, + ); + oswd(queue, signal_value, callback, userdata); + } } - extern fn wgpuQueueOnSubmittedWorkDone( - queue: Queue, - signal_value: u64, - callback: QueueWorkDoneCallback, - userdata: ?*anyopaque, - ) void; - pub fn setLabel(queue: Queue, label: ?[*:0]const u8) void { wgpuQueueSetLabel(queue, label); } @@ -2169,16 +2268,16 @@ pub const Queue = *opaque { pub fn writeBuffer( queue: Queue, buffer: Buffer, - buffer_offset: u64, + buffer_offset: usize, comptime T: type, data: []const T, ) void { wgpuQueueWriteBuffer( queue, buffer, - buffer_offset, + @intCast(buffer_offset), @as(*const anyopaque, @ptrCast(data.ptr)), - @as(u64, @intCast(data.len)) * @sizeOf(T), + data.len * @sizeOf(T), ); } extern fn wgpuQueueWriteBuffer( @@ -2186,7 +2285,7 @@ pub const Queue = *opaque { buffer: Buffer, buffer_offset: u64, data: *const anyopaque, - size: u64, + size: usize, ) void; pub fn writeTexture( @@ -2210,7 +2309,7 @@ pub const Queue = *opaque { queue: Queue, destination: *const ImageCopyTexture, data: *const anyopaque, - data_size: u64, + data_size: usize, data_layout: *const TextureDataLayout, write_size: *const Extent3D, ) void; @@ -2864,3 +2963,12 @@ pub const TextureView = *opaque { } extern fn wgpuTextureViewRelease(texture_view: TextureView) void; }; + +pub const InstanceDescriptor = extern struct { + next_in_chain: ?*const ChainedStruct = null, +}; +pub inline fn createInstance(desc: InstanceDescriptor) Instance { + _ = desc; + return wgpuCreateInstance(null); +} +extern fn wgpuCreateInstance(desc: ?*const InstanceDescriptor) Instance; diff --git a/src/zgpu.zig b/src/zgpu.zig index 526be6c..e735371 100644 --- a/src/zgpu.zig +++ b/src/zgpu.zig @@ -11,6 +11,8 @@ const assert = std.debug.assert; const wgsl = @import("common_wgsl.zig"); const zgpu_options = @import("zgpu_options"); pub const wgpu = @import("wgpu.zig"); +pub const slog = std.log.scoped(.zgpu); // scoped log that can be comptime processed in main logger +const emscripten = @import("builtin").target.os.tag == .emscripten; test { _ = wgpu; @@ -117,12 +119,12 @@ pub const GraphicsContext = struct { window_provider: WindowProvider, options: GraphicsContextOptions, ) !*GraphicsContext { - dawnProcSetProcs(dnGetProcs()); + if (!emscripten) dawnProcSetProcs(dnGetProcs()); - const native_instance = dniCreate(); - errdefer dniDestroy(native_instance); + const native_instance = if (!emscripten) dniCreate(); + errdefer if (!emscripten) dniDestroy(native_instance); - const instance = dniGetWgpuInstance(native_instance).?; + const instance = if (emscripten) wgpu.createInstance(.{}) else dniGetWgpuInstance(native_instance).?; const adapter = adapter: { const Response = struct { @@ -151,6 +153,14 @@ pub const GraphicsContext = struct { @ptrCast(&response), ); + if (emscripten) { + // wait for response. requires emscripten `-sASYNC` flag + // otherwise whole api would need to be changed in a way that allows whole program to return from main and wait to js to call back + std.log.debug("wait for instance.requestAdapter...", .{}); + while (response.status == .unknown) emscripten_sleep(5); + std.log.debug("{}", .{response.status}); + } + if (response.status != .success) { std.log.err("Failed to request GPU adapter (status: {s}).", .{@tagName(response.status)}); return error.NoGraphicsAdapter; @@ -162,6 +172,13 @@ pub const GraphicsContext = struct { var properties: wgpu.AdapterProperties = undefined; properties.next_in_chain = null; adapter.getProperties(&properties); + + if (emscripten) { + properties.name = "emscripten"; + properties.driver_description = "emscripten"; + properties.adapter_type = .unknown; + properties.backend_type = .undef; + } std.log.info("[zgpu] High-performance device has been selected:", .{}); std.log.info("[zgpu] Name: {s}", .{properties.name}); std.log.info("[zgpu] Driver: {s}", .{properties.driver_description}); @@ -210,6 +227,14 @@ pub const GraphicsContext = struct { @ptrCast(&response), ); + if (emscripten) { + // wait for response. requires emscripten `-sASYNC` flag + // otherwise whole api would need to be changed in a way that allows whole program to return from main and wait to js to call back + std.log.debug("wait for adapter.requestDevice...", .{}); + while (response.status == .unknown) emscripten_sleep(5); + std.log.debug("{}", .{response.status}); + } + if (response.status != .success) { std.log.err("Failed to request GPU device (status: {s}).", .{@tagName(response.status)}); return error.NoGraphicsDevice; @@ -239,7 +264,7 @@ pub const GraphicsContext = struct { const gctx = try allocator.create(GraphicsContext); gctx.* = .{ .window_provider = window_provider, - .native_instance = native_instance, + .native_instance = if (emscripten) null else native_instance, .instance = instance, .device = device, .queue = device.getQueue(), @@ -294,7 +319,7 @@ pub const GraphicsContext = struct { gctx.swapchain.release(); gctx.queue.release(); gctx.device.release(); - dniDestroy(gctx.native_instance); + if (!emscripten) dniDestroy(gctx.native_instance); allocator.destroy(gctx); } @@ -401,7 +426,7 @@ pub const GraphicsContext = struct { const buffer_handle = gctx.createBuffer(.{ .usage = .{ .copy_src = true, .map_write = true }, .size = uniforms_buffer_size, - .mapped_at_creation = true, + .mapped_at_creation = .true, }); // Add new (mapped) staging buffer to the buffer list. @@ -464,7 +489,7 @@ pub const GraphicsContext = struct { normal_execution, swap_chain_resized, } { - gctx.swapchain.present(); + if (!emscripten) gctx.swapchain.present(); const fb_size = gctx.window_provider.getFramebufferSize(); if (gctx.swapchain_descriptor.width != fb_size[0] or @@ -488,6 +513,22 @@ pub const GraphicsContext = struct { return .normal_execution; } + pub fn canRender(gctx: *GraphicsContext) bool { + if (emscripten) { + if (gctx.uniforms.stage.buffers[gctx.uniforms.stage.current].slice == null) { + var i: u32 = 0; + while (i < gctx.uniforms.stage.num) : (i += 1) { + if (gctx.uniforms.stage.buffers[i].slice != null) { + gctx.uniforms.stage.current = i; + return true; + } + } + return false; + } + } + return true; + } + // // Resources // @@ -519,7 +560,7 @@ pub const GraphicsContext = struct { const texture = gctx.lookupResource(texture_handle).?; const info = gctx.lookupResourceInfo(texture_handle).?; var dim = descriptor.dimension; - if (dim == .undef) { + if (!emscripten and dim == .undef) { dim = switch (info.dimension) { .tdim_1d => .tvdim_1d, .tdim_2d => .tvdim_2d, @@ -1074,6 +1115,8 @@ extern fn dnGetProcs() DawnProcsTable; // Defined in Dawn codebase extern fn dawnProcSetProcs(procs: DawnProcsTable) void; +extern fn emscripten_sleep(ms: u32) void; + /// Helper to create a buffer BindGroupLayoutEntry. pub fn bufferEntry( binding: u32, @@ -1087,7 +1130,10 @@ pub fn bufferEntry( .visibility = visibility, .buffer = .{ .binding_type = binding_type, - .has_dynamic_offset = has_dynamic_offset, + .has_dynamic_offset = switch (has_dynamic_offset) { + true => .true, + false => .false, + }, .min_binding_size = min_binding_size, }, }; @@ -1287,7 +1333,7 @@ pub fn imageInfoToTextureFormat(num_components: u32, bytes_per_component: u32, i pub const BufferInfo = struct { gpuobj: ?wgpu.Buffer = null, - size: usize = 0, + size: u64 = 0, usage: wgpu.BufferUsage = .{}, }; @@ -1541,6 +1587,7 @@ const SurfaceDescriptorTag = enum { windows_hwnd, xlib, wayland, + canvas_html, }; const SurfaceDescriptor = union(SurfaceDescriptorTag) { @@ -1563,6 +1610,10 @@ const SurfaceDescriptor = union(SurfaceDescriptorTag) { display: *anyopaque, surface: *anyopaque, }, + canvas_html: struct { + label: ?[*:0]const u8 = null, + selector: [*:0]const u8, + }, }; fn isLinuxDesktopLike(tag: std.Target.Os.Tag) bool { @@ -1608,6 +1659,12 @@ fn createSurfaceForWindow(instance: wgpu.Instance, window_provider: WindowProvid }, }; }, + .emscripten => SurfaceDescriptor{ + .canvas_html = .{ + .label = "basic surface", + .selector = "#canvas", // TODO: can this be somehow exposed through api? + }, + }, else => if (isLinuxDesktopLike(os_tag)) linux: { if (window_provider.getWaylandDisplay()) |wl_display| { break :linux SurfaceDescriptor{ @@ -1673,6 +1730,16 @@ fn createSurfaceForWindow(instance: wgpu.Instance, window_provider: WindowProvid .label = if (src.label) |l| l else null, }); }, + .canvas_html => |src| blk: { + var desc: wgpu.SurfaceDescriptorFromCanvasHTMLSelector = .{ + .chain = .{ .struct_type = .surface_descriptor_from_canvas_html_selector, .next = null }, + .selector = src.selector, + }; + break :blk instance.createSurface(.{ + .next_in_chain = @as(*const wgpu.ChainedStruct, @ptrCast(&desc)), + .label = if (src.label) |l| l else null, + }); + }, }; } @@ -1781,3 +1848,15 @@ fn formatToShaderFormat(format: wgpu.TextureFormat) []const u8 { else => unreachable, }; } + +usingnamespace if (emscripten) struct { + // Missing symbols + var wgpuDeviceTickWarnPrinted: bool = false; + pub export fn wgpuDeviceTick() void { + if (!wgpuDeviceTickWarnPrinted) { + std.log.warn("wgpuDeviceTick(): this fn should be avoided! RequestAnimationFrame() is advised for smooth rendering in browser.", .{}); + wgpuDeviceTickWarnPrinted = true; + } + emscripten_sleep(1); + } +} else struct {};