-
-
Notifications
You must be signed in to change notification settings - Fork 146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Emscripten support: wasm & webgpu in browser #309
base: main
Are you sure you want to change the base?
Changes from 4 commits
5a20226
b63885f
b4881d4
d1b7804
b4d319e
35af55e
a2d1ea9
560dac0
2924d8b
c341dc7
f3649d7
e5c8e91
d5a0cd2
8098d3c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,8 +22,9 @@ pub fn build(b: *std.Build) void { | |
"Enable DirectX 12 GPU-Based Validation (GBV)", | ||
) orelse false, | ||
.zpix_enable = b.option(bool, "zpix-enable", "Enable PIX for Windows profiler") orelse false, | ||
.emscripten = b.option(bool, "emscripten", "Build for linking with emscripten toolchain") orelse false, | ||
}; | ||
ensureTarget(options.target) catch return; | ||
ensureTarget(options) catch return; | ||
ensureGit(b.allocator) catch return; | ||
ensureGitLfs(b.allocator, "install") catch return; | ||
ensureGitLfs(b.allocator, "pull") catch return; | ||
|
@@ -95,22 +96,24 @@ fn packagesCrossPlatform(b: *std.Build, options: Options) void { | |
const target = options.target; | ||
const optimize = options.optimize; | ||
|
||
zopengl_pkg = zopengl.package(b, target, optimize, .{}); | ||
zmath_pkg = zmath.package(b, target, optimize, .{}); | ||
zpool_pkg = zpool.package(b, target, optimize, .{}); | ||
zglfw_pkg = zglfw.package(b, target, optimize, .{}); | ||
zsdl_pkg = zsdl.package(b, target, optimize, .{}); | ||
zmesh_pkg = zmesh.package(b, target, optimize, .{}); | ||
znoise_pkg = znoise.package(b, target, optimize, .{}); | ||
zstbi_pkg = zstbi.package(b, target, optimize, .{}); | ||
zbullet_pkg = zbullet.package(b, target, optimize, .{}); | ||
zglfw_pkg = zglfw.package(b, target, optimize, .{ .options = .{ .emscripten = options.emscripten } }); | ||
zgui_pkg = zgui.package(b, target, optimize, .{ | ||
.options = .{ .backend = .glfw_wgpu }, | ||
.options = .{ .backend = .glfw_wgpu, .emscripten = options.emscripten }, | ||
}); | ||
zgpu_pkg = zgpu.package(b, target, optimize, .{ | ||
.options = .{ .uniforms_buffer_size = 4 * 1024 * 1024 }, | ||
.options = .{ .uniforms_buffer_size = 4 * 1024 * 1024, .emscripten = options.emscripten }, | ||
.deps = .{ .zpool = zpool_pkg.zpool, .zglfw = zglfw_pkg.zglfw }, | ||
}); | ||
if (options.emscripten) return; | ||
|
||
zopengl_pkg = zopengl.package(b, target, optimize, .{}); | ||
zsdl_pkg = zsdl.package(b, target, optimize, .{}); | ||
zmesh_pkg = zmesh.package(b, target, optimize, .{}); | ||
znoise_pkg = znoise.package(b, target, optimize, .{}); | ||
zstbi_pkg = zstbi.package(b, target, optimize, .{}); | ||
zbullet_pkg = zbullet.package(b, target, optimize, .{}); | ||
ztracy_pkg = ztracy.package(b, target, optimize, .{ | ||
.options = .{ | ||
.enable_ztracy = !target.isDarwin(), // TODO: ztracy fails to compile on macOS. | ||
|
@@ -168,6 +171,7 @@ fn packagesWindows(b: *std.Build, options: Options) void { | |
fn samplesCrossPlatform(b: *std.Build, options: Options) void { | ||
const minimal_gl = @import("samples/minimal_gl/build.zig"); | ||
const triangle_wgpu = @import("samples/triangle_wgpu/build.zig"); | ||
const triangle_wgpu_emscripten = @import("samples/triangle_wgpu_emscripten/build.zig"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a good way to develop initially but I think we should make the existing cross-platform samples emscripten compatible instead of adding new sample variants. Any samples that have not yet been made compatible can be individually skipped. |
||
const procedural_mesh_wgpu = @import("samples/procedural_mesh_wgpu/build.zig"); | ||
const textured_quad_wgpu = @import("samples/textured_quad_wgpu/build.zig"); | ||
const physically_based_rendering_wgpu = @import("samples/physically_based_rendering_wgpu/build.zig"); | ||
|
@@ -179,6 +183,10 @@ fn samplesCrossPlatform(b: *std.Build, options: Options) void { | |
const gamepad_wgpu = @import("samples/gamepad_wgpu/build.zig"); | ||
const physics_test_wgpu = @import("samples/physics_test_wgpu/build.zig"); | ||
|
||
install_options = options; | ||
install(b, triangle_wgpu_emscripten.build(b, options), "triangle_wgpu_emscripten"); | ||
if (options.emscripten) return; | ||
|
||
install(b, minimal_gl.build(b, options), "minimal_gl"); | ||
install(b, triangle_wgpu.build(b, options), "triangle_wgpu"); | ||
install(b, textured_quad_wgpu.build(b, options), "textured_quad_wgpu"); | ||
|
@@ -298,9 +306,13 @@ pub const Options = struct { | |
zd3d12_enable_gbv: bool, | ||
|
||
zpix_enable: bool, | ||
|
||
emscripten: bool = false, | ||
}; | ||
|
||
var install_options: ?Options = null; | ||
fn install(b: *std.Build, exe: *std.Build.CompileStep, comptime name: []const u8) void { | ||
const emscripten = install_options != null and install_options.?.emscripten; | ||
// TODO: Problems with LTO on Windows. | ||
exe.want_lto = false; | ||
if (exe.optimize == .ReleaseFast) | ||
|
@@ -311,12 +323,17 @@ fn install(b: *std.Build, exe: *std.Build.CompileStep, comptime name: []const u8 | |
//comptime var desc_size = std.mem.indexOf(u8, &desc_name, "\x00").?; | ||
|
||
const install_step = b.step(name, "Build '" ++ name ++ "' demo"); | ||
install_step.dependOn(&b.addInstallArtifact(exe).step); | ||
|
||
const run_step = b.step(name ++ "-run", "Run '" ++ name ++ "' demo"); | ||
const run_cmd = b.addRunArtifact(exe); | ||
run_cmd.step.dependOn(install_step); | ||
run_step.dependOn(&run_cmd.step); | ||
if (!emscripten) { | ||
install_step.dependOn(&b.addInstallArtifact(exe).step); | ||
|
||
const run_step = b.step(name ++ "-run", "Run '" ++ name ++ "' demo"); | ||
const run_cmd = b.addRunArtifact(exe); | ||
run_cmd.step.dependOn(install_step); | ||
run_step.dependOn(&run_cmd.step); | ||
} else { | ||
const link_step = linkEmscripten(b, install_options.?, exe) catch unreachable; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Linking should not happen inside |
||
install_step.dependOn(&link_step.step); | ||
} | ||
|
||
b.getInstallStep().dependOn(install_step); | ||
} | ||
|
@@ -344,7 +361,8 @@ fn ensureZigVersion() !void { | |
} | ||
} | ||
|
||
fn ensureTarget(cross: std.zig.CrossTarget) !void { | ||
fn ensureTarget(options: Options) !void { | ||
const cross = options.target; | ||
const target = (std.zig.system.NativeTargetInfo.detect(cross) catch unreachable).target; | ||
|
||
const supported = switch (target.os.tag) { | ||
|
@@ -359,6 +377,7 @@ fn ensureTarget(cross: std.zig.CrossTarget) !void { | |
if (target.os.version_range.semver.min.order(min_available) == .lt) break :blk false; | ||
break :blk true; | ||
}, | ||
.freestanding => target.cpu.arch == .wasm32 and options.emscripten, | ||
else => false, | ||
}; | ||
if (!supported) { | ||
|
@@ -482,3 +501,75 @@ fn ensureGitLfsContent(comptime file_path: []const u8) !void { | |
inline fn thisDir() []const u8 { | ||
return comptime std.fs.path.dirname(@src().file) orelse "."; | ||
} | ||
|
||
pub fn linkEmscripten(b: *std.Build, options: Options, exe: *std.Build.CompileStep) !*std.Build.Step.Run { | ||
var ems_closure: ?[]const u8 = null; | ||
const emsdk_path = b.env_map.get("EMSDK") orelse @panic("Failed to get emscripten SDK path, have you installed & sourced the SDK?"); | ||
const emscripten_include = b.pathJoin(&.{ emsdk_path, "upstream", "emscripten", "cache", "sysroot", "include" }); | ||
exe.addSystemIncludePath(emscripten_include); | ||
exe.stack_protector = false; | ||
exe.disable_stack_probing = true; | ||
exe.linkLibC(); | ||
|
||
const emlink = b.addSystemCommand(&.{"emcc"}); | ||
emlink.addArtifactArg(exe); | ||
for (exe.link_objects.items) |link_dependency| { | ||
switch (link_dependency) { | ||
.other_step => |o| emlink.addArtifactArg(o), | ||
// .c_source_file => |f| emlink.addFileSourceArg(f.source), // f.args? | ||
// .c_source_files => |fs| for (fs.files) |f| emlink.addArg(f), // fs.flags? | ||
else => {}, | ||
} | ||
} | ||
const out_path = b.pathJoin(&.{ b.pathFromRoot("."), "zig-out", "www", exe.name }); | ||
std.fs.cwd().makePath(out_path) catch unreachable; | ||
const out_file = try std.mem.concat(b.allocator, u8, &.{ "-o", out_path, std.fs.path.sep_str ++ "index.html" }); | ||
emlink.addArgs(&.{"-sEXPORTED_FUNCTIONS=['_malloc','_emmalloc_realloc','_main']"}); | ||
//emling.addArgs(&.{"-sLLD_REPORT_UNDEFINED", "-sERROR_ON_UNDEFINED_SYMBOLS=0"}); | ||
emlink.addArg(out_file); | ||
emlink.addArg("-sFILESYSTEM=0"); | ||
emlink.addArg("-sUSE_GLFW=3"); | ||
emlink.addArg("-sUSE_WEBGPU=1"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Library/sample specific flags could be added separately i.e. |
||
emlink.addArg("-sASYNCIFY"); | ||
//emlink.addArg("-sASYNCIFY_IGNORE_INDIRECT=1"); | ||
//emlink.addArg("-sASYNCIFY_ADVISE=1"); | ||
//emlink.addArg("-sERROR_ON_UNDEFINED_SYMBOLS=0"); | ||
emlink.addArgs(&.{ "-sINITIAL_MEMORY=64MB", "-sMALLOC=emmalloc", "-sABORTING_MALLOC=0", "-sEXIT_RUNTIME=0" }); | ||
emlink.addArg("-sALLOW_MEMORY_GROWTH"); | ||
emlink.addArgs(&.{ "-fno-rtti", "-fno-exceptions" }); | ||
emlink.addArg("-sWASM_BIGINT"); | ||
|
||
// there are a lot of flags that can be tried used either for debugging or optimization, these are basic defaults | ||
// see: https://emscripten.org/docs/tools_reference/emcc.html#emccdoc | ||
switch (options.optimize) { | ||
.Debug => { | ||
ems_closure = "0"; | ||
emlink.addArgs(&.{"-g"}); | ||
//emlink.addArgs(&.{"-Og"}); | ||
const source_map_base = "./"; // depending on how webserver is configured this might need to be changed | ||
emlink.addArgs(&.{ "-gsource-map", "--source-map-base", source_map_base }); | ||
}, | ||
.ReleaseSmall => { | ||
emlink.addArgs(&.{"-Os"}); | ||
exe.strip = true; | ||
//emlink.addArg("-Oz"); | ||
}, | ||
.ReleaseFast => { | ||
emlink.addArg("-O2"); // keeps function names, useful for release-debug style build | ||
//emlink.addArgs(&.{"-flto"}); | ||
}, | ||
.ReleaseSafe => { | ||
emlink.addArg("-O2"); | ||
}, | ||
} | ||
|
||
if (ems_closure) |c| { | ||
emlink.addArgs(&.{ "--closure", c }); | ||
} | ||
|
||
// custom html shell | ||
//emlink.addArgs(&.{ "--shell-file", "src/shell_simple.html" }); | ||
|
||
emlink.step.dependOn(&exe.step); | ||
return emlink; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,12 +3,15 @@ const std = @import("std"); | |
pub const Package = struct { | ||
zglfw: *std.Build.Module, | ||
zglfw_c_cpp: *std.Build.CompileStep, | ||
options: Options, | ||
|
||
pub fn link(pkg: Package, exe: *std.Build.CompileStep) void { | ||
exe.addModule("zglfw", pkg.zglfw); | ||
|
||
const host = (std.zig.system.NativeTargetInfo.detect(exe.target) catch unreachable).target; | ||
|
||
if (pkg.options.emscripten) return; | ||
|
||
switch (host.os.tag) { | ||
.windows => {}, | ||
.macos => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can just leverage the Zigs builtin emscripten target here and treat it like the others. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just commited this. Not sure if its better. Its still tricky because zig modules reuse target from target its linked with (as I understand), now zglfw and zgpu etc. has to assume both .emscripten and .freestanding target means emscripten. It works as long as we don't need any other .freestanding target. But still we have to support There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Why? I don't understand. Do you mean that if we compile our game as freestanding we are unable to link zglfw if has been built with emscripten os tag? I thought that it would link because they are both wasm32. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Basically issue is that I had to use In regards to zglfw & libs issue is not with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, i follow now. Thanks for clarifying. I'm not sure whether it's better to deal with the entry point or not in the long run; but let's just go with this for now. |
||
|
@@ -36,6 +39,7 @@ pub const Package = struct { | |
|
||
pub const Options = struct { | ||
shared: bool = false, | ||
emscripten: bool = false, | ||
}; | ||
|
||
pub fn package( | ||
|
@@ -53,6 +57,12 @@ pub fn package( | |
.source_file = .{ .path = thisDir() ++ "/src/zglfw.zig" }, | ||
}); | ||
|
||
if (args.options.emscripten) return .{ | ||
.zglfw = zglfw, | ||
.zglfw_c_cpp = undefined, | ||
.options = args.options, | ||
}; | ||
|
||
const zglfw_c_cpp = if (args.options.shared) blk: { | ||
const lib = b.addSharedLibrary(.{ | ||
.name = "zglfw", | ||
|
@@ -164,6 +174,7 @@ pub fn package( | |
return .{ | ||
.zglfw = zglfw, | ||
.zglfw_c_cpp = zglfw_c_cpp, | ||
.options = args.options, | ||
}; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
const std = @import("std"); | ||
const testing = std.testing; | ||
const c = @cImport({ | ||
@cInclude("webgpu/webgpu.h"); | ||
}); | ||
const gpu = @import("wgpu.zig"); | ||
|
||
// test struct sizes | ||
fn assertStructBindings(zig_type: anytype, c_type: anytype) void { | ||
if (@sizeOf(zig_type) != @sizeOf(c_type)) { | ||
@compileLog("emscripten zgpu type has different size from webgpu.h type:", zig_type, c_type, @sizeOf(zig_type), @sizeOf(c_type)); | ||
unreachable; | ||
} | ||
// more checks? iterate struct fields? | ||
// emscripten in .Debug mode asserts at runtime if required field is missing etc. but something always slips through | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand what is meant by something slips through There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. core issue is why I added these tests is that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @michal-z do you have any thoughts regarding how struct layouts can be validated? |
||
} | ||
|
||
comptime { | ||
assertStructBindings(gpu.ChainedStruct, c.WGPUChainedStruct); | ||
assertStructBindings(gpu.ChainedStructOut, c.WGPUChainedStructOut); | ||
assertStructBindings(gpu.AdapterProperties, c.WGPUAdapterProperties); | ||
assertStructBindings(gpu.BindGroupEntry, c.WGPUBindGroupEntry); | ||
assertStructBindings(gpu.BlendComponent, c.WGPUBlendComponent); | ||
assertStructBindings(gpu.BufferBindingLayout, c.WGPUBufferBindingLayout); | ||
assertStructBindings(gpu.BufferDescriptor, c.WGPUBufferDescriptor); | ||
assertStructBindings(gpu.Color, c.WGPUColor); | ||
assertStructBindings(gpu.CommandBufferDescriptor, c.WGPUCommandBufferDescriptor); | ||
assertStructBindings(gpu.CommandEncoderDescriptor, c.WGPUCommandEncoderDescriptor); | ||
assertStructBindings(gpu.CompilationMessage, c.WGPUCompilationMessage); | ||
assertStructBindings(gpu.ComputePassTimestampWrite, c.WGPUComputePassTimestampWrite); | ||
assertStructBindings(gpu.ConstantEntry, c.WGPUConstantEntry); | ||
assertStructBindings(gpu.Extent3D, c.WGPUExtent3D); | ||
//assertStructBindings(gpu.InstanceDescriptor, c.WGPUInstanceDescriptor); | ||
assertStructBindings(gpu.Limits, c.WGPULimits); | ||
assertStructBindings(gpu.MultisampleState, c.WGPUMultisampleState); | ||
assertStructBindings(gpu.Origin3D, c.WGPUOrigin3D); | ||
assertStructBindings(gpu.PipelineLayoutDescriptor, c.WGPUPipelineLayoutDescriptor); | ||
//assertStructBindings(gpu.PrimitiveDepthClipControl, c.WGPUPrimitiveDepthClipControl); | ||
assertStructBindings(gpu.PrimitiveState, c.WGPUPrimitiveState); | ||
assertStructBindings(gpu.QuerySetDescriptor, c.WGPUQuerySetDescriptor); | ||
//assertStructBindings(gpu.QueueDescriptor, c.WGPUQueueDescriptor); | ||
assertStructBindings(gpu.RenderBundleDescriptor, c.WGPURenderBundleDescriptor); | ||
assertStructBindings(gpu.RenderBundleEncoderDescriptor, c.WGPURenderBundleEncoderDescriptor); | ||
assertStructBindings(gpu.RenderPassDepthStencilAttachment, c.WGPURenderPassDepthStencilAttachment); | ||
//assertStructBindings(gpu.RenderPassDescriptorMaxDrawCount, c.WGPURenderPassDescriptorMaxDrawCount); | ||
assertStructBindings(gpu.RenderPassTimestampWrite, c.WGPURenderPassTimestampWrite); | ||
assertStructBindings(gpu.RequestAdapterOptions, c.WGPURequestAdapterOptions); | ||
assertStructBindings(gpu.SamplerBindingLayout, c.WGPUSamplerBindingLayout); | ||
assertStructBindings(gpu.SamplerDescriptor, c.WGPUSamplerDescriptor); | ||
assertStructBindings(gpu.ShaderModuleDescriptor, c.WGPUShaderModuleDescriptor); | ||
//assertStructBindings(gpu.ShaderModuleSPIRVDescriptor, c.WGPUShaderModuleSPIRVDescriptor); | ||
//assertStructBindings(gpu.ShaderModuleWGSLDescriptor, c.WGPUShaderModuleWGSLDescriptor); | ||
assertStructBindings(gpu.StencilFaceState, c.WGPUStencilFaceState); | ||
assertStructBindings(gpu.StorageTextureBindingLayout, c.WGPUStorageTextureBindingLayout); | ||
assertStructBindings(gpu.SurfaceDescriptor, c.WGPUSurfaceDescriptor); | ||
assertStructBindings(gpu.SurfaceDescriptorFromCanvasHTMLSelector, c.WGPUSurfaceDescriptorFromCanvasHTMLSelector); | ||
//assertStructBindings(gpu.SwapChainDescriptor, c.WGPUSwapChainDescriptor); | ||
assertStructBindings(gpu.TextureBindingLayout, c.WGPUTextureBindingLayout); | ||
assertStructBindings(gpu.TextureDataLayout, c.WGPUTextureDataLayout); | ||
assertStructBindings(gpu.TextureViewDescriptor, c.WGPUTextureViewDescriptor); | ||
assertStructBindings(gpu.VertexAttribute, c.WGPUVertexAttribute); | ||
assertStructBindings(gpu.BindGroupDescriptor, c.WGPUBindGroupDescriptor); | ||
assertStructBindings(gpu.BindGroupLayoutEntry, c.WGPUBindGroupLayoutEntry); | ||
assertStructBindings(gpu.BlendState, c.WGPUBlendState); | ||
assertStructBindings(gpu.CompilationInfo, c.WGPUCompilationInfo); | ||
assertStructBindings(gpu.ComputePassDescriptor, c.WGPUComputePassDescriptor); | ||
assertStructBindings(gpu.DepthStencilState, c.WGPUDepthStencilState); | ||
assertStructBindings(gpu.ImageCopyBuffer, c.WGPUImageCopyBuffer); | ||
assertStructBindings(gpu.ImageCopyTexture, c.WGPUImageCopyTexture); | ||
assertStructBindings(gpu.ProgrammableStageDescriptor, c.WGPUProgrammableStageDescriptor); | ||
assertStructBindings(gpu.RenderPassColorAttachment, c.WGPURenderPassColorAttachment); | ||
assertStructBindings(gpu.RequiredLimits, c.WGPURequiredLimits); | ||
assertStructBindings(gpu.SupportedLimits, c.WGPUSupportedLimits); | ||
assertStructBindings(gpu.TextureDescriptor, c.WGPUTextureDescriptor); | ||
assertStructBindings(gpu.VertexBufferLayout, c.WGPUVertexBufferLayout); | ||
assertStructBindings(gpu.BindGroupLayoutDescriptor, c.WGPUBindGroupLayoutDescriptor); | ||
assertStructBindings(gpu.ColorTargetState, c.WGPUColorTargetState); | ||
assertStructBindings(gpu.ComputePipelineDescriptor, c.WGPUComputePipelineDescriptor); | ||
assertStructBindings(gpu.DeviceDescriptor, c.WGPUDeviceDescriptor); | ||
assertStructBindings(gpu.RenderPassDescriptor, c.WGPURenderPassDescriptor); | ||
assertStructBindings(gpu.VertexState, c.WGPUVertexState); | ||
assertStructBindings(gpu.FragmentState, c.WGPUFragmentState); | ||
assertStructBindings(gpu.RenderPipelineDescriptor, c.WGPURenderPipelineDescriptor); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of adding another option, I think we should drive emsc builds using Zig's built-in emscripten os target, the same way as when x-compiling other targets. it can be "overridden" to freestanding wherever appropriate.