├── .gitattributes ├── .gitignore ├── .gitmodules ├── build.zig ├── demo ├── cube │ ├── build.zig │ ├── cube.zig │ ├── cube_frag.wgsl │ └── cube_vert.wgsl ├── tri │ ├── build.zig │ ├── tri.zig │ ├── tri_frag.wgsl │ └── tri_vert.wgsl └── ui │ ├── build.zig │ └── ui.zig ├── license.txt ├── readme.md ├── res └── ui_dbg_font.png └── src ├── bake.zig ├── bake_types.zig ├── cfg.zig ├── gfx.zig ├── gfx_web.zig ├── gfx_webgpu.js ├── main.zig ├── main_web.js ├── main_web.zig ├── math.zig ├── mem.zig ├── mem_web.zig ├── minify.zig ├── qoi.zig ├── res.zig ├── res_web.js ├── res_web.zig ├── serde.zig ├── time.zig ├── time_web.js ├── time_web.zig ├── ui.zig ├── ui_frag.wgsl ├── ui_gfx.zig ├── ui_res.zig ├── ui_vert.wgsl ├── utils.js ├── wnd.zig ├── wnd_gfx.zig ├── wnd_web.js └── wnd_web.zig /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | zig-cache/ 2 | zig-out/ 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ext/zig-gamedev"] 2 | path = ext/zig-gamedev 3 | url = https://github.com/michal-z/zig-gamedev.git 4 | [submodule "ext/stb"] 5 | path = ext/stb 6 | url = https://github.com/nothings/stb.git 7 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const cfg = @import("src/cfg.zig"); 2 | const demo_cube = @import("demo/cube/build.zig"); 3 | const demo_tri = @import("demo/tri/build.zig"); 4 | const demo_ui = @import("demo/ui/build.zig"); 5 | const minify = @import("src/minify.zig"); 6 | const std = @import("std"); 7 | const zmath = @import("ext/zig-gamedev/libs/zmath/build.zig"); 8 | 9 | const Demo = enum { 10 | tri, 11 | cube, 12 | ui, 13 | }; 14 | 15 | pub fn build(builder: *std.build.Builder) !void { 16 | const demo = builder.option(Demo, "demo", "demo project") orelse .tri; 17 | switch (demo) { 18 | .tri => try demo_tri.build(builder), 19 | .cube => try demo_cube.build(builder), 20 | .ui => try demo_ui.build(builder), 21 | } 22 | } 23 | 24 | // package for "baking" resources into runtime-ready format 25 | pub fn getBakePkg( 26 | builder: *std.build.Builder, 27 | options: Options, 28 | name: []const u8, 29 | dest_dir: []const u8, 30 | recipe: Recipe, 31 | ) std.build.Pkg { 32 | const baker = Baker.init( 33 | builder, 34 | options, 35 | name, 36 | dest_dir, 37 | recipe, 38 | ) catch unreachable; 39 | return baker.getPkg(); 40 | } 41 | 42 | // cross platform graphics based on webgpu 43 | pub fn getGfxPkg() std.build.Pkg { 44 | return std.build.Pkg{ 45 | .name = "cc_gfx", 46 | .path = .{ .path = "src/gfx.zig" }, 47 | }; 48 | } 49 | 50 | // cross platform math (exports zmath) 51 | pub fn getMathPkg() std.build.Pkg { 52 | return std.build.Pkg{ 53 | .name = "cc_math", 54 | .path = .{ .path = "src/math.zig" }, 55 | .dependencies = &.{zmath.pkg}, 56 | }; 57 | } 58 | 59 | // cross platform memory allocation 60 | pub fn getMemPkg() std.build.Pkg { 61 | return std.build.Pkg{ 62 | .name = "cc_mem", 63 | .path = .{ .path = "src/mem.zig" }, 64 | }; 65 | } 66 | 67 | // cross platform file system resources 68 | pub fn getResPkg() std.build.Pkg { 69 | return std.build.Pkg{ 70 | .name = "cc_res", 71 | .path = .{ .path = "src/res.zig" }, 72 | }; 73 | } 74 | 75 | // cross platform timing and profiling 76 | pub fn getTimePkg() std.build.Pkg { 77 | return std.build.Pkg{ 78 | .name = "cc_time", 79 | .path = .{ .path = "src/time.zig" }, 80 | }; 81 | } 82 | 83 | // user interface 84 | pub fn getUiPkg() std.build.Pkg { 85 | return std.build.Pkg{ 86 | .name = "cc_ui", 87 | .path = .{ .path = "src/ui.zig" }, 88 | }; 89 | } 90 | 91 | // gfx implementation for user interface 92 | pub fn getUiGfxPkg() std.build.Pkg { 93 | return std.build.Pkg{ 94 | .name = "cc_ui_gfx", 95 | .path = .{ .path = "src/ui_gfx.zig" }, 96 | }; 97 | } 98 | 99 | // res implementation for user interface 100 | pub fn getUiResPkg( 101 | builder: *std.build.Builder, 102 | options: Options, 103 | ) std.build.Pkg { 104 | const recipe = Recipe{ 105 | .dir = ".", 106 | .items = &.{ .{ 107 | .id = "ui_vert_shader", 108 | .output = .pkg_embed, 109 | .bake_type = "shader", 110 | .deps = &.{"src/ui_vert.wgsl"}, 111 | }, .{ 112 | .id = "ui_frag_shader", 113 | .output = .pkg_embed, 114 | .bake_type = "shader", 115 | .deps = &.{"src/ui_frag.wgsl"}, 116 | }, .{ 117 | .id = "ui_dbg_font_texture", 118 | .output = .pkg_embed, 119 | .bake_type = "texture", 120 | .deps = &.{"res/ui_dbg_font.png"}, 121 | } }, 122 | }; 123 | const deps = builder.allocator.alloc(std.build.Pkg, 1) catch unreachable; 124 | deps[0] = getBakePkg(builder, options, "cc_ui_res", ".", recipe); 125 | 126 | return std.build.Pkg{ 127 | .name = "cc_ui_res", 128 | .path = .{ .path = "src/ui_res.zig" }, 129 | .dependencies = deps, 130 | }; 131 | } 132 | 133 | // cross platform windowing 134 | pub fn getWndPkg() std.build.Pkg { 135 | return std.build.Pkg{ 136 | .name = "cc_wnd", 137 | .path = .{ .path = "src/wnd.zig" }, 138 | }; 139 | } 140 | 141 | // helper package for wnd and gfx interop 142 | pub fn getWndGfxPkg() std.build.Pkg { 143 | return std.build.Pkg{ 144 | .name = "cc_wnd_gfx", 145 | .path = .{ .path = "src/wnd_gfx.zig" }, 146 | }; 147 | } 148 | 149 | pub const RecipeItemOutput = enum { 150 | cache, // item is baked to cache, but not included in the pkg 151 | pkg_embed, // item gets embedded into the binary 152 | pkg_install, // item gets installed to dest dir 153 | }; 154 | 155 | pub const RecipeItem = struct { 156 | id: []const u8, // needs to be valid zig identifier and unique within the recipe 157 | output: RecipeItemOutput, // where the baked data gets output to 158 | bake_pkg: []const u8 = "cc_bake", // modify if you are using a custom bake pkg 159 | bake_type: []const u8, // bake function that should be used for this item 160 | deps: []const []const u8, // file path or id of another recipe item 161 | }; 162 | 163 | pub const BakePkg = struct { 164 | pkg: std.build.Pkg, 165 | link_fn: fn (*std.build.Builder, *std.build.LibExeObjStep) anyerror!void, 166 | }; 167 | 168 | pub const Recipe = struct { 169 | dir: []const u8, // relative to build root. all items are relative to this dir 170 | items: []const RecipeItem, // list of items to bake 171 | bake_pkgs: []const BakePkg = &.{}, // list of custom bake pkgs 172 | }; 173 | 174 | pub const Options = struct { 175 | platform: cfg.Platform = .web, 176 | opt_level: cfg.OptLevel = .dbg, 177 | bake_level: ?cfg.OptLevel = null, 178 | log_level: std.log.Level = .debug, 179 | log_enabled: bool = true, 180 | 181 | pub fn init(builder: *std.build.Builder) Options { 182 | const platform = builder.option( 183 | cfg.Platform, 184 | "p", 185 | "platform", 186 | ) orelse .web; 187 | const opt_level = builder.option( 188 | cfg.OptLevel, 189 | "opt", 190 | "optimization level", 191 | ) orelse .dbg; 192 | const bake_level = builder.option( 193 | cfg.OptLevel, 194 | "bake", 195 | "when specified, resources will be baked before compiling", 196 | ); 197 | const log_level = builder.option( 198 | std.log.Level, 199 | "log", 200 | "log level", 201 | ) orelse switch (opt_level) { 202 | .dbg => std.log.Level.debug, 203 | .rel => std.log.Level.err, 204 | }; 205 | const log_enabled = builder.option( 206 | bool, 207 | "log_enabled", 208 | "set to false to disable logging", 209 | ) orelse true; 210 | return Options{ 211 | .platform = platform, 212 | .opt_level = opt_level, 213 | .bake_level = bake_level, 214 | .log_level = log_level, 215 | .log_enabled = log_enabled, 216 | }; 217 | } 218 | }; 219 | 220 | pub fn initMainLibExe( 221 | builder: *std.build.Builder, 222 | options: Options, 223 | dest_dir: []const u8, 224 | pkg: std.build.Pkg, 225 | ) !*std.build.LibExeObjStep { 226 | const app_name = pkg.name; 227 | var app_pkg = builder.dupePkg(pkg); 228 | app_pkg.name = "app"; 229 | 230 | const build_options = builder.addOptions(); 231 | build_options.addOption(bool, "log_enabled", options.log_enabled); 232 | build_options.addOption(std.log.Level, "log_level", options.log_level); 233 | 234 | const main_lib_exe = switch (options.platform) { 235 | .web => try initWebLibExe(builder, app_name), 236 | .win => return error.NotYetImplemented, 237 | }; 238 | main_lib_exe.setBuildMode(switch (options.opt_level) { 239 | .dbg => .Debug, 240 | .rel => .ReleaseFast, 241 | }); 242 | main_lib_exe.override_dest_dir = .{ .custom = dest_dir }; 243 | main_lib_exe.addPackage(build_options.getPackage("build_options")); 244 | main_lib_exe.addPackage(app_pkg); 245 | main_lib_exe.install(); 246 | 247 | try installPlatformFiles(builder, options, app_name, dest_dir); 248 | 249 | return main_lib_exe; 250 | } 251 | 252 | fn initWebLibExe( 253 | builder: *std.build.Builder, 254 | name: []const u8, 255 | ) !*std.build.LibExeObjStep { 256 | const main_lib_exe = builder.addSharedLibrary( 257 | name, 258 | "src/main.zig", 259 | .unversioned, 260 | ); 261 | const target = try std.zig.CrossTarget.parse( 262 | .{ .arch_os_abi = "wasm32-freestanding" }, 263 | ); 264 | main_lib_exe.setTarget(target); 265 | return main_lib_exe; 266 | } 267 | 268 | pub fn installPlatformFiles( 269 | builder: *std.build.Builder, 270 | options: Options, 271 | name: []const u8, 272 | dest_dir: []const u8, 273 | ) !void { 274 | switch (options.platform) { 275 | .web => try installWebFiles(builder, options, name, dest_dir), 276 | .win => return error.NotYetImplemented, 277 | } 278 | } 279 | 280 | fn installWebFiles( 281 | builder: *std.build.Builder, 282 | options: Options, 283 | name: []const u8, 284 | dest_dir: []const u8, 285 | ) !void { 286 | const gen_web_files = try GenerateWebFiles.init(builder, options, name); 287 | 288 | const install_html_path = try std.fs.path.join( 289 | builder.allocator, 290 | &.{ dest_dir, "index.html" }, 291 | ); 292 | defer builder.allocator.free(install_html_path); 293 | const install_js_path = try std.fs.path.join( 294 | builder.allocator, 295 | &.{ dest_dir, "cupcake.js" }, 296 | ); 297 | defer builder.allocator.free(install_js_path); 298 | 299 | const install_html = builder.addInstallFile( 300 | .{ .generated = &gen_web_files.html_file }, 301 | install_html_path, 302 | ); 303 | const install_js = builder.addInstallFile( 304 | .{ .generated = &gen_web_files.js_file }, 305 | install_js_path, 306 | ); 307 | 308 | if (options.bake_level != null) { 309 | install_html.step.dependOn(&gen_web_files.step); 310 | install_js.step.dependOn(&gen_web_files.step); 311 | } 312 | 313 | const install_step = builder.getInstallStep(); 314 | install_step.dependOn(&install_html.step); 315 | install_step.dependOn(&install_js.step); 316 | } 317 | 318 | fn getCacheDir(builder: *std.build.Builder) ![]u8 { 319 | return try std.fs.path.join( 320 | builder.allocator, 321 | &.{ builder.build_root, builder.cache_root, "cupcake" }, 322 | ); 323 | } 324 | 325 | fn getBakeCacheDir( 326 | builder: *std.build.Builder, 327 | options: Options, 328 | name: []const u8, 329 | ) ![]u8 { 330 | return try std.fs.path.join( 331 | builder.allocator, 332 | &.{ try getCacheDir(builder), "bake", name, @tagName(options.platform) }, 333 | ); 334 | } 335 | 336 | pub fn getDestDir( 337 | builder: *std.build.Builder, 338 | options: Options, 339 | name: []const u8, 340 | ) ![]u8 { 341 | return try std.fs.path.join( 342 | builder.allocator, 343 | &.{ name, @tagName(options.platform) }, 344 | ); 345 | } 346 | 347 | // escapes separators in a string for embedding in a source file 348 | fn getPathString(allocator: std.mem.Allocator, path: []const u8) ![]u8 { 349 | var path_string = std.ArrayList(u8).init(allocator); 350 | for (path) |char| { 351 | if (std.fs.path.isSep(char)) { 352 | try path_string.appendSlice("/"); 353 | } else { 354 | try path_string.append(char); 355 | } 356 | } 357 | return path_string.toOwnedSlice(); 358 | } 359 | 360 | const Baker = struct { 361 | builder: *std.build.Builder, 362 | recipe: Recipe, 363 | platform: cfg.Platform, 364 | bake_level: ?cfg.OptLevel, 365 | cache_dir: []const u8, 366 | install_step: std.build.Step, 367 | list_step: std.build.Step, 368 | list_file: std.build.GeneratedFile, 369 | pkg_file: std.build.GeneratedFile, 370 | deps: std.ArrayList(std.build.Pkg), 371 | 372 | pub fn init( 373 | builder: *std.build.Builder, 374 | options: Options, 375 | name: []const u8, 376 | dest_dir: []const u8, 377 | recipe: Recipe, 378 | ) !*Baker { 379 | const cache_dir = try getBakeCacheDir(builder, options, name); 380 | 381 | var baker = try builder.allocator.create(Baker); 382 | baker.* = .{ 383 | .builder = builder, 384 | .recipe = recipe, 385 | .platform = options.platform, 386 | .bake_level = options.bake_level, 387 | .cache_dir = cache_dir, 388 | .install_step = std.build.Step.initNoOp( 389 | .custom, 390 | "bake install", 391 | builder.allocator, 392 | ), 393 | .list_step = std.build.Step.init( 394 | .custom, 395 | "bake list", 396 | builder.allocator, 397 | make, 398 | ), 399 | .list_file = .{ .step = &baker.list_step }, 400 | .pkg_file = .{ 401 | .step = &baker.install_step, 402 | .path = try std.fs.path.join( 403 | builder.allocator, 404 | &.{ cache_dir, "bake_pkg.zig" }, 405 | ), 406 | }, 407 | .deps = std.ArrayList(std.build.Pkg).init(builder.allocator), 408 | }; 409 | 410 | var bake_exe = builder.addExecutable("bake", "src/bake.zig"); 411 | bake_exe.setBuildMode(.ReleaseSafe); 412 | 413 | const cc_bake_pkg = BakePkg{ 414 | .pkg = .{ 415 | .name = "cc_bake", 416 | .path = .{ .path = "src/bake_types.zig" }, 417 | }, 418 | .link_fn = ccLinkBakePkg, 419 | }; 420 | try baker.addBakePkg(bake_exe, cc_bake_pkg); 421 | for (recipe.bake_pkgs) |bake_pkg| { 422 | try baker.addBakePkg(bake_exe, bake_pkg); 423 | } 424 | 425 | bake_exe.addPackage(.{ 426 | .name = "bake_list", 427 | .path = .{ .generated = &baker.list_file }, 428 | .dependencies = baker.deps.items, 429 | }); 430 | 431 | if (options.bake_level != null) { 432 | try std.fs.cwd().makePath(baker.cache_dir); 433 | baker.install_step.dependOn(&bake_exe.run().step); 434 | } 435 | 436 | for (recipe.items) |item| { 437 | if (item.output != .pkg_install) { 438 | continue; 439 | } 440 | 441 | const item_path = try std.fs.path.join( 442 | builder.allocator, 443 | &.{ baker.cache_dir, item.id }, 444 | ); 445 | defer builder.allocator.free(item_path); 446 | 447 | const install_path = try std.fs.path.join( 448 | builder.allocator, 449 | &.{ dest_dir, item.id }, 450 | ); 451 | defer builder.allocator.free(install_path); 452 | 453 | const install_item = builder.addInstallFile( 454 | .{ .path = item_path }, 455 | install_path, 456 | ); 457 | install_item.step.dependOn(&baker.install_step); 458 | builder.getInstallStep().dependOn(&install_item.step); 459 | } 460 | 461 | return baker; 462 | } 463 | 464 | fn addBakePkg( 465 | baker: *Baker, 466 | lib_exe: *std.build.LibExeObjStep, 467 | bake_pkg: BakePkg, 468 | ) !void { 469 | try baker.deps.append(baker.builder.dupePkg(bake_pkg.pkg)); 470 | try bake_pkg.link_fn(baker.builder, lib_exe); 471 | } 472 | 473 | fn ccLinkBakePkg( 474 | builder: *std.build.Builder, 475 | lib_exe: *std.build.LibExeObjStep, 476 | ) !void { 477 | lib_exe.linkLibC(); 478 | lib_exe.addIncludePath("ext"); 479 | 480 | const header_lib_impl = try GenerateHeaderImpl.init(builder, &.{ 481 | .{ .path = "stb/stb_image.h", .define = "STB_IMAGE_IMPLEMENTATION" }, 482 | }); 483 | const header_lib_obj = builder.addStaticLibrary("header_libs", null); 484 | header_lib_obj.addCSourceFileSource(.{ 485 | .source = .{ .generated = &header_lib_impl.file }, 486 | .args = &.{}, 487 | }); 488 | header_lib_obj.linkLibC(); 489 | header_lib_obj.addIncludePath("ext"); 490 | lib_exe.linkLibrary(header_lib_obj); 491 | } 492 | 493 | pub fn getPkg(baker: Baker) std.build.Pkg { 494 | const pkg_src = if (baker.bake_level == null) block: { 495 | break :block std.build.FileSource{ .path = baker.pkg_file.getPath() }; 496 | } else block: { 497 | break :block std.build.FileSource{ .generated = &baker.pkg_file }; 498 | }; 499 | 500 | return std.build.Pkg{ 501 | .name = "cc_bake", 502 | .path = pkg_src, 503 | .dependencies = baker.deps.items, 504 | }; 505 | } 506 | 507 | fn make(step: *std.build.Step) !void { 508 | const baker = @fieldParentPtr(Baker, "list_step", step); 509 | 510 | var list_file_contents = std.ArrayList(u8).init(baker.builder.allocator); 511 | defer list_file_contents.deinit(); 512 | 513 | const root_dir = try std.fs.path.join( 514 | baker.builder.allocator, 515 | &.{ baker.builder.build_root, baker.recipe.dir }, 516 | ); 517 | 518 | const writer = list_file_contents.writer(); 519 | 520 | try writer.print("pub const pkgs = struct {{\n", .{}); 521 | for (baker.deps.items) |dep| { 522 | try writer.print(" ", .{}); 523 | try writer.print( 524 | "pub const {s} = @import(\"{s}\");\n", 525 | .{ dep.name, dep.name }, 526 | ); 527 | } 528 | try writer.print("}};\n", .{}); 529 | 530 | try writer.print( 531 | "pub const in_dir = \"{s}\";\n", 532 | .{try getPathString(baker.builder.allocator, root_dir)}, 533 | ); 534 | try writer.print( 535 | "pub const out_dir = \"{s}\";\n", 536 | .{try getPathString(baker.builder.allocator, baker.cache_dir)}, 537 | ); 538 | try writer.print( 539 | "pub const platform = .{s};\n", 540 | .{@tagName(baker.platform)}, 541 | ); 542 | try writer.print( 543 | "pub const opt_level = .{s};\n", 544 | .{@tagName(baker.bake_level.?)}, 545 | ); 546 | try writer.print("pub const items = struct {{\n", .{}); 547 | for (baker.recipe.items) |item| { 548 | try writer.print(" ", .{}); 549 | try writer.print("pub const {s} = .{{\n", .{item.id}); 550 | try writer.print(" ", .{}); 551 | try writer.print(".bake_pkg = \"{s}\",\n", .{item.bake_pkg}); 552 | try writer.print(" ", .{}); 553 | try writer.print(".bake_type = \"{s}\",\n", .{item.bake_type}); 554 | try writer.print(" ", .{}); 555 | try writer.print(".output = .{s},\n", .{@tagName(item.output)}); 556 | try writer.print(" ", .{}); 557 | try writer.print(".deps = &[_][]const u8{{\n", .{}); 558 | for (item.deps) |dep| { 559 | try writer.print(" ", .{}); 560 | try writer.print("\"{s}\",\n", .{dep}); 561 | } 562 | try writer.print(" ", .{}); 563 | try writer.print("}},\n", .{}); 564 | try writer.print(" ", .{}); 565 | try writer.print("}};\n", .{}); 566 | } 567 | try writer.print("}};\n", .{}); 568 | 569 | const list_path = try std.fs.path.join( 570 | baker.builder.allocator, 571 | &.{ baker.cache_dir, "bake_list.zig" }, 572 | ); 573 | try std.fs.cwd().writeFile(list_path, list_file_contents.items); 574 | baker.list_file.path = list_path; 575 | } 576 | }; 577 | 578 | const GenerateWebFiles = struct { 579 | builder: *std.build.Builder, 580 | step: std.build.Step, 581 | cache_dir: []const u8, 582 | name: []const u8, 583 | bake_level: ?cfg.OptLevel, 584 | html_file: std.build.GeneratedFile, 585 | js_file: std.build.GeneratedFile, 586 | 587 | pub fn init( 588 | builder: *std.build.Builder, 589 | options: Options, 590 | name: []const u8, 591 | ) !*GenerateWebFiles { 592 | const cache_dir = try getBakeCacheDir(builder, options, name); 593 | try std.fs.cwd().makePath(cache_dir); 594 | 595 | const html_file_path = try std.fs.path.join( 596 | builder.allocator, 597 | &.{ cache_dir, "index.html" }, 598 | ); 599 | const js_file_path = try std.fs.path.join( 600 | builder.allocator, 601 | &.{ cache_dir, "cupcake.js" }, 602 | ); 603 | 604 | var gen_web_files = try builder.allocator.create(GenerateWebFiles); 605 | gen_web_files.* = .{ 606 | .builder = builder, 607 | .step = std.build.Step.init( 608 | .custom, 609 | "gen web files", 610 | builder.allocator, 611 | make, 612 | ), 613 | .cache_dir = cache_dir, 614 | .name = name, 615 | .bake_level = options.bake_level, 616 | .html_file = .{ .step = &gen_web_files.step, .path = html_file_path }, 617 | .js_file = .{ .step = &gen_web_files.step, .path = js_file_path }, 618 | }; 619 | return gen_web_files; 620 | } 621 | 622 | fn make(step: *std.build.Step) !void { 623 | const gen_web_files = @fieldParentPtr(GenerateWebFiles, "step", step); 624 | 625 | try gen_web_files.makeHtmlFile(); 626 | try gen_web_files.makeJsFile(); 627 | } 628 | 629 | fn makeHtmlFile(gen_web_files: *GenerateWebFiles) !void { 630 | const html_file = try std.fs.cwd().createFile( 631 | gen_web_files.html_file.getPath(), 632 | .{ .truncate = true }, 633 | ); 634 | defer html_file.close(); 635 | const html_fmt = 636 | \\ 637 | \\ 638 | \\ 639 | \\ 640 | \\ 647 | \\ 648 | \\ 649 | \\ 650 | \\ 653 | \\ 654 | \\ 655 | ; 656 | try std.fmt.format(html_file.writer(), html_fmt, .{gen_web_files.name}); 657 | } 658 | 659 | fn makeJsFile(gen_web_files: *GenerateWebFiles) !void { 660 | // intentional ordering to prevent dependency issues 661 | const js_srcs: []const []const u8 = &.{ 662 | "utils.js", 663 | "main_web.js", 664 | "time_web.js", 665 | "res_web.js", 666 | "wnd_web.js", 667 | "gfx_webgpu.js", 668 | }; 669 | 670 | const js_src_path = try std.fs.path.join( 671 | gen_web_files.builder.allocator, 672 | &.{ gen_web_files.builder.build_root, "src" }, 673 | ); 674 | defer gen_web_files.builder.allocator.free(js_src_path); 675 | 676 | var js_src_dir = try std.fs.cwd().openDir(js_src_path, .{}); 677 | defer js_src_dir.close(); 678 | 679 | var js_contents = std.ArrayList(u8).init(gen_web_files.builder.allocator); 680 | defer js_contents.deinit(); 681 | 682 | for (js_srcs) |js_src| { 683 | const js_src_file = try js_src_dir.openFile(js_src, .{}); 684 | defer js_src_file.close(); 685 | const js_src_file_stat = try js_src_file.stat(); 686 | const js_src_bytes = try js_src_file.readToEndAlloc( 687 | gen_web_files.builder.allocator, 688 | js_src_file_stat.size, 689 | ); 690 | defer gen_web_files.builder.allocator.free(js_src_bytes); 691 | 692 | try js_contents.appendSlice(js_src_bytes[0..]); 693 | try js_contents.appendSlice("\n"); 694 | } 695 | 696 | if (gen_web_files.bake_level.? == .dbg) { 697 | try std.fs.cwd().writeFile( 698 | gen_web_files.js_file.getPath(), 699 | js_contents.items, 700 | ); 701 | } else { 702 | const js_src_bytes_min = try minify.js( 703 | gen_web_files.builder.allocator, 704 | js_contents.items, 705 | gen_web_files.bake_level.?, 706 | ); 707 | defer gen_web_files.builder.allocator.free(js_src_bytes_min); 708 | try std.fs.cwd().writeFile( 709 | gen_web_files.js_file.getPath(), 710 | js_src_bytes_min, 711 | ); 712 | } 713 | } 714 | }; 715 | 716 | const HeaderLib = struct { 717 | path: []const u8, 718 | define: []const u8, 719 | }; 720 | 721 | const GenerateHeaderImpl = struct { 722 | builder: *std.build.Builder, 723 | step: std.build.Step, 724 | file: std.build.GeneratedFile, 725 | libs: []const HeaderLib, 726 | 727 | pub fn init( 728 | builder: *std.build.Builder, 729 | libs: []const HeaderLib, 730 | ) !*GenerateHeaderImpl { 731 | const header_lib_impl = try builder.allocator.create(GenerateHeaderImpl); 732 | header_lib_impl.* = .{ 733 | .builder = builder, 734 | .step = std.build.Step.init( 735 | .custom, 736 | "make header impl", 737 | builder.allocator, 738 | make, 739 | ), 740 | .file = std.build.GeneratedFile{ .step = &header_lib_impl.step }, 741 | .libs = libs, 742 | }; 743 | return header_lib_impl; 744 | } 745 | 746 | fn make(step: *std.build.Step) !void { 747 | const header_lib_impl = @fieldParentPtr(GenerateHeaderImpl, "step", step); 748 | const allocator = header_lib_impl.builder.allocator; 749 | 750 | var contents = std.ArrayList(u8).init(allocator); 751 | defer contents.deinit(); 752 | 753 | const writer = contents.writer(); 754 | for (header_lib_impl.libs) |lib| { 755 | const path = try getPathString(allocator, lib.path); 756 | try writer.print("#define {s}\n", .{lib.define}); 757 | try writer.print("#include \"{s}\"\n", .{path}); 758 | } 759 | 760 | const write_path = try std.fs.path.join(allocator, &.{ 761 | try getCacheDir(header_lib_impl.builder), 762 | "header_libs_impl.c", 763 | }); 764 | try std.fs.cwd().writeFile(write_path, contents.items); 765 | 766 | header_lib_impl.file.path = write_path; 767 | } 768 | }; 769 | -------------------------------------------------------------------------------- /demo/cube/build.zig: -------------------------------------------------------------------------------- 1 | const build_cc = @import("../../build.zig"); 2 | const std = @import("std"); 3 | 4 | pub fn build(builder: *std.build.Builder) !void { 5 | const options = build_cc.Options.init(builder); 6 | const dest_dir = try build_cc.getDestDir(builder, options, "cube"); 7 | const recipe = build_cc.Recipe{ 8 | .dir = "demo/cube", 9 | .items = &.{ 10 | .{ 11 | .id = "cube_vert_shader", 12 | .output = .pkg_embed, 13 | .bake_type = "shader", 14 | .deps = &.{"cube_vert.wgsl"}, 15 | }, 16 | .{ 17 | .id = "cube_frag_shader", 18 | .output = .pkg_embed, 19 | .bake_type = "shader", 20 | .deps = &.{"cube_frag.wgsl"}, 21 | }, 22 | }, 23 | }; 24 | const cube_pkg = std.build.Pkg{ 25 | .name = "cube", 26 | .path = .{ .path = "demo/cube/cube.zig" }, 27 | .dependencies = &.{ 28 | build_cc.getBakePkg(builder, options, "cube", dest_dir, recipe), 29 | build_cc.getGfxPkg(), 30 | build_cc.getMathPkg(), 31 | build_cc.getResPkg(), 32 | build_cc.getTimePkg(), 33 | build_cc.getWndPkg(), 34 | build_cc.getWndGfxPkg(), 35 | }, 36 | }; 37 | _ = try build_cc.initMainLibExe(builder, options, dest_dir, cube_pkg); 38 | } 39 | -------------------------------------------------------------------------------- /demo/cube/cube.zig: -------------------------------------------------------------------------------- 1 | const cc_bake = @import("cc_bake"); 2 | const cc_gfx = @import("cc_gfx"); 3 | const cc_math = @import("cc_math"); 4 | const cc_res = @import("cc_res"); 5 | const cc_time = @import("cc_time"); 6 | const cc_wnd = @import("cc_wnd"); 7 | const cc_wnd_gfx = @import("cc_wnd_gfx"); 8 | const std = @import("std"); 9 | 10 | const Vertex = struct { 11 | position: [4]f32, 12 | color: [4]f32, 13 | }; 14 | 15 | const Uniforms = struct { 16 | mvp: [16]f32, 17 | }; 18 | 19 | const vertices: []const Vertex = &.{ 20 | // front 21 | .{ .position = [_]f32{ -1, -1, -1, 1 }, .color = [_]f32{ 1.0, 1.0, 1.0, 1 } }, 22 | .{ .position = [_]f32{ 1, -1, -1, 1 }, .color = [_]f32{ 0.71, 1.0, 1.0, 1 } }, 23 | .{ .position = [_]f32{ -1, 1, -1, 1 }, .color = [_]f32{ 1.0, 0.71, 1.0, 1 } }, 24 | .{ .position = [_]f32{ 1, 1, -1, 1 }, .color = [_]f32{ 0.71, 0.71, 1.0, 1 } }, 25 | // back 26 | .{ .position = [_]f32{ -1, -1, 1, 1 }, .color = [_]f32{ 1.0, 1.0, 0.71, 1 } }, 27 | .{ .position = [_]f32{ 1, -1, 1, 1 }, .color = [_]f32{ 0.71, 1.0, 0.71, 1 } }, 28 | .{ .position = [_]f32{ -1, 1, 1, 1 }, .color = [_]f32{ 1.0, 0.71, 0.71, 1 } }, 29 | .{ .position = [_]f32{ 1, 1, 1, 1 }, .color = [_]f32{ 0.71, 0.71, 0.71, 1 } }, 30 | }; 31 | 32 | const indices: []const u16 = &.{ 33 | 0, 1, 2, 2, 1, 3, // front 34 | 2, 3, 6, 6, 3, 7, // top 35 | 1, 5, 3, 3, 5, 7, // right 36 | 4, 5, 0, 0, 5, 1, // bottom 37 | 4, 0, 6, 6, 0, 2, // left 38 | 5, 4, 7, 7, 4, 6, // back 39 | }; 40 | 41 | const Demo = struct { 42 | window: cc_wnd.Window, 43 | gctx: cc_gfx.Context, 44 | vertex_buffer: cc_gfx.Buffer, 45 | index_buffer: cc_gfx.Buffer, 46 | uniform_buffer: cc_gfx.Buffer, 47 | bind_group: cc_gfx.BindGroup, 48 | render_pipeline: cc_gfx.RenderPipeline, 49 | uniforms: Uniforms, 50 | game_clock: cc_time.Timer, 51 | }; 52 | 53 | pub fn init() !Demo { 54 | var window = try cc_wnd.Window.init(.{ 55 | .width = 800, 56 | .height = 600, 57 | .title = "cube", 58 | }); 59 | var gctx = try cc_gfx.Context.init(cc_wnd_gfx.getContextDesc(window)); 60 | 61 | const vertex_buffer = try gctx.device.initBufferSlice( 62 | vertices, 63 | .{ .vertex = true }, 64 | ); 65 | const index_buffer = try gctx.device.initBufferSlice( 66 | indices, 67 | .{ .index = true }, 68 | ); 69 | const uniform_buffer = try gctx.device.initBuffer(.{ 70 | .size = @sizeOf(Uniforms), 71 | .usage = .{ .uniform = true, .copy_dst = true }, 72 | }); 73 | 74 | var bind_group_layout = try gctx.device.initBindGroupLayout(.{ 75 | .entries = &.{.{ 76 | .binding = 0, 77 | .visibility = .{ .vertex = true }, 78 | .buffer = .{}, 79 | }}, 80 | }); 81 | defer gctx.device.deinitBindGroupLayout(&bind_group_layout); 82 | 83 | const bind_group = try gctx.device.initBindGroup(.{ 84 | .layout = &bind_group_layout, 85 | // todo: zig #7607 86 | .entries = &[_]cc_gfx.BindGroupEntry{.{ 87 | .binding = 0, 88 | .resource = .{ .buffer_binding = .{ .buffer = &uniform_buffer } }, 89 | }}, 90 | }); 91 | 92 | var pipeline_layout = try gctx.device.initPipelineLayout( 93 | .{ .bind_group_layouts = &.{bind_group_layout} }, 94 | ); 95 | defer gctx.device.deinitPipelineLayout(&pipeline_layout); 96 | 97 | const vert_shader_desc = try cc_res.load(cc_bake.cube_vert_shader, .{}); 98 | var vert_shader = try gctx.device.initShader(vert_shader_desc); 99 | defer gctx.device.deinitShader(&vert_shader); 100 | 101 | const frag_shader_desc = try cc_res.load(cc_bake.cube_frag_shader, .{}); 102 | var frag_shader = try gctx.device.initShader(frag_shader_desc); 103 | defer gctx.device.deinitShader(&frag_shader); 104 | 105 | var pipeline_desc = cc_gfx.RenderPipelineDesc{}; 106 | pipeline_desc.setPipelineLayout(&pipeline_layout); 107 | pipeline_desc.setVertexState(.{ 108 | .module = &vert_shader, 109 | .entry_point = "vs_main", 110 | // todo: zig #7607 111 | .buffers = &[_]cc_gfx.VertexBufferLayout{ 112 | cc_gfx.getVertexBufferLayoutStruct(Vertex, .vertex, 0), 113 | }, 114 | }); 115 | pipeline_desc.setPrimitiveState(.{ .cull_mode = .back }); 116 | pipeline_desc.setDepthStencilState(.{ 117 | .format = gctx.depth_texture_format, 118 | .depth_write_enabled = true, 119 | .depth_compare = .less, 120 | }); 121 | pipeline_desc.setFragmentState(.{ 122 | .module = &frag_shader, 123 | .entry_point = "fs_main", 124 | // todo: zig #7607 125 | .targets = &[_]cc_gfx.ColorTargetState{ 126 | .{ .format = gctx.swapchain_format }, 127 | }, 128 | }); 129 | 130 | const pipeline = try gctx.device.initRenderPipeline(pipeline_desc); 131 | 132 | const game_clock = try cc_time.Timer.start(); 133 | 134 | return Demo{ 135 | .window = window, 136 | .gctx = gctx, 137 | .vertex_buffer = vertex_buffer, 138 | .index_buffer = index_buffer, 139 | .uniform_buffer = uniform_buffer, 140 | .bind_group = bind_group, 141 | .render_pipeline = pipeline, 142 | .uniforms = .{ .mvp = [_]f32{0} ** 16 }, 143 | .game_clock = game_clock, 144 | }; 145 | } 146 | 147 | pub fn loop(demo: *Demo) !void { 148 | if (!demo.window.isVisible()) { 149 | return; 150 | } 151 | 152 | const time = demo.game_clock.readSeconds(); 153 | const model = cc_math.matFromAxisAngle( 154 | cc_math.f32x4(cc_math.sin(time), cc_math.cos(time), 0.0, 0.0), 155 | 1.0, 156 | ); 157 | const view = cc_math.lookToLh( 158 | cc_math.f32x4(0, 0, -4, 0), 159 | cc_math.f32x4(0, 0, 1, 0), 160 | cc_math.f32x4(0, 1, 0, 0), 161 | ); 162 | const proj = cc_math.perspectiveFovLh( 163 | 2.0 * std.math.pi / 5.0, 164 | demo.window.getAspectRatio(), 165 | 1, 166 | 100, 167 | ); 168 | const mvp = cc_math.transpose(cc_math.mul(cc_math.mul(model, view), proj)); 169 | cc_math.storeMat(&demo.uniforms.mvp, mvp); 170 | 171 | var queue = demo.gctx.device.getQueue(); 172 | try queue.writeBuffer( 173 | &demo.uniform_buffer, 174 | 0, 175 | std.mem.asBytes(&demo.uniforms), 176 | 0, 177 | ); 178 | 179 | const swapchain_view = try demo.gctx.swapchain.getCurrentTextureView(); 180 | var command_encoder = try demo.gctx.device.initCommandEncoder(); 181 | 182 | var render_pass_desc = cc_gfx.RenderPassDesc{}; 183 | // todo: zig #7607 184 | render_pass_desc.setColorAttachments(&[_]cc_gfx.ColorAttachment{.{ 185 | .view = &swapchain_view, 186 | .load_op = .clear, 187 | .clear_value = demo.gctx.clear_color, 188 | .store_op = .store, 189 | }}); 190 | render_pass_desc.setDepthStencilAttachment(.{ 191 | .view = &demo.gctx.depth_texture_view, 192 | .depth_clear_value = 1.0, 193 | .depth_load_op = .clear, 194 | .depth_store_op = .store, 195 | }); 196 | 197 | var render_pass = try command_encoder.beginRenderPass(render_pass_desc); 198 | try render_pass.setPipeline(&demo.render_pipeline); 199 | try render_pass.setBindGroup(0, &demo.bind_group, null); 200 | try render_pass.setVertexBuffer(0, &demo.vertex_buffer, 0, cc_gfx.whole_size); 201 | try render_pass.setIndexBuffer( 202 | &demo.index_buffer, 203 | .uint16, 204 | 0, 205 | cc_gfx.whole_size, 206 | ); 207 | try render_pass.drawIndexed(indices.len, 1, 0, 0, 0); 208 | try render_pass.end(); 209 | 210 | try queue.submit(&.{try command_encoder.finish()}); 211 | try demo.gctx.swapchain.present(); 212 | } 213 | 214 | pub fn deinit(demo: *Demo) !void { 215 | demo.gctx.device.deinitRenderPipeline(&demo.render_pipeline); 216 | demo.gctx.device.deinitBindGroup(&demo.bind_group); 217 | demo.gctx.device.deinitBuffer(&demo.uniform_buffer); 218 | demo.gctx.device.deinitBuffer(&demo.index_buffer); 219 | demo.gctx.device.deinitBuffer(&demo.vertex_buffer); 220 | demo.gctx.deinit(); 221 | demo.window.deinit(); 222 | } 223 | -------------------------------------------------------------------------------- /demo/cube/cube_frag.wgsl: -------------------------------------------------------------------------------- 1 | @fragment fn fs_main(@location(0) _color: vec4) -> @location(0) vec4 { 2 | return _color; 3 | } 4 | -------------------------------------------------------------------------------- /demo/cube/cube_vert.wgsl: -------------------------------------------------------------------------------- 1 | struct _VertexInput { 2 | @location(0) _pos: vec4, 3 | @location(1) _color: vec4, 4 | }; 5 | 6 | struct _VertexOutput { 7 | @builtin(position) _pos : vec4, 8 | @location(0) _color: vec4, 9 | }; 10 | 11 | struct _Uniforms { 12 | _mvp : mat4x4, 13 | }; 14 | 15 | @group(0) @binding(0) var _uniforms : _Uniforms; 16 | 17 | @vertex fn vs_main(_in: _VertexInput) -> _VertexOutput { 18 | var _out : _VertexOutput; 19 | _out._pos = _in._pos * _uniforms._mvp; 20 | _out._color = _in._color; 21 | return _out; 22 | } 23 | -------------------------------------------------------------------------------- /demo/tri/build.zig: -------------------------------------------------------------------------------- 1 | const build_cc = @import("../../build.zig"); 2 | const std = @import("std"); 3 | 4 | pub fn build(builder: *std.build.Builder) !void { 5 | const options = build_cc.Options.init(builder); 6 | const dest_dir = try build_cc.getDestDir(builder, options, "tri"); 7 | const recipe = build_cc.Recipe{ 8 | .dir = "demo/tri", 9 | .items = &.{ 10 | .{ 11 | .id = "tri_vert_shader", 12 | .output = .pkg_embed, 13 | .bake_type = "shader", 14 | .deps = &.{"tri_vert.wgsl"}, 15 | }, 16 | .{ 17 | .id = "tri_frag_shader", 18 | .output = .pkg_embed, 19 | .bake_type = "shader", 20 | .deps = &.{"tri_frag.wgsl"}, 21 | }, 22 | }, 23 | }; 24 | const tri_pkg = std.build.Pkg{ 25 | .name = "tri", 26 | .path = .{ .path = "demo/tri/tri.zig" }, 27 | .dependencies = &.{ 28 | build_cc.getBakePkg(builder, options, "tri", dest_dir, recipe), 29 | build_cc.getGfxPkg(), 30 | build_cc.getResPkg(), 31 | build_cc.getWndPkg(), 32 | build_cc.getWndGfxPkg(), 33 | }, 34 | }; 35 | _ = try build_cc.initMainLibExe(builder, options, dest_dir, tri_pkg); 36 | } 37 | -------------------------------------------------------------------------------- /demo/tri/tri.zig: -------------------------------------------------------------------------------- 1 | const cc_bake = @import("cc_bake"); 2 | const cc_gfx = @import("cc_gfx"); 3 | const cc_res = @import("cc_res"); 4 | const cc_wnd = @import("cc_wnd"); 5 | const cc_wnd_gfx = @import("cc_wnd_gfx"); 6 | 7 | const Demo = struct { 8 | window: cc_wnd.Window, 9 | gctx: cc_gfx.Context, 10 | render_pipeline: cc_gfx.RenderPipeline, 11 | }; 12 | 13 | pub fn init() !Demo { 14 | var window = try cc_wnd.Window.init(.{ 15 | .width = 800, 16 | .height = 600, 17 | .title = "tri", 18 | }); 19 | var gctx = try cc_gfx.Context.init(cc_wnd_gfx.getContextDesc(window)); 20 | 21 | const vert_shader_desc = try cc_res.load(cc_bake.tri_vert_shader, .{}); 22 | var vert_shader = try gctx.device.initShader(vert_shader_desc); 23 | defer gctx.device.deinitShader(&vert_shader); 24 | 25 | const frag_shader_desc = try cc_res.load(cc_bake.tri_frag_shader, .{}); 26 | var frag_shader = try gctx.device.initShader(frag_shader_desc); 27 | defer gctx.device.deinitShader(&frag_shader); 28 | 29 | var pipeline_desc = cc_gfx.RenderPipelineDesc{}; 30 | pipeline_desc.setVertexState(.{ 31 | .module = &vert_shader, 32 | .entry_point = "vs_main", 33 | }); 34 | pipeline_desc.setFragmentState(.{ 35 | .module = &frag_shader, 36 | .entry_point = "fs_main", 37 | // todo: zig #7607 38 | .targets = &[_]cc_gfx.ColorTargetState{ 39 | .{ .format = gctx.swapchain_format }, 40 | }, 41 | }); 42 | const pipeline = try gctx.device.initRenderPipeline(pipeline_desc); 43 | 44 | return Demo{ .window = window, .gctx = gctx, .render_pipeline = pipeline }; 45 | } 46 | 47 | pub fn loop(demo: *Demo) !void { 48 | if (!demo.window.isVisible()) { 49 | return; 50 | } 51 | 52 | const swapchain_view = try demo.gctx.swapchain.getCurrentTextureView(); 53 | var command_encoder = try demo.gctx.device.initCommandEncoder(); 54 | 55 | var render_pass_desc = cc_gfx.RenderPassDesc{}; 56 | // todo: zig #7607 57 | render_pass_desc.setColorAttachments(&[_]cc_gfx.ColorAttachment{.{ 58 | .view = &swapchain_view, 59 | .load_op = .clear, 60 | .clear_value = demo.gctx.clear_color, 61 | .store_op = .store, 62 | }}); 63 | 64 | var render_pass = try command_encoder.beginRenderPass(render_pass_desc); 65 | try render_pass.setPipeline(&demo.render_pipeline); 66 | try render_pass.draw(3, 1, 0, 0); 67 | try render_pass.end(); 68 | 69 | try demo.gctx.device.getQueue().submit(&.{try command_encoder.finish()}); 70 | try demo.gctx.swapchain.present(); 71 | } 72 | 73 | pub fn deinit(demo: *Demo) !void { 74 | demo.gctx.device.deinitRenderPipeline(&demo.render_pipeline); 75 | demo.gctx.deinit(); 76 | demo.window.deinit(); 77 | } 78 | -------------------------------------------------------------------------------- /demo/tri/tri_frag.wgsl: -------------------------------------------------------------------------------- 1 | @fragment fn fs_main() -> @location(0) vec4 { 2 | return vec4(0.89, 0.79, 1.0, 1.0); 3 | } 4 | -------------------------------------------------------------------------------- /demo/tri/tri_vert.wgsl: -------------------------------------------------------------------------------- 1 | @vertex fn vs_main(@builtin(vertex_index) _in_vertex_index: u32) -> @builtin(position) vec4 { 2 | let _x = f32(i32(_in_vertex_index) - 1); 3 | let _y = f32(i32(_in_vertex_index & 1u) * 2 - 1); 4 | return vec4(_x, _y, 0.0, 1.0); 5 | } 6 | -------------------------------------------------------------------------------- /demo/ui/build.zig: -------------------------------------------------------------------------------- 1 | const bake = @import("../../src/bake.zig"); 2 | const build_cc = @import("../../build.zig"); 3 | const std = @import("std"); 4 | 5 | pub fn build(builder: *std.build.Builder) !void { 6 | const options = build_cc.Options.init(builder); 7 | const dest_dir = try build_cc.getDestDir(builder, options, "ui"); 8 | const ui_pkg = std.build.Pkg{ 9 | .name = "ui", 10 | .path = .{ .path = "demo/ui/ui.zig" }, 11 | .dependencies = &.{ 12 | build_cc.getGfxPkg(), 13 | build_cc.getMemPkg(), 14 | build_cc.getUiPkg(), 15 | build_cc.getUiGfxPkg(), 16 | build_cc.getUiResPkg(builder, options), 17 | build_cc.getWndPkg(), 18 | build_cc.getWndGfxPkg(), 19 | }, 20 | }; 21 | _ = try build_cc.initMainLibExe(builder, options, dest_dir, ui_pkg); 22 | } 23 | -------------------------------------------------------------------------------- /demo/ui/ui.zig: -------------------------------------------------------------------------------- 1 | const cc_gfx = @import("cc_gfx"); 2 | const cc_mem = @import("cc_mem"); 3 | const cc_ui = @import("cc_ui"); 4 | const cc_ui_gfx = @import("cc_ui_gfx"); 5 | const cc_ui_res = @import("cc_ui_res"); 6 | const cc_wnd = @import("cc_wnd"); 7 | const cc_wnd_gfx = @import("cc_wnd_gfx"); 8 | 9 | const Demo = struct { 10 | ba: cc_mem.BumpAllocator, 11 | window: cc_wnd.Window, 12 | gctx: cc_gfx.Context, 13 | uictx: cc_ui.Context(cc_ui_gfx), 14 | }; 15 | 16 | const max_instances = 256; 17 | 18 | pub fn init() !Demo { 19 | var ba = try cc_mem.BumpAllocator.init(256 * 1024); 20 | const allocator = ba.allocator(); 21 | const window = try cc_wnd.Window.init(.{ 22 | .width = 800, 23 | .height = 600, 24 | .title = "ui", 25 | }); 26 | var gctx = try cc_gfx.Context.init(cc_wnd_gfx.getContextDesc(window)); 27 | const uictx = try cc_ui.Context(cc_ui_gfx).init( 28 | .{ 29 | .device = &gctx.device, 30 | .format = gctx.swapchain_format, 31 | .max_instances = max_instances, 32 | .allocator = allocator, 33 | }, 34 | cc_ui_res, 35 | ); 36 | 37 | return Demo{ .ba = ba, .window = window, .gctx = gctx, .uictx = uictx }; 38 | } 39 | 40 | pub fn loop(demo: *Demo) !void { 41 | if (!demo.window.isVisible()) { 42 | return; 43 | } 44 | 45 | demo.uictx.reset(); 46 | demo.uictx.setViewport( 47 | @intToFloat(f32, demo.window.getWidth()), 48 | @intToFloat(f32, demo.window.getHeight()), 49 | ); 50 | try demo.uictx.debugText(.{}, "Hello, world!", .{}); 51 | 52 | const swapchain_view = try demo.gctx.swapchain.getCurrentTextureView(); 53 | var command_encoder = try demo.gctx.device.initCommandEncoder(); 54 | 55 | var render_pass_desc = cc_gfx.RenderPassDesc{}; 56 | render_pass_desc.setColorAttachments(&[_]cc_gfx.ColorAttachment{.{ 57 | .view = &swapchain_view, 58 | .load_op = .clear, 59 | .clear_value = demo.gctx.clear_color, 60 | .store_op = .store, 61 | }}); 62 | var render_pass = try command_encoder.beginRenderPass(render_pass_desc); 63 | try demo.uictx.render(&render_pass); 64 | try render_pass.end(); 65 | 66 | try demo.gctx.device.getQueue().submit(&.{try command_encoder.finish()}); 67 | try demo.gctx.swapchain.present(); 68 | } 69 | 70 | pub fn deinit(demo: *Demo) !void { 71 | demo.uictx.deinit(); 72 | demo.gctx.deinit(); 73 | demo.window.deinit(); 74 | demo.ba.deinit(); 75 | } 76 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 bootra 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | cupcake is an app framework for making small and delicious games! (very wip) 2 | 3 | At the moment, it's just my personal place to doodle around with game development in zig. Don't expect any sort of usability, documentation, or code quality! 4 | 5 | ### goals 6 | 7 | _web first_ 8 | 9 | Web pages are easily sharable, work on most devices, and are one of the most constrained platforms for applications. Porting to other platforms later on should be easier. 10 | 11 | _small binaries_ 12 | 13 | Binary size is important for the web because it affects the responsiveness of page loads and bandwidth costs. The application binary should strive to be small and performant. 14 | 15 | _simple code_ 16 | 17 | The best way to end up with a small binary is to focus on simple code. When complexity is necessary, try to move it to compile time or build time. 18 | 19 | _minimal dependencies_ 20 | 21 | External dependencies are one of the biggest contributors to large binary sizes. Replace complex third party libraries with simpler pieces of handwritten code when reasonable. 22 | 23 | ### contact 24 | if you have any questions or comments, contact me on the zig discord. i am happy to chat! 25 | 26 | -bootra 27 | -------------------------------------------------------------------------------- /res/ui_dbg_font.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootradev/cupcake/c56c23c59b93832d78f33c384dc1a1428a2c166c/res/ui_dbg_font.png -------------------------------------------------------------------------------- /src/bake.zig: -------------------------------------------------------------------------------- 1 | const bake_list = @import("bake_list"); 2 | const serde = @import("serde.zig"); 3 | const std = @import("std"); 4 | 5 | pub fn main() !void { 6 | var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{}; 7 | defer _ = gpa.deinit(); 8 | const allocator = gpa.allocator(); 9 | 10 | std.log.info("bake begin...", .{}); 11 | std.log.info("platform: {s}", .{@tagName(bake_list.platform)}); 12 | std.log.info("optimization level: {s}", .{@tagName(bake_list.opt_level)}); 13 | 14 | var in_dir = try std.fs.cwd().openDir(bake_list.in_dir, .{}); 15 | defer in_dir.close(); 16 | var out_dir = try std.fs.cwd().openDir(bake_list.out_dir, .{}); 17 | defer out_dir.close(); 18 | 19 | std.log.info("input dir: {s}", .{bake_list.in_dir}); 20 | std.log.info("output dir: {s}", .{bake_list.out_dir}); 21 | 22 | var deps = std.ArrayList([]u8).init(allocator); 23 | defer deps.deinit(); 24 | 25 | var pkg_contents = std.ArrayList(u8).init(allocator); 26 | defer pkg_contents.deinit(); 27 | const writer = pkg_contents.writer(); 28 | inline for (@typeInfo(bake_list.pkgs).Struct.decls) |decl| { 29 | try writer.print( 30 | "pub const {s} = @import(\"{s}\");\n", 31 | .{ decl.name, decl.name }, 32 | ); 33 | } 34 | 35 | inline for (@typeInfo(bake_list.items).Struct.decls) |decl| { 36 | const item = @field(bake_list.items, decl.name); 37 | 38 | std.log.info("{s} {s}", .{ item.bake_type, decl.name }); 39 | 40 | inline for (item.deps) |dep| { 41 | const is_id = comptime std.mem.indexOfScalar(u8, dep, '.') == null; 42 | const dir = if (is_id) out_dir else in_dir; 43 | const file = try dir.openFile(dep, .{}); 44 | defer file.close(); 45 | const file_stat = try file.stat(); 46 | const file_bytes = try file.readToEndAlloc(allocator, file_stat.size); 47 | try deps.append(file_bytes); 48 | } 49 | 50 | const bake_pkg = @field(bake_list.pkgs, item.bake_pkg); 51 | const bake_fn = @field(bake_pkg, item.bake_type ++ "Bake"); 52 | const BakeType = comptime block: { 53 | const return_type = @typeInfo(@TypeOf(bake_fn)).Fn.return_type.?; 54 | switch (@typeInfo(return_type)) { 55 | .ErrorUnion => |EU| break :block EU.payload, 56 | else => @compileError( 57 | "bake fn return type must be an error union!", 58 | ), 59 | } 60 | }; 61 | const bake_result = try bake_fn( 62 | allocator, 63 | deps.items, 64 | bake_list.platform, 65 | bake_list.opt_level, 66 | ); 67 | defer if (@hasDecl(bake_pkg, item.bake_type ++ "BakeFree")) { 68 | const bake_free_fn = @field(bake_pkg, item.bake_type ++ "BakeFree"); 69 | bake_free_fn(allocator, bake_result); 70 | }; 71 | const bake_bytes = try serde.serialize(allocator, bake_result); 72 | defer allocator.free(bake_bytes); 73 | 74 | try out_dir.writeFile(decl.name, bake_bytes); 75 | 76 | if (item.output != .cache) { 77 | try writer.print("pub const {s} = .{{\n", .{decl.name}); 78 | try writer.print(" ", .{}); 79 | try writer.print( 80 | ".Type = {s}.{s},\n", 81 | .{ item.bake_pkg, @typeName(BakeType) }, 82 | ); 83 | if (item.output == .pkg_embed) { 84 | try writer.print(" ", .{}); 85 | try writer.print( 86 | ".data = .{{ .embed = @embedFile(\"{s}\") }},\n", 87 | .{decl.name}, 88 | ); 89 | } else { 90 | try writer.print(" ", .{}); 91 | try writer.print( 92 | ".data = .{{ .file = .{{ .path = \"{s}\", .size = {} }} }},\n", 93 | .{ decl.name, bake_bytes.len }, 94 | ); 95 | } 96 | try writer.print("}};\n", .{}); 97 | } 98 | 99 | for (deps.items) |dep| { 100 | allocator.free(dep); 101 | } 102 | deps.clearRetainingCapacity(); 103 | } 104 | 105 | try out_dir.writeFile("bake_pkg.zig", pkg_contents.items); 106 | 107 | std.log.info("bake complete!", .{}); 108 | } 109 | -------------------------------------------------------------------------------- /src/bake_types.zig: -------------------------------------------------------------------------------- 1 | const cfg = @import("cfg.zig"); 2 | const gfx = @import("gfx.zig"); 3 | const minify = @import("minify.zig"); 4 | const stb = @cImport({ 5 | @cInclude("stb/stb_image.h"); 6 | }); 7 | const std = @import("std"); 8 | 9 | pub const ShaderDesc = gfx.ShaderDesc; 10 | 11 | pub fn shaderBake( 12 | allocator: std.mem.Allocator, 13 | deps: []const []const u8, 14 | platform: cfg.Platform, 15 | opt_level: cfg.OptLevel, 16 | ) !ShaderDesc { 17 | const shader_bytes = try minify.shader( 18 | allocator, 19 | deps[0], 20 | platform, 21 | opt_level, 22 | ); 23 | return ShaderDesc{ .bytes = shader_bytes }; 24 | } 25 | 26 | pub fn shaderBakeFree(allocator: std.mem.Allocator, shader: ShaderDesc) void { 27 | allocator.free(shader.bytes); 28 | } 29 | 30 | pub const TextureDesc = gfx.TextureDesc; 31 | 32 | pub fn textureBake( 33 | allocator: std.mem.Allocator, 34 | deps: []const []const u8, 35 | _: cfg.Platform, 36 | _: cfg.OptLevel, 37 | ) !TextureDesc { 38 | var width: c_int = undefined; 39 | var height: c_int = undefined; 40 | var channels: c_int = undefined; 41 | const desired_channels: c_int = 4; // todo: add option to configure this 42 | const texture_bytes = stb.stbi_load_from_memory( 43 | deps[0].ptr, 44 | @intCast(c_int, deps[0].len), 45 | &width, 46 | &height, 47 | &channels, 48 | desired_channels, 49 | ); 50 | defer stb.stbi_image_free(texture_bytes); 51 | const texture_bytes_len = @intCast(usize, width * height * desired_channels); 52 | const texture_bytes_slice = texture_bytes[0..texture_bytes_len]; 53 | 54 | return TextureDesc{ 55 | .size = .{ .width = @intCast(u32, width), .height = @intCast(u32, height) }, 56 | .format = .rgba8unorm, 57 | .bytes = try allocator.dupe(u8, texture_bytes_slice), 58 | }; 59 | } 60 | 61 | pub fn textureBakeFree(allocator: std.mem.Allocator, texture: TextureDesc) void { 62 | allocator.free(texture.bytes.?); 63 | } 64 | -------------------------------------------------------------------------------- /src/cfg.zig: -------------------------------------------------------------------------------- 1 | const builtin = @import("builtin"); 2 | const std = @import("std"); 3 | 4 | pub const Platform = enum { 5 | web, 6 | win, 7 | }; 8 | 9 | pub const platform: Platform = block: { 10 | if (builtin.target.cpu.arch.isWasm()) { 11 | break :block .web; 12 | } else { 13 | switch (builtin.target.os.tag) { 14 | .windows => break :block .win, 15 | else => @compileError("Invalid platform!"), 16 | } 17 | } 18 | }; 19 | 20 | // todo: add support for profile mode enabled by root 21 | pub const OptLevel = enum { 22 | dbg, 23 | rel, 24 | }; 25 | 26 | pub const opt_level: OptLevel = switch (builtin.mode) { 27 | .Debug => .dbg, 28 | else => .rel, 29 | }; 30 | -------------------------------------------------------------------------------- /src/gfx.zig: -------------------------------------------------------------------------------- 1 | const api = switch (cfg.platform) { 2 | .web => @import("gfx_web.zig"), 3 | .win => @compileError("Not yet implemented!"), 4 | }; 5 | const cfg = @import("cfg.zig"); 6 | const serde = @import("serde.zig"); 7 | const std = @import("std"); 8 | const qoi = @import("qoi.zig"); 9 | 10 | pub const ContextDesc = struct { 11 | surface_desc: SurfaceDesc, 12 | adapter_desc: AdapterDesc = .{}, 13 | device_desc: DeviceDesc = .{}, 14 | swapchain_size: Extent3d, 15 | }; 16 | 17 | pub const Context = struct { 18 | instance: Instance, 19 | surface: Surface, 20 | adapter: Adapter, 21 | device: Device, 22 | swapchain: Swapchain, 23 | swapchain_format: TextureFormat, 24 | clear_color: Color, 25 | depth_texture: Texture, 26 | depth_texture_format: TextureFormat, 27 | depth_texture_view: TextureView, 28 | 29 | pub fn init(desc: ContextDesc) !Context { 30 | var instance = try Instance.init(); 31 | var surface = try instance.initSurface(desc.surface_desc); 32 | var adapter = try instance.initAdapter(desc.adapter_desc); 33 | var device = try adapter.initDevice(desc.device_desc); 34 | const swapchain_format = try surface.getPreferredFormat(); 35 | const swapchain = try device.initSwapchain(&surface, .{ 36 | .size = desc.swapchain_size, 37 | .format = swapchain_format, 38 | }); 39 | const clear_color = .{ .r = 0.32, .g = 0.1, .b = 0.18, .a = 1.0 }; 40 | const depth_texture_format = .depth24plus; 41 | var depth_texture = try device.initTexture( 42 | .{ .size = desc.swapchain_size, .format = depth_texture_format }, 43 | .{ .render_attachment = true }, 44 | ); 45 | const depth_texture_view = try device.initTextureView(.{ 46 | .texture = &depth_texture, 47 | .format = depth_texture_format, 48 | }); 49 | 50 | return Context{ 51 | .instance = instance, 52 | .surface = surface, 53 | .adapter = adapter, 54 | .device = device, 55 | .swapchain = swapchain, 56 | .swapchain_format = swapchain_format, 57 | .clear_color = clear_color, 58 | .depth_texture = depth_texture, 59 | .depth_texture_format = depth_texture_format, 60 | .depth_texture_view = depth_texture_view, 61 | }; 62 | } 63 | 64 | pub fn deinit(ctx: *Context) void { 65 | ctx.device.deinitTextureView(&ctx.depth_texture_view); 66 | ctx.device.deinitTexture(&ctx.depth_texture); 67 | ctx.device.deinitSwapchain(&ctx.swapchain); 68 | ctx.adapter.deinitDevice(&ctx.device); 69 | ctx.instance.deinitAdapter(&ctx.adapter); 70 | ctx.instance.deinitSurface(&ctx.surface); 71 | ctx.instance.deinit(); 72 | } 73 | }; 74 | 75 | pub const Instance = struct { 76 | impl: api.Instance, 77 | 78 | pub fn init() !Instance { 79 | return Instance{ .impl = try api.Instance.init() }; 80 | } 81 | 82 | pub fn deinit(instance: *Instance) void { 83 | instance.impl.deinit(); 84 | } 85 | 86 | pub fn initSurface(instance: *Instance, desc: SurfaceDesc) !Surface { 87 | return Surface{ .impl = try instance.impl.initSurface(desc) }; 88 | } 89 | 90 | pub fn deinitSurface(instance: *Instance, surface: *Surface) void { 91 | instance.impl.deinitSurface(&surface.impl); 92 | } 93 | 94 | pub fn initAdapter(instance: *Instance, desc: AdapterDesc) !Adapter { 95 | return Adapter{ .impl = try instance.impl.initAdapter(desc) }; 96 | } 97 | 98 | pub fn deinitAdapter(instance: *Instance, adapter: *Adapter) void { 99 | instance.impl.deinitAdapter(&adapter.impl); 100 | } 101 | }; 102 | 103 | const CanvasWindowInfo = struct { 104 | canvas_id: []const u8, 105 | }; 106 | 107 | const SurfaceWindowInfo: type = switch (cfg.platform) { 108 | .web => CanvasWindowInfo, 109 | else => @compileError("Unsupported platform!"), 110 | }; 111 | 112 | pub const SurfaceDesc = struct { 113 | window_info: SurfaceWindowInfo, 114 | }; 115 | 116 | pub const Surface = struct { 117 | impl: api.Surface, 118 | 119 | pub fn getPreferredFormat(surface: *Surface) !TextureFormat { 120 | return try surface.impl.getPreferredFormat(); 121 | } 122 | }; 123 | 124 | pub const PowerPreference = enum { 125 | low_power, 126 | high_performance, 127 | }; 128 | 129 | pub const AdapterDesc = struct { 130 | impl: api.AdapterDesc = .{}, 131 | 132 | pub fn setPowerPreference( 133 | desc: *AdapterDesc, 134 | power_preference: PowerPreference, 135 | ) void { 136 | desc.impl.setPowerPreference(power_preference); 137 | } 138 | 139 | pub fn setForceFallbackAdapter( 140 | desc: *AdapterDesc, 141 | force_fallback_adapter: bool, 142 | ) void { 143 | desc.impl.setForceFallbackAdapter(force_fallback_adapter); 144 | } 145 | }; 146 | 147 | pub const Adapter = struct { 148 | impl: api.Adapter, 149 | 150 | pub fn initDevice(adapter: *Adapter, desc: DeviceDesc) !Device { 151 | return Device{ .impl = try adapter.impl.initDevice(desc) }; 152 | } 153 | 154 | pub fn deinitDevice(adapter: *Adapter, device: *Device) void { 155 | adapter.impl.deinitDevice(&device.impl); 156 | } 157 | }; 158 | 159 | pub const FeatureName = enum { 160 | depth_clip_control, 161 | depth24unorm_stencil8, 162 | depth32float_stencil8, 163 | timestamp_query, 164 | pipeline_statistics_query, 165 | texture_compression_bc, 166 | texture_compression_etc2, 167 | texture_compression_astc, 168 | indirect_first_instance, 169 | }; 170 | 171 | pub const Limits = struct { 172 | max_texture_dimension_1d: ?u32 = null, 173 | max_texture_dimension_2d: ?u32 = null, 174 | max_texture_dimension_3d: ?u32 = null, 175 | max_texture_array_layers: ?u32 = null, 176 | max_bind_groups: ?u32 = null, 177 | max_dynamic_uniform_buffers_per_pipeline_layout: ?u32 = null, 178 | max_dynamic_storage_buffers_per_pipeline_layout: ?u32 = null, 179 | max_sampled_textures_per_shader_stage: ?u32 = null, 180 | max_samplers_per_shader_stage: ?u32 = null, 181 | max_storage_buffers_per_shader_stage: ?u32 = null, 182 | max_storage_textures_per_shader_stage: ?u32 = null, 183 | max_uniform_buffers_per_shader_stage: ?u32 = null, 184 | max_uniform_buffer_binding_size: ?u32 = null, 185 | max_storage_buffer_binding_size: ?u32 = null, 186 | min_uniform_buffer_offset_alignment: ?u32 = null, 187 | min_storage_buffer_offset_alignment: ?u32 = null, 188 | max_vertex_buffers: ?u32 = null, 189 | max_vertex_attributes: ?u32 = null, 190 | max_vertex_buffer_array_stride: ?u32 = null, 191 | max_inter_stage_shader_components: ?u32 = null, 192 | max_compute_workgroup_storage_size: ?u32 = null, 193 | max_compute_invocations_per_workgroup: ?u32 = null, 194 | max_compute_workgroup_size_x: ?u32 = null, 195 | max_compute_workgroup_size_y: ?u32 = null, 196 | max_compute_workgroup_size_z: ?u32 = null, 197 | max_compute_workgroups_per_dimension: ?u32 = null, 198 | }; 199 | 200 | pub const DeviceDesc = struct { 201 | impl: api.DeviceDesc = .{}, 202 | 203 | pub fn setRequiredFeatures( 204 | desc: *DeviceDesc, 205 | required_features: []const FeatureName, 206 | ) void { 207 | desc.impl.setRequiredFeatures(required_features); 208 | } 209 | 210 | pub fn setRequiredLimits(desc: *DeviceDesc, required_limits: Limits) void { 211 | desc.impl.setRequiredLimits(required_limits); 212 | } 213 | }; 214 | 215 | pub const Device = struct { 216 | impl: api.Device, 217 | 218 | pub fn initSwapchain( 219 | device: *Device, 220 | surface: *Surface, 221 | desc: SwapchainDesc, 222 | ) !Swapchain { 223 | return Swapchain{ 224 | .impl = try device.impl.initSwapchain(&surface.impl, desc), 225 | }; 226 | } 227 | 228 | pub fn deinitSwapchain(device: *Device, swapchain: *Swapchain) void { 229 | device.impl.deinitSwapchain(&swapchain.impl); 230 | } 231 | 232 | pub fn initShader(device: *Device, desc: ShaderDesc) !Shader { 233 | return Shader{ .impl = try device.impl.initShader(desc) }; 234 | } 235 | 236 | pub fn deinitShader(device: *Device, shader: *Shader) void { 237 | device.impl.deinitShader(&shader.impl); 238 | } 239 | 240 | pub fn initBuffer(device: *Device, desc: BufferDesc) !Buffer { 241 | return Buffer{ .impl = try device.impl.initBuffer(desc) }; 242 | } 243 | 244 | pub fn initBufferSlice( 245 | device: *Device, 246 | slice: anytype, 247 | usage: BufferUsage, 248 | ) !Buffer { 249 | const bytes = std.mem.sliceAsBytes(slice); 250 | return try device.initBuffer(.{ 251 | .size = bytes.len, 252 | .usage = usage, 253 | .data = bytes, 254 | }); 255 | } 256 | 257 | pub fn deinitBuffer(device: *Device, buffer: *Buffer) void { 258 | device.impl.deinitBuffer(&buffer.impl); 259 | } 260 | 261 | pub fn initTexture( 262 | device: *Device, 263 | desc: TextureDesc, 264 | usage: TextureUsage, 265 | ) !Texture { 266 | const texture = Texture{ 267 | .impl = try device.impl.initTexture(desc, usage), 268 | }; 269 | if (desc.bytes) |bytes| { 270 | const bytes_per_pixel = try getBytesPerPixel(desc.format); 271 | var queue = device.getQueue(); 272 | try queue.writeTexture( 273 | .{ .texture = &texture }, 274 | bytes, 275 | .{ 276 | .bytes_per_row = desc.size.width * bytes_per_pixel, 277 | .rows_per_image = desc.size.height, 278 | }, 279 | desc.size, 280 | ); 281 | } 282 | return texture; 283 | } 284 | 285 | pub fn deinitTexture(device: *Device, texture: *Texture) void { 286 | device.impl.deinitTexture(&texture.impl); 287 | } 288 | 289 | pub fn initTextureView(device: *Device, desc: TextureViewDesc) !TextureView { 290 | return TextureView{ .impl = try device.impl.initTextureView(desc) }; 291 | } 292 | 293 | pub fn deinitTextureView(device: *Device, texture_view: *TextureView) void { 294 | device.impl.deinitTextureView(&texture_view.impl); 295 | } 296 | 297 | pub fn initSampler(device: *Device, desc: SamplerDesc) !Sampler { 298 | return Sampler{ .impl = try device.impl.initSampler(desc) }; 299 | } 300 | 301 | pub fn deinitSampler(device: *Device, sampler: *Sampler) void { 302 | device.impl.deinitSampler(&sampler.impl); 303 | } 304 | 305 | pub fn initBindGroupLayout( 306 | device: *Device, 307 | desc: BindGroupLayoutDesc, 308 | ) !BindGroupLayout { 309 | return BindGroupLayout{ .impl = try device.impl.initBindGroupLayout(desc) }; 310 | } 311 | 312 | pub fn deinitBindGroupLayout( 313 | device: *Device, 314 | bind_group_layout: *BindGroupLayout, 315 | ) void { 316 | device.impl.deinitBindGroupLayout(&bind_group_layout.impl); 317 | } 318 | 319 | pub fn initBindGroup(device: *Device, desc: BindGroupDesc) !BindGroup { 320 | return BindGroup{ .impl = try device.impl.initBindGroup(desc) }; 321 | } 322 | 323 | pub fn deinitBindGroup(device: *Device, bind_group: *BindGroup) void { 324 | device.impl.deinitBindGroup(&bind_group.impl); 325 | } 326 | 327 | pub fn initPipelineLayout( 328 | device: *Device, 329 | desc: PipelineLayoutDesc, 330 | ) !PipelineLayout { 331 | return PipelineLayout{ .impl = try device.impl.initPipelineLayout(desc) }; 332 | } 333 | 334 | pub fn deinitPipelineLayout( 335 | device: *Device, 336 | pipeline_layout: *PipelineLayout, 337 | ) void { 338 | device.impl.deinitPipelineLayout(&pipeline_layout.impl); 339 | } 340 | 341 | pub fn initRenderPipeline( 342 | device: *Device, 343 | desc: RenderPipelineDesc, 344 | ) !RenderPipeline { 345 | return RenderPipeline{ .impl = try device.impl.initRenderPipeline(desc) }; 346 | } 347 | 348 | pub fn deinitRenderPipeline( 349 | device: *Device, 350 | render_pipeline: *RenderPipeline, 351 | ) void { 352 | device.impl.deinitRenderPipeline(&render_pipeline.impl); 353 | } 354 | 355 | pub fn initCommandEncoder(device: *Device) !CommandEncoder { 356 | return CommandEncoder{ .impl = try device.impl.initCommandEncoder() }; 357 | } 358 | 359 | pub fn getQueue(device: *Device) Queue { 360 | return Queue{ .impl = device.impl.getQueue() }; 361 | } 362 | }; 363 | 364 | pub const Extent3d = struct { 365 | width: u32, 366 | height: u32, 367 | depth_or_array_layers: u32 = 1, 368 | }; 369 | 370 | pub const SwapchainDesc = struct { 371 | size: Extent3d, 372 | format: TextureFormat, 373 | }; 374 | 375 | pub const Swapchain = struct { 376 | impl: api.Swapchain, 377 | 378 | pub fn getCurrentTextureView(swapchain: *Swapchain) !TextureView { 379 | return TextureView{ .impl = try swapchain.impl.getCurrentTextureView() }; 380 | } 381 | 382 | pub fn present(swapchain: *Swapchain) !void { 383 | try swapchain.impl.present(); 384 | } 385 | }; 386 | 387 | pub const ShaderStage = packed struct { 388 | vertex: bool = false, 389 | fragment: bool = false, 390 | compute: bool = false, 391 | }; 392 | 393 | pub const ShaderDesc = struct { 394 | bytes: []const u8, 395 | }; 396 | 397 | pub const Shader = struct { 398 | impl: api.Shader, 399 | }; 400 | 401 | pub const BufferUsage = packed struct { 402 | map_read: bool = false, 403 | map_write: bool = false, 404 | copy_src: bool = false, 405 | copy_dst: bool = false, 406 | index: bool = false, 407 | vertex: bool = false, 408 | uniform: bool = false, 409 | storage: bool = false, 410 | indirect: bool = false, 411 | query_resolve: bool = false, 412 | }; 413 | 414 | pub const whole_size = std.math.maxInt(usize); 415 | 416 | pub const BufferDesc = struct { 417 | size: usize, 418 | usage: BufferUsage, 419 | data: ?[]const u8 = null, 420 | }; 421 | 422 | pub const Buffer = struct { 423 | impl: api.Buffer, 424 | }; 425 | 426 | pub const TextureFormat = enum { 427 | r8unorm, 428 | r8snorm, 429 | r8uint, 430 | r8sint, 431 | r16uint, 432 | r16sint, 433 | r16float, 434 | rg8unorm, 435 | rg8snorm, 436 | rg8uint, 437 | rg8sint, 438 | r32float, 439 | r32uint, 440 | r32sint, 441 | rg16uint, 442 | rg16sint, 443 | rg16float, 444 | rgba8unorm, 445 | rgba8unorm_srgb, 446 | rgba8snorm, 447 | rgba8uint, 448 | rgba8sint, 449 | bgra8unorm, 450 | bgra8unorm_srgb, 451 | rgb10a2unorm, 452 | rg11b10ufloat, 453 | rgb9e5ufloat, 454 | rg32float, 455 | rg32uint, 456 | rg32sint, 457 | rgba16uint, 458 | rgba16sint, 459 | rgba16float, 460 | rgba32float, 461 | rgba32uint, 462 | rgba32sint, 463 | stencil8, 464 | depth16unorm, 465 | depth24plus, 466 | depth24plus_stencil8, 467 | depth24unorm_stencil8, 468 | depth32float, 469 | depth32float_stencil8, 470 | bc1_rgba_unorm, 471 | bc1_rgba_unorm_srgb, 472 | bc2_rgba_unorm, 473 | bc2_rgba_unorm_srgb, 474 | bc3_rgba_unorm, 475 | bc3_rgba_unorm_srgb, 476 | bc4_r_unorm, 477 | bc4_r_snorm, 478 | bc5_rg_unorm, 479 | bc5_rg_snorm, 480 | bc6h_rgb_ufloat, 481 | bc6h_rgb_float, 482 | bc7_rgba_unorm, 483 | bc7_rgba_unorm_srgb, 484 | etc2_rgb8unorm, 485 | etc2_rgb8unorm_srgb, 486 | etc2_rgb8a1unorm, 487 | etc2_rgb8a1unorm_srgb, 488 | etc2_rgba8unorm, 489 | etc2_rgba8unorm_srgb, 490 | eac_r11unorm, 491 | eac_r11snorm, 492 | eac_rg11unorm, 493 | eac_rg11snorm, 494 | astc_4x4_unorm, 495 | astc_4x4_unorm_srgb, 496 | astc_5x4_unorm, 497 | astc_5x4_unorm_srgb, 498 | astc_5x5_unorm, 499 | astc_5x5_unorm_srgb, 500 | astc_6x5_unorm, 501 | astc_6x5_unorm_srgb, 502 | astc_6x6_unorm, 503 | astc_6x6_unorm_srgb, 504 | astc_8x5_unorm, 505 | astc_8x5_unorm_srgb, 506 | astc_8x6_unorm, 507 | astc_8x6_unorm_srgb, 508 | astc_8x8_unorm, 509 | astc_8x8_unorm_srgb, 510 | astc_10x5_unorm, 511 | astc_10x5_unorm_srgb, 512 | astc_10x6_unorm, 513 | astc_10x6_unorm_srgb, 514 | astc_10x8_unorm, 515 | astc_10x8_unorm_srgb, 516 | astc_10x10_unorm, 517 | astc_10x10_unorm_srgb, 518 | astc_12x10_unorm, 519 | astc_12x10_unorm_srgb, 520 | astc_12x12_unorm, 521 | astc_12x12_unorm_srgb, 522 | }; 523 | 524 | pub fn getBytesPerPixel(format: TextureFormat) !u32 { 525 | return switch (format) { 526 | .r8unorm, .r8snorm, .r8uint, .r8sint => 1, 527 | .r16uint, .r16sint, .r16float => 2, 528 | .rg8unorm, .rg8snorm, .rg8uint, .rg8sint => 2, 529 | .r32float, .r32uint, .r32sint => 4, 530 | .rg16uint, .rg16sint, .rg16float => 4, 531 | .rgba8unorm, .rgba8unorm_srgb, .rgba8snorm, .rgba8uint, .rgba8sint => 4, 532 | .bgra8unorm, .bgra8unorm_srgb => 4, 533 | .rgb10a2unorm, .rg11b10ufloat, .rgb9e5ufloat => 4, 534 | .rg32float, .rg32uint, .rg32sint => 4, 535 | .rgba16uint, .rgba16sint, .rgba16float => 8, 536 | .rgba32float, .rgba32uint, .rgba32sint => 16, 537 | else => error.InvalidTextureFormat, 538 | }; 539 | } 540 | 541 | pub const TextureUsage = packed struct { 542 | copy_src: bool = false, 543 | copy_dst: bool = false, 544 | texture_binding: bool = false, 545 | storage_binding: bool = false, 546 | render_attachment: bool = false, 547 | }; 548 | 549 | pub const TextureViewDimension = enum { 550 | @"1d", 551 | @"2d", 552 | @"2d_array", 553 | cube, 554 | cube_array, 555 | @"3d", 556 | }; 557 | 558 | pub const TextureDesc = struct { 559 | size: Extent3d, 560 | format: TextureFormat, 561 | dimension: TextureViewDimension = .@"2d", 562 | mip_level_count: u32 = 1, 563 | sample_count: u32 = 1, 564 | bytes: ?[]const u8 = null, 565 | 566 | pub fn serialize(allocator: std.mem.Allocator, value: TextureDesc) ![]u8 { 567 | var value_encoded = value; 568 | if (value.bytes) |bytes| { 569 | const qoi_image = qoi.Image{ 570 | .width = value.size.width, 571 | .height = value.size.height, 572 | .data = bytes, 573 | }; 574 | const result = try qoi.encode(qoi_image, allocator); 575 | value_encoded.bytes = allocator.resize(result.bytes, result.len) orelse 576 | return error.ResizeFailed; 577 | } 578 | defer if (value_encoded.bytes) |bytes| { 579 | allocator.free(bytes); 580 | }; 581 | return try serde.serializeGeneric(allocator, value_encoded); 582 | } 583 | 584 | pub fn deserialize( 585 | desc: serde.DeserializeDesc, 586 | bytes: []const u8, 587 | ) !TextureDesc { 588 | var texture_desc = try serde.deserializeGeneric(desc, TextureDesc, bytes); 589 | if (texture_desc.bytes) |texture_bytes| { 590 | const allocator = desc.allocator orelse return error.AllocatorRequired; 591 | const image = try qoi.decode(texture_bytes, allocator); 592 | if (!desc.bytes_are_embedded) { 593 | allocator.free(texture_bytes); 594 | } 595 | texture_desc.bytes = image.data; 596 | } 597 | return texture_desc; 598 | } 599 | }; 600 | 601 | pub const Texture = struct { 602 | impl: api.Texture, 603 | }; 604 | 605 | pub const TextureAspect = enum { 606 | all, 607 | stencil_only, 608 | depth_only, 609 | }; 610 | 611 | pub const TextureViewDesc = struct { 612 | texture: *Texture, 613 | format: TextureFormat, 614 | dimension: TextureViewDimension = .@"2d", 615 | aspect: TextureAspect = .all, 616 | base_mip_level: u32 = 0, 617 | mip_level_count: u32 = 1, 618 | base_array_layer: u32 = 0, 619 | array_layer_count: u32 = 1, 620 | }; 621 | 622 | pub const TextureView = struct { 623 | impl: api.TextureView, 624 | }; 625 | 626 | pub const AddressMode = enum { 627 | clamp_to_edge, 628 | repeat, 629 | mirror_repeat, 630 | }; 631 | 632 | pub const FilterMode = enum { 633 | nearest, 634 | linear, 635 | }; 636 | 637 | pub const SamplerDesc = struct { 638 | address_mode_u: AddressMode = .clamp_to_edge, 639 | address_mode_v: AddressMode = .clamp_to_edge, 640 | address_mode_w: AddressMode = .clamp_to_edge, 641 | mag_filter: FilterMode = .nearest, 642 | min_filter: FilterMode = .nearest, 643 | mipmap_filter: FilterMode = .nearest, 644 | lod_min_clamp: f32 = 0.0, 645 | lod_max_clamp: f32 = 32.0, 646 | max_anisotropy: u32 = 1, 647 | compare: ?CompareFunction = null, 648 | }; 649 | 650 | pub const Sampler = struct { 651 | impl: api.Sampler, 652 | }; 653 | 654 | pub const BufferBindingType = enum { 655 | uniform, 656 | storage, 657 | read_only_storage, 658 | }; 659 | 660 | pub const BufferBindingLayout = struct { 661 | @"type": BufferBindingType = .uniform, 662 | has_dynamic_offset: bool = false, 663 | min_binding_size: usize = 0, 664 | }; 665 | 666 | pub const TextureSampleType = enum { 667 | float, 668 | unfilterable_float, 669 | depth, 670 | sint, 671 | uint, 672 | }; 673 | 674 | pub const TextureBindingLayout = struct { 675 | sample_type: TextureSampleType = .float, 676 | view_dimension: TextureViewDimension = .@"2d", 677 | multisampled: bool = false, 678 | }; 679 | 680 | pub const StorageTextureAccess = enum { 681 | write_only, 682 | }; 683 | 684 | pub const StorageTextureBindingLayout = struct { 685 | format: TextureFormat, 686 | access: StorageTextureAccess = .write_only, 687 | view_dimension: TextureViewDimension = .@"2d", 688 | }; 689 | 690 | pub const SamplerBindingType = enum { 691 | filtering, 692 | non_filtering, 693 | comparison, 694 | }; 695 | 696 | pub const SamplerBindingLayout = struct { 697 | @"type": SamplerBindingType = .filtering, 698 | }; 699 | 700 | pub const BindGroupLayoutEntry = struct { 701 | binding: u32, 702 | visibility: ShaderStage, 703 | buffer: ?BufferBindingLayout = null, 704 | texture: ?TextureBindingLayout = null, 705 | storage_texture: ?StorageTextureBindingLayout = null, 706 | sampler: ?SamplerBindingLayout = null, 707 | }; 708 | 709 | pub const BindGroupLayoutDesc = struct { 710 | entries: []const BindGroupLayoutEntry, 711 | }; 712 | 713 | pub const BindGroupLayout = struct { 714 | impl: api.BindGroupLayout, 715 | }; 716 | 717 | pub const BufferBinding = struct { 718 | buffer: *const Buffer, 719 | offset: usize = 0, 720 | size: usize = whole_size, 721 | }; 722 | 723 | pub const BindingResource = union(enum) { 724 | buffer_binding: BufferBinding, 725 | texture_view: *const TextureView, 726 | sampler: *const Sampler, 727 | }; 728 | 729 | pub const BindGroupEntry = struct { 730 | binding: u32, 731 | resource: BindingResource, 732 | }; 733 | 734 | pub const BindGroupDesc = struct { 735 | layout: *const BindGroupLayout, 736 | entries: []const BindGroupEntry, 737 | }; 738 | 739 | pub const BindGroup = struct { 740 | impl: api.BindGroup, 741 | }; 742 | 743 | pub const PipelineLayoutDesc = struct { 744 | bind_group_layouts: []const BindGroupLayout, 745 | }; 746 | 747 | pub const PipelineLayout = struct { 748 | impl: api.PipelineLayout, 749 | }; 750 | 751 | pub const VertexFormat = enum { 752 | uint8x2, 753 | uint8x4, 754 | sint8x2, 755 | sint8x4, 756 | unorm8x2, 757 | unorm8x4, 758 | snorm8x2, 759 | snorm8x4, 760 | uint16x2, 761 | uint16x4, 762 | sint16x2, 763 | sint16x4, 764 | unorm16x2, 765 | unorm16x4, 766 | snorm16x2, 767 | snorm16x4, 768 | float16x2, 769 | float16x4, 770 | float32, 771 | float32x2, 772 | float32x3, 773 | float32x4, 774 | uint32, 775 | uint32x2, 776 | uint32x3, 777 | uint32x4, 778 | sint32, 779 | sint32x2, 780 | sint32x3, 781 | sint32x4, 782 | }; 783 | 784 | pub const VertexAttribute = struct { 785 | format: VertexFormat, 786 | offset: usize, 787 | shader_location: u32, 788 | }; 789 | 790 | pub const VertexStepMode = enum { 791 | vertex, 792 | instance, 793 | }; 794 | 795 | pub const VertexBufferLayout = struct { 796 | array_stride: usize, 797 | step_mode: VertexStepMode = .vertex, 798 | attributes: []const VertexAttribute, 799 | }; 800 | 801 | pub const VertexState = struct { 802 | module: *const Shader, 803 | entry_point: []const u8, 804 | buffers: []const VertexBufferLayout = &.{}, 805 | }; 806 | 807 | pub fn getVertexBufferLayoutStruct( 808 | comptime Type: type, 809 | comptime step_mode: VertexStepMode, 810 | comptime base_location: u32, 811 | ) VertexBufferLayout { 812 | comptime var types: []const type = &.{}; 813 | inline for (@typeInfo(Type).Struct.fields) |field| { 814 | types = types ++ &[_]type{field.field_type}; 815 | } 816 | return getVertexBufferLayoutTypes(types, step_mode, base_location); 817 | } 818 | 819 | pub fn getVertexBufferLayoutTypes( 820 | comptime types: []const type, 821 | comptime step_mode: VertexStepMode, 822 | comptime base_location: u32, 823 | ) VertexBufferLayout { 824 | comptime var attributes: []const VertexAttribute = &.{}; 825 | comptime var offset: usize = 0; 826 | comptime var location: u32 = base_location; 827 | 828 | inline for (types) |Type| { 829 | attributes = attributes ++ &[_]VertexAttribute{ 830 | .{ 831 | .format = getVertexFormat(Type), 832 | .offset = offset, 833 | .shader_location = location, 834 | }, 835 | }; 836 | 837 | if (offset % @sizeOf(Type) != 0) { 838 | @compileError("Invalid alignment for vertex buffer layout!"); 839 | } 840 | 841 | offset += @sizeOf(Type); 842 | location += 1; 843 | } 844 | 845 | return VertexBufferLayout{ 846 | .array_stride = offset, 847 | .step_mode = step_mode, 848 | .attributes = attributes, 849 | }; 850 | } 851 | 852 | fn getVertexFormat(comptime Type: type) VertexFormat { 853 | return switch (@typeInfo(Type)) { 854 | .Int, .ComptimeInt, .Float, .ComptimeFloat => getVertexFormatLen(Type, 1), 855 | .Array => |A| getVertexFormatLen(A.child, A.len), 856 | .Vector => |V| getVertexFormatLen(V.child, V.len), 857 | .Struct => |S| block: { 858 | if (S.fields.len == 0 or S.fields.len > 4) { 859 | @compileError("Invalid number of fields for vertex attribute!"); 860 | } 861 | const field_type = S.fields[0].field_type; 862 | const field_type_info = @typeInfo(field_type); 863 | if (field_type_info != .Int and field_type_info != .Float) { 864 | @compileError( 865 | "Vertex attribute structs must be composed of ints or floats!", 866 | ); 867 | } 868 | inline for (S.fields) |field| { 869 | if (field.field_type != field_type) { 870 | @compileError("Vertex attribute fields must be homogenous!"); 871 | } 872 | } 873 | break :block getVertexFormatLen(field_type, S.fields.len); 874 | }, 875 | else => @compileError( 876 | "Invalid vertex attribute type " ++ @typeName(Type) ++ "!", 877 | ), 878 | }; 879 | } 880 | 881 | fn getVertexFormatLen(comptime Type: type, len: comptime_int) VertexFormat { 882 | return switch (@typeInfo(Type)) { 883 | .Int => |I| switch (I.signedness) { 884 | .signed => switch (I.bits) { 885 | 8 => switch (len) { 886 | 2 => .sint8x2, 887 | 4 => .sint8x4, 888 | else => @compileError("Invalid len for vertex attribute!"), 889 | }, 890 | 16 => switch (len) { 891 | 2 => .sint16x2, 892 | 4 => .sint16x4, 893 | else => @compileError("Invalid len for vertex attribute!"), 894 | }, 895 | 32 => switch (len) { 896 | 1 => .sint32, 897 | 2 => .sint32x2, 898 | 3 => .sint32x3, 899 | 4 => .sint32x4, 900 | else => @compileError("Invalid len for vertex attribute!"), 901 | }, 902 | else => @compileError("Invalid bit size for vertex attribute!"), 903 | }, 904 | .unsigned => switch (I.bits) { 905 | 8 => switch (len) { 906 | 2 => .uint8x2, 907 | 4 => .uint8x4, 908 | else => @compileError("Invalid len for vertex attribute!"), 909 | }, 910 | 16 => switch (len) { 911 | 2 => .uint16x2, 912 | 4 => .uint16x4, 913 | else => @compileError("Invalid len for vertex attribute!"), 914 | }, 915 | 32 => switch (len) { 916 | 1 => .uint32, 917 | 2 => .uint32x2, 918 | 3 => .uint32x3, 919 | 4 => .uint32x4, 920 | else => @compileError("Invalid len for vertex attribute!"), 921 | }, 922 | else => @compileError("Invalid bit size for vertex attribute!"), 923 | }, 924 | }, 925 | .Float => |F| switch (F.bits) { 926 | 16 => switch (len) { 927 | 2 => .float16x2, 928 | 4 => .float16x4, 929 | else => @compileError("Invalid len for vertex attribute"), 930 | }, 931 | 32 => switch (len) { 932 | 1 => .float32, 933 | 2 => .float32x2, 934 | 3 => .float32x3, 935 | 4 => .float32x4, 936 | else => @compileError("Invalid len for vertex attribute"), 937 | }, 938 | else => @compileError("Invalid bit size for vertex attribute!"), 939 | }, 940 | else => @compileError( 941 | "Invalid vertex attribute type " ++ @typeName(Type) ++ "!", 942 | ), 943 | }; 944 | } 945 | 946 | pub const PrimitiveTopology = enum { 947 | point_list, 948 | line_list, 949 | line_strip, 950 | triangle_list, 951 | triangle_strip, 952 | }; 953 | 954 | pub const FrontFace = enum { 955 | ccw, 956 | cw, 957 | }; 958 | 959 | pub const CullMode = enum { 960 | none, 961 | front, 962 | back, 963 | }; 964 | 965 | pub const IndexFormat = enum { 966 | uint16, 967 | uint32, 968 | }; 969 | 970 | pub const PrimitiveState = struct { 971 | topology: PrimitiveTopology = .triangle_list, 972 | front_face: FrontFace = .ccw, 973 | cull_mode: CullMode = .none, 974 | strip_index_format: ?IndexFormat = null, 975 | }; 976 | 977 | pub const CompareFunction = enum { 978 | never, 979 | less, 980 | less_equal, 981 | greater, 982 | greater_equal, 983 | equal, 984 | not_equal, 985 | always, 986 | }; 987 | 988 | pub const StencilOperation = enum { 989 | keep, 990 | zero, 991 | replace, 992 | invert, 993 | increment_clamp, 994 | decrement_clamp, 995 | increment_wrap, 996 | decrement_wrap, 997 | }; 998 | 999 | pub const StencilFaceState = struct { 1000 | compare: CompareFunction = .always, 1001 | fail_op: StencilOperation = .keep, 1002 | depth_fail_op: StencilOperation = .keep, 1003 | pass_op: StencilOperation = .keep, 1004 | }; 1005 | 1006 | pub const DepthStencilState = struct { 1007 | format: TextureFormat, 1008 | depth_write_enabled: bool = false, 1009 | depth_compare: CompareFunction = .always, 1010 | depth_bias: i32 = 0, 1011 | depth_bias_clamp: f32 = 0.0, 1012 | depth_bias_slope_scale: f32 = 0.0, 1013 | stencil_front: StencilFaceState = .{}, 1014 | stencil_back: StencilFaceState = .{}, 1015 | stencil_read_mask: u32 = 0xFFFFFFFF, 1016 | stencil_write_mask: u32 = 0xFFFFFFFF, 1017 | }; 1018 | 1019 | pub const MultisampleState = struct { 1020 | count: u32 = 1, 1021 | mask: u32 = 0xFFFFFFFF, 1022 | alpha_to_coverage_enabled: bool = false, 1023 | }; 1024 | 1025 | pub const BlendOperation = enum { 1026 | add, 1027 | subtract, 1028 | reverse_subtract, 1029 | min, 1030 | max, 1031 | }; 1032 | 1033 | pub const BlendFactor = enum { 1034 | zero, 1035 | one, 1036 | src, 1037 | one_minus_src, 1038 | src_alpha, 1039 | one_minus_src_alpha, 1040 | dst, 1041 | one_minus_dst, 1042 | dst_alpha, 1043 | one_minus_dst_alpha, 1044 | src_alpha_saturated, 1045 | constant, 1046 | one_minus_constant, 1047 | }; 1048 | 1049 | pub const BlendComponent = struct { 1050 | operation: BlendOperation = .add, 1051 | src_factor: BlendFactor = .one, 1052 | dst_factor: BlendFactor = .zero, 1053 | }; 1054 | 1055 | pub const BlendState = struct { 1056 | color: BlendComponent = .{}, 1057 | alpha: BlendComponent = .{}, 1058 | }; 1059 | 1060 | pub const ColorWrite = packed struct { 1061 | red: bool = false, 1062 | green: bool = false, 1063 | blue: bool = false, 1064 | alpha: bool = false, 1065 | 1066 | pub const all = ColorWrite{ 1067 | .red = true, 1068 | .green = true, 1069 | .blue = true, 1070 | .alpha = true, 1071 | }; 1072 | }; 1073 | 1074 | pub const ColorTargetState = struct { 1075 | format: TextureFormat, 1076 | blend: BlendState = .{}, 1077 | write_mask: ColorWrite = ColorWrite.all, 1078 | }; 1079 | 1080 | pub const FragmentState = struct { 1081 | module: *const Shader, 1082 | entry_point: []const u8, 1083 | targets: []const ColorTargetState, 1084 | }; 1085 | 1086 | pub const RenderPipelineDesc = struct { 1087 | impl: api.RenderPipelineDesc = .{}, 1088 | 1089 | pub fn setPipelineLayout( 1090 | desc: *RenderPipelineDesc, 1091 | pipeline_layout: *const PipelineLayout, 1092 | ) void { 1093 | desc.impl.setPipelineLayout(pipeline_layout); 1094 | } 1095 | 1096 | pub fn setVertexState( 1097 | desc: *RenderPipelineDesc, 1098 | vertex_state: VertexState, 1099 | ) void { 1100 | desc.impl.setVertexState(vertex_state); 1101 | } 1102 | 1103 | pub fn setPrimitiveState( 1104 | desc: *RenderPipelineDesc, 1105 | primitive_state: PrimitiveState, 1106 | ) void { 1107 | desc.impl.setPrimitiveState(primitive_state); 1108 | } 1109 | 1110 | pub fn setDepthStencilState( 1111 | desc: *RenderPipelineDesc, 1112 | depth_stencil_state: DepthStencilState, 1113 | ) void { 1114 | desc.impl.setDepthStencilState(depth_stencil_state); 1115 | } 1116 | 1117 | pub fn setMultisampleState( 1118 | desc: *RenderPipelineDesc, 1119 | multisample_state: MultisampleState, 1120 | ) void { 1121 | desc.impl.setMultisampleState(multisample_state); 1122 | } 1123 | 1124 | pub fn setFragmentState( 1125 | desc: *RenderPipelineDesc, 1126 | fragment_state: FragmentState, 1127 | ) void { 1128 | desc.impl.setFragmentState(fragment_state); 1129 | } 1130 | }; 1131 | 1132 | pub const RenderPipeline = struct { 1133 | impl: api.RenderPipeline, 1134 | }; 1135 | 1136 | pub const CommandEncoder = struct { 1137 | impl: api.CommandEncoder, 1138 | 1139 | pub fn beginRenderPass( 1140 | encoder: *CommandEncoder, 1141 | desc: RenderPassDesc, 1142 | ) !RenderPass { 1143 | return RenderPass{ .impl = try encoder.impl.beginRenderPass(desc) }; 1144 | } 1145 | 1146 | pub fn finish(encoder: *CommandEncoder) !CommandBuffer { 1147 | return CommandBuffer{ .impl = try encoder.impl.finish() }; 1148 | } 1149 | }; 1150 | 1151 | pub const CommandBuffer = struct { 1152 | impl: api.CommandBuffer, 1153 | }; 1154 | 1155 | pub const LoadOp = enum { 1156 | load, 1157 | clear, 1158 | }; 1159 | 1160 | pub const StoreOp = enum { 1161 | store, 1162 | discard, 1163 | }; 1164 | 1165 | pub const Color = struct { 1166 | r: f32, 1167 | g: f32, 1168 | b: f32, 1169 | a: f32, 1170 | }; 1171 | 1172 | pub const ColorAttachment = struct { 1173 | view: *const TextureView, 1174 | resolve_target: ?*const TextureView = null, 1175 | load_op: LoadOp, 1176 | store_op: StoreOp, 1177 | clear_value: Color = Color{ .r = 0.0, .g = 0.0, .b = 0.0, .a = 1.0 }, 1178 | }; 1179 | 1180 | pub const DepthStencilAttachment = struct { 1181 | view: *const TextureView, 1182 | depth_clear_value: f32 = 0.0, 1183 | depth_load_op: ?LoadOp = null, 1184 | depth_store_op: ?StoreOp = null, 1185 | depth_read_only: bool = false, 1186 | stencil_clear_value: u32 = 0, 1187 | stencil_load_op: ?LoadOp = null, 1188 | stencil_store_op: ?StoreOp = null, 1189 | stencil_read_only: bool = false, 1190 | }; 1191 | 1192 | pub const RenderPassDesc = struct { 1193 | impl: api.RenderPassDesc = .{}, 1194 | 1195 | pub fn setColorAttachments( 1196 | desc: *RenderPassDesc, 1197 | color_attachments: []const ColorAttachment, 1198 | ) void { 1199 | desc.impl.setColorAttachments(color_attachments); 1200 | } 1201 | 1202 | pub fn setDepthStencilAttachment( 1203 | desc: *RenderPassDesc, 1204 | depth_stencil_attachment: DepthStencilAttachment, 1205 | ) void { 1206 | desc.impl.setDepthStencilAttachment(depth_stencil_attachment); 1207 | } 1208 | }; 1209 | 1210 | pub const RenderPass = struct { 1211 | impl: api.RenderPass, 1212 | 1213 | pub fn setPipeline( 1214 | render_pass: *RenderPass, 1215 | render_pipeline: *const RenderPipeline, 1216 | ) !void { 1217 | try render_pass.impl.setPipeline(&render_pipeline.impl); 1218 | } 1219 | 1220 | pub fn setBindGroup( 1221 | render_pass: *RenderPass, 1222 | group_index: u32, 1223 | group: *const BindGroup, 1224 | dynamic_offsets: ?[]const u32, 1225 | ) !void { 1226 | try render_pass.impl.setBindGroup( 1227 | group_index, 1228 | &group.impl, 1229 | dynamic_offsets, 1230 | ); 1231 | } 1232 | 1233 | pub fn setVertexBuffer( 1234 | render_pass: *RenderPass, 1235 | slot: u32, 1236 | buffer: *const Buffer, 1237 | offset: u32, 1238 | size: usize, 1239 | ) !void { 1240 | try render_pass.impl.setVertexBuffer(slot, &buffer.impl, offset, size); 1241 | } 1242 | 1243 | pub fn setIndexBuffer( 1244 | render_pass: *RenderPass, 1245 | buffer: *const Buffer, 1246 | index_format: IndexFormat, 1247 | offset: u32, 1248 | size: usize, 1249 | ) !void { 1250 | try render_pass.impl.setIndexBuffer( 1251 | &buffer.impl, 1252 | index_format, 1253 | offset, 1254 | size, 1255 | ); 1256 | } 1257 | 1258 | pub fn draw( 1259 | render_pass: *RenderPass, 1260 | vertex_count: usize, 1261 | instance_count: usize, 1262 | first_vertex: usize, 1263 | first_instance: usize, 1264 | ) !void { 1265 | try render_pass.impl.draw( 1266 | vertex_count, 1267 | instance_count, 1268 | first_vertex, 1269 | first_instance, 1270 | ); 1271 | } 1272 | 1273 | pub fn drawIndexed( 1274 | render_pass: *RenderPass, 1275 | index_count: usize, 1276 | instance_count: usize, 1277 | first_index: usize, 1278 | base_vertex: i32, 1279 | first_instance: usize, 1280 | ) !void { 1281 | try render_pass.impl.drawIndexed( 1282 | index_count, 1283 | instance_count, 1284 | first_index, 1285 | base_vertex, 1286 | first_instance, 1287 | ); 1288 | } 1289 | 1290 | pub fn end(render_pass: *RenderPass) !void { 1291 | try render_pass.impl.end(); 1292 | } 1293 | }; 1294 | 1295 | pub const Origin3d = struct { 1296 | x: u32 = 0, 1297 | y: u32 = 0, 1298 | z: u32 = 0, 1299 | }; 1300 | 1301 | pub const ImageCopyTexture = struct { 1302 | texture: *const Texture, 1303 | mip_level: u32 = 0, 1304 | origin: Origin3d = .{}, 1305 | aspect: TextureAspect = .all, 1306 | }; 1307 | 1308 | pub const ImageDataLayout = struct { 1309 | offset: usize = 0, 1310 | bytes_per_row: usize, 1311 | rows_per_image: usize, 1312 | }; 1313 | 1314 | pub const Queue = struct { 1315 | impl: api.Queue, 1316 | 1317 | pub fn writeBuffer( 1318 | queue: *Queue, 1319 | buffer: *const Buffer, 1320 | buffer_offset: usize, 1321 | data: []const u8, 1322 | data_offset: usize, 1323 | ) !void { 1324 | try queue.impl.writeBuffer(&buffer.impl, buffer_offset, data, data_offset); 1325 | } 1326 | 1327 | pub fn writeTexture( 1328 | queue: *Queue, 1329 | destination: ImageCopyTexture, 1330 | data: []const u8, 1331 | data_layout: ImageDataLayout, 1332 | size: Extent3d, 1333 | ) !void { 1334 | try queue.impl.writeTexture(destination, data, data_layout, size); 1335 | } 1336 | 1337 | pub fn submit(queue: *Queue, buffers: []const CommandBuffer) !void { 1338 | try queue.impl.submit(buffers); 1339 | } 1340 | }; 1341 | -------------------------------------------------------------------------------- /src/gfx_web.zig: -------------------------------------------------------------------------------- 1 | const gfx = @import("gfx.zig"); 2 | const std = @import("std"); 3 | 4 | const js = struct { 5 | const GPUSize32 = u32; 6 | const GPUSize64 = usize; 7 | const GPUIndex32 = u32; 8 | const GPUSignedOffset32 = i32; 9 | const GPUIntegerCoordinate = u32; 10 | 11 | const ObjectId = u32; 12 | const DescId = ObjectId; 13 | const ContextId = ObjectId; 14 | const AdapterId = ObjectId; 15 | const DeviceId = ObjectId; 16 | const ShaderId = ObjectId; 17 | const BindGroupLayoutId = ObjectId; 18 | const BindGroupId = ObjectId; 19 | const PipelineLayoutId = ObjectId; 20 | const RenderPipelineId = ObjectId; 21 | const RenderPassId = ObjectId; 22 | const CommandEncoderId = ObjectId; 23 | const CommandBufferId = ObjectId; 24 | const TextureId = ObjectId; 25 | const TextureViewId = ObjectId; 26 | const SamplerId = ObjectId; 27 | const QuerySetId = ObjectId; 28 | const BufferId = ObjectId; 29 | 30 | const invalid_id: ObjectId = 0; 31 | const default_desc_id: DescId = 0; 32 | 33 | extern fn initDesc() DescId; 34 | extern fn deinitDesc(desc_id: DescId) void; 35 | extern fn setDescField( 36 | desc_id: DescId, 37 | field_ptr: [*]const u8, 38 | field_len: usize, 39 | ) void; 40 | extern fn setDescString( 41 | desc_id: DescId, 42 | value_ptr: [*]const u8, 43 | value_len: usize, 44 | ) void; 45 | extern fn setDescBool(desc_id: DescId, value: bool) void; 46 | extern fn setDescU32(desc_id: DescId, value: u32) void; 47 | extern fn setDescI32(desc_id: DescId, value: i32) void; 48 | extern fn setDescF32(desc_id: DescId, value: f32) void; 49 | extern fn beginDescArray(desc_id: DescId) void; 50 | extern fn endDescArray(desc_id: DescId) void; 51 | extern fn beginDescChild(desc_id: DescId) void; 52 | extern fn endDescChild(desc_id: DescId) void; 53 | 54 | extern fn createContext( 55 | canvas_id_ptr: [*]const u8, 56 | canvas_id_len: usize, 57 | ) ContextId; 58 | extern fn destroyContext(context_id: ContextId) void; 59 | extern fn getContextCurrentTexture(context_id: ContextId) TextureId; 60 | extern fn configure( 61 | device_id: DeviceId, 62 | context_id: ContextId, 63 | desc_id: DescId, 64 | ) void; 65 | extern fn getPreferredFormat() usize; 66 | 67 | extern fn requestAdapter(desc_id: DescId) void; 68 | extern fn destroyAdapter(adapter_id: AdapterId) void; 69 | 70 | extern fn requestDevice(adapter_id: AdapterId, desc_id: DescId) void; 71 | extern fn destroyDevice(device_id: DeviceId) void; 72 | 73 | extern fn createShader( 74 | device_id: DeviceId, 75 | code_ptr: [*]const u8, 76 | code_len: usize, 77 | ) ShaderId; 78 | extern fn destroyShader(shader_id: ShaderId) void; 79 | extern fn checkShaderCompile(shader_id: ShaderId) void; 80 | 81 | extern fn createBuffer( 82 | device_id: DeviceId, 83 | desc_id: DescId, 84 | init_data_ptr: [*]const u8, 85 | init_data_len: usize, 86 | ) BufferId; 87 | extern fn destroyBuffer(buffer_id: BufferId) void; 88 | 89 | extern fn createTexture(device_id: DeviceId, desc_id: DescId) TextureId; 90 | extern fn destroyTexture(texture_id: TextureId) void; 91 | extern fn createTextureView(desc_id: DescId) TextureViewId; 92 | extern fn destroyTextureView(texture_view_id: TextureViewId) void; 93 | 94 | extern fn createSampler(device_id: DeviceId, desc_id: DescId) SamplerId; 95 | extern fn destroySampler(sampler_id: SamplerId) void; 96 | 97 | extern fn createBindGroupLayout( 98 | device_id: DeviceId, 99 | desc_id: DescId, 100 | ) BindGroupLayoutId; 101 | extern fn destroyBindGroupLayout(bind_group_layout_id: BindGroupLayoutId) void; 102 | extern fn createBindGroup(device_id: DeviceId, desc_id: DescId) BindGroupId; 103 | extern fn destroyBindGroup(bind_group_id: BindGroupId) void; 104 | 105 | extern fn createPipelineLayout( 106 | device_id: DeviceId, 107 | desc_id: DescId, 108 | ) PipelineLayoutId; 109 | extern fn destroyPipelineLayout(pipeline_layout_id: PipelineLayoutId) void; 110 | extern fn createRenderPipeline( 111 | device_id: DeviceId, 112 | desc_id: DescId, 113 | ) RenderPipelineId; 114 | extern fn destroyRenderPipeline(render_pipeline_id: RenderPipelineId) void; 115 | 116 | extern fn createCommandEncoder(device_id: DeviceId) CommandEncoderId; 117 | extern fn finishCommandEncoder( 118 | command_encoder_id: CommandEncoderId, 119 | ) CommandBufferId; 120 | 121 | extern fn beginRenderPass( 122 | command_encoder_id: CommandEncoderId, 123 | desc_id: DescId, 124 | ) RenderPassId; 125 | extern fn setPipeline( 126 | render_pass_id: RenderPassId, 127 | render_pipeline_id: RenderPipelineId, 128 | ) void; 129 | extern fn setBindGroup( 130 | render_pass_id: RenderPassId, 131 | group_index: GPUIndex32, 132 | bind_group_id: BindGroupId, 133 | dynamic_offsets_ptr: [*]const u8, 134 | dynamic_offsets_len: usize, 135 | ) void; 136 | extern fn setVertexBuffer( 137 | render_pass_id: RenderPassId, 138 | slot: GPUIndex32, 139 | buffer: BufferId, 140 | offset: GPUSize64, 141 | size: GPUSize64, 142 | ) void; 143 | extern fn setIndexBuffer( 144 | render_pass_id: RenderPassId, 145 | buffer_id: BufferId, 146 | index_format_ptr: [*]const u8, 147 | index_format_len: usize, 148 | offset: usize, 149 | size: usize, 150 | ) void; 151 | extern fn draw( 152 | render_pass_id: RenderPassId, 153 | vertex_count: GPUSize32, 154 | instance_count: GPUSize32, 155 | first_vertex: GPUSize32, 156 | first_instance: GPUSize32, 157 | ) void; 158 | extern fn drawIndexed( 159 | render_pass_id: RenderPassId, 160 | index_count: GPUSize32, 161 | instance_count: GPUSize32, 162 | first_index: GPUSize32, 163 | base_vertex: GPUSignedOffset32, 164 | first_instance: GPUSize32, 165 | ) void; 166 | extern fn endRenderPass(render_pass_id: RenderPassId) void; 167 | 168 | extern fn queueSubmit( 169 | device_id: DeviceId, 170 | command_buffer_id: CommandBufferId, 171 | ) void; 172 | extern fn queueWriteBuffer( 173 | device_id: DeviceId, 174 | buffer_id: BufferId, 175 | buffer_offset: GPUSize64, 176 | data_ptr: [*]const u8, 177 | data_len: usize, 178 | data_offset: GPUSize64, 179 | ) void; 180 | extern fn queueWriteTexture( 181 | device_id: DeviceId, 182 | destination_id: DescId, 183 | data_ptr: [*]const u8, 184 | data_len: usize, 185 | data_layout_id: DescId, 186 | size_width: GPUIntegerCoordinate, 187 | size_height: GPUIntegerCoordinate, 188 | size_depth_or_array_layers: GPUIntegerCoordinate, 189 | ) void; 190 | }; 191 | 192 | fn getEnumName(value: anytype) []const u8 { 193 | @setEvalBranchQuota(10000); 194 | comptime var enum_names: []const []const u8 = &.{}; 195 | inline for (@typeInfo(@TypeOf(value)).Enum.fields) |field| { 196 | comptime var enum_name: []const u8 = &.{}; 197 | inline for (field.name) |char| { 198 | enum_name = enum_name ++ &[_]u8{if (char == '_') '-' else char}; 199 | } 200 | enum_names = enum_names ++ &[_][]const u8{enum_name}; 201 | } 202 | return enum_names[@enumToInt(value)]; 203 | } 204 | 205 | fn getFieldName(comptime name: []const u8) []const u8 { 206 | comptime var field_name: []const u8 = &.{}; 207 | comptime var next_upper = false; 208 | inline for (name) |char| { 209 | if (char == '_') { 210 | next_upper = true; 211 | continue; 212 | } 213 | const next_char = if (next_upper) std.ascii.toUpper(char) else char; 214 | field_name = field_name ++ &[_]u8{next_char}; 215 | next_upper = false; 216 | } 217 | return field_name; 218 | } 219 | 220 | fn setDescField(desc_id: js.DescId, field: []const u8) void { 221 | js.setDescField(desc_id, field.ptr, field.len); 222 | } 223 | 224 | fn setDescValue(desc_id: js.DescId, value: anytype) void { 225 | switch (@typeInfo(@TypeOf(value))) { 226 | .Bool => js.setDescBool(desc_id, value), 227 | .Int => |I| { 228 | if (I.bits != 32) { 229 | @compileError("Desc ints must be 32 bits!"); 230 | } 231 | switch (I.signedness) { 232 | .signed => js.setDescI32(desc_id, value), 233 | .unsigned => js.setDescU32(desc_id, value), 234 | } 235 | }, 236 | .Float => |F| { 237 | if (F.bits != 32) { 238 | @compileError("Desc floats must be 32 bits!"); 239 | } 240 | js.setDescF32(desc_id, value); 241 | }, 242 | .Enum => { 243 | const enum_name = getEnumName(value); 244 | js.setDescString(desc_id, enum_name.ptr, enum_name.len); 245 | }, 246 | .Optional => { 247 | if (value) |v| { 248 | setDescValue(desc_id, v); 249 | } 250 | }, 251 | .Pointer => |P| { 252 | switch (P.size) { 253 | .One => { 254 | setDescValue(desc_id, value.*); 255 | }, 256 | .Slice => { 257 | if (P.child == u8) { 258 | js.setDescString(desc_id, value.ptr, value.len); 259 | } else { 260 | js.beginDescArray(desc_id); 261 | for (value) |v| { 262 | setDescValue(desc_id, v); 263 | } 264 | js.endDescArray(desc_id); 265 | } 266 | }, 267 | else => @compileError("Invalid desc pointer size!"), 268 | } 269 | }, 270 | .Struct => |S| { 271 | if (S.layout == .Packed) { 272 | const BitType = @Type(.{ 273 | .Int = .{ 274 | .signedness = .unsigned, 275 | .bits = @bitSizeOf(@TypeOf(value)), 276 | }, 277 | }); 278 | js.setDescU32(desc_id, @intCast(u32, @bitCast(BitType, value))); 279 | } else if (typeIsArrayDesc(@TypeOf(value))) { 280 | js.beginDescArray(desc_id); 281 | inline for (S.fields) |field| { 282 | setDescValue(desc_id, @field(value, field.name)); 283 | } 284 | js.endDescArray(desc_id); 285 | } else if (S.fields.len == 1 and @hasField(@TypeOf(value), "impl")) { 286 | setDescValue(desc_id, value.impl.id); 287 | } else { 288 | js.beginDescChild(desc_id); 289 | inline for (S.fields) |field| { 290 | const name = comptime getFieldName(field.name); 291 | setDescFieldValue(desc_id, name, @field(value, field.name)); 292 | } 293 | js.endDescChild(desc_id); 294 | } 295 | }, 296 | .Union => |U| { 297 | inline for (U.fields) |field, i| { 298 | const Tag = U.tag_type orelse 299 | @compileError("Desc union must be tagged!"); 300 | const tag = std.meta.activeTag(value); 301 | const type_name = @typeName(@TypeOf(value)) ++ "Type"; 302 | if (@field(Tag, field.name) == tag) { 303 | setDescValue(desc_id, @field(value, field.name)); 304 | setDescFieldValue(desc_id, type_name, @as(u32, i)); 305 | break; 306 | } 307 | } 308 | }, 309 | else => @compileError("Invalid desc type!"), 310 | } 311 | } 312 | 313 | fn typeIsArrayDesc(comptime Type: type) bool { 314 | return Type == gfx.Extent3d or Type == gfx.Origin3d or Type == gfx.Color; 315 | } 316 | 317 | fn setDescFieldValue(desc_id: js.DescId, field: []const u8, value: anytype) void { 318 | setDescField(desc_id, field); 319 | setDescValue(desc_id, value); 320 | } 321 | 322 | fn setDesc(desc_id: js.DescId, desc: anytype) void { 323 | inline for (@typeInfo(@TypeOf(desc)).Struct.fields) |field| { 324 | const field_name = comptime getFieldName(field.name); 325 | setDescFieldValue(desc_id, field_name, @field(desc, field.name)); 326 | } 327 | } 328 | 329 | pub const Instance = struct { 330 | pub fn init() !Instance { 331 | return Instance{}; 332 | } 333 | 334 | pub fn deinit(_: *Instance) void {} 335 | 336 | pub fn initSurface(_: *Instance, desc: gfx.SurfaceDesc) !Surface { 337 | return Surface{ 338 | .context_id = js.createContext( 339 | desc.window_info.canvas_id.ptr, 340 | desc.window_info.canvas_id.len, 341 | ), 342 | }; 343 | } 344 | 345 | pub fn deinitSurface(_: *Instance, _: *Surface) void {} 346 | 347 | pub fn initAdapter(_: *Instance, desc: gfx.AdapterDesc) !Adapter { 348 | return try await async requestAdapterAsync(desc); 349 | } 350 | 351 | pub fn deinitAdapter(_: *Instance, adapter: *Adapter) void { 352 | js.destroyAdapter(adapter.id); 353 | } 354 | 355 | var request_adapter_frame: anyframe = undefined; 356 | var request_adapter_id: anyerror!js.AdapterId = undefined; 357 | 358 | fn requestAdapterAsync(desc: gfx.AdapterDesc) !Adapter { 359 | defer js.deinitDesc(desc.impl.id); 360 | js.requestAdapter(desc.impl.id); 361 | suspend { 362 | request_adapter_frame = @frame(); 363 | } 364 | return Adapter{ .id = try request_adapter_id }; 365 | } 366 | 367 | export fn requestAdapterComplete(adapter_id: js.AdapterId) void { 368 | request_adapter_id = if (adapter_id == js.invalid_id) 369 | error.RequestAdapterFailed 370 | else 371 | adapter_id; 372 | resume request_adapter_frame; 373 | } 374 | }; 375 | 376 | pub const Surface = struct { 377 | context_id: js.ContextId, 378 | 379 | pub fn getPreferredFormat(_: Surface) !gfx.TextureFormat { 380 | const format = js.getPreferredFormat(); 381 | return @intToEnum(gfx.TextureFormat, format); 382 | } 383 | }; 384 | 385 | pub const AdapterDesc = struct { 386 | id: js.DescId = js.default_desc_id, 387 | 388 | pub fn setPowerPreference( 389 | desc: *AdapterDesc, 390 | power_preference: gfx.PowerPreference, 391 | ) void { 392 | if (desc.id == js.default_desc_id) { 393 | desc.id = js.initDesc(); 394 | } 395 | setDescFieldValue(desc.id, "powerPreference", power_preference); 396 | } 397 | 398 | pub fn setForceFallbackAdapter( 399 | desc: *AdapterDesc, 400 | force_fallback_adapter: bool, 401 | ) void { 402 | if (desc.id == js.default_desc_id) { 403 | desc.id = js.initDesc(); 404 | } 405 | setDescFieldValue(desc.id, "forceFallbackAdapter", force_fallback_adapter); 406 | } 407 | }; 408 | 409 | pub const Adapter = struct { 410 | id: js.AdapterId, 411 | 412 | pub fn initDevice(adapter: *Adapter, desc: gfx.DeviceDesc) !Device { 413 | return try await async adapter.requestDeviceAsync(desc); 414 | } 415 | 416 | pub fn deinitDevice(_: *Adapter, device: *Device) void { 417 | js.destroyDevice(device.id); 418 | } 419 | 420 | var request_device_frame: anyframe = undefined; 421 | var request_device_id: anyerror!js.DeviceId = undefined; 422 | 423 | fn requestDeviceAsync(adapter: *Adapter, desc: gfx.DeviceDesc) !Device { 424 | defer js.deinitDesc(desc.impl.id); 425 | js.requestDevice(adapter.id, desc.impl.id); 426 | suspend { 427 | request_device_frame = @frame(); 428 | } 429 | return Device{ .id = try request_device_id }; 430 | } 431 | 432 | export fn requestDeviceComplete(device_id: js.DeviceId) void { 433 | request_device_id = if (device_id == js.invalid_id) 434 | error.RequestDeviceFailed 435 | else 436 | device_id; 437 | resume request_device_frame; 438 | } 439 | }; 440 | 441 | pub const DeviceDesc = struct { 442 | id: js.DescId = js.default_desc_id, 443 | 444 | pub fn setRequiredFeatures( 445 | desc: *DeviceDesc, 446 | required_features: []const gfx.FeatureName, 447 | ) void { 448 | if (desc.id == js.default_desc_id) { 449 | desc.id = js.initDesc(); 450 | } 451 | setDescFieldValue(desc.id, "requiredFeatures", required_features); 452 | } 453 | 454 | pub fn setRequiredLimits(desc: *DeviceDesc, required_limits: gfx.Limits) void { 455 | if (desc.id == js.default_desc_id) { 456 | desc.id = js.initDesc(); 457 | } 458 | setDescFieldValue(desc.id, "requiredLimits", required_limits); 459 | } 460 | }; 461 | 462 | pub const Device = struct { 463 | id: js.AdapterId, 464 | 465 | pub fn initSwapchain( 466 | device: *Device, 467 | surface: *Surface, 468 | desc: gfx.SwapchainDesc, 469 | ) !Swapchain { 470 | var js_desc = js.initDesc(); 471 | defer js.deinitDesc(js_desc); 472 | setDesc(js_desc, desc); 473 | setDescFieldValue(js_desc, "alphaMode", @as([]const u8, "opaque")); 474 | 475 | const swapchain = Swapchain{ 476 | .id = surface.context_id, 477 | .view_desc = js.initDesc(), 478 | }; 479 | js.configure(device.id, swapchain.id, js_desc); 480 | return swapchain; 481 | } 482 | 483 | pub fn deinitSwapchain(_: *Device, swapchain: *Swapchain) void { 484 | js.deinitDesc(swapchain.view_desc); 485 | js.destroyContext(swapchain.id); 486 | } 487 | 488 | pub fn initShader(device: *Device, desc: gfx.ShaderDesc) !Shader { 489 | const shader = Shader{ 490 | .id = js.createShader(device.id, desc.bytes.ptr, desc.bytes.len), 491 | }; 492 | return shader; 493 | } 494 | 495 | pub fn deinitShader(_: *Device, shader: *Shader) void { 496 | js.destroyShader(shader.id); 497 | } 498 | 499 | pub fn initBuffer(device: *Device, desc: gfx.BufferDesc) !Buffer { 500 | var js_desc = js.initDesc(); 501 | defer js.deinitDesc(js_desc); 502 | setDesc(js_desc, desc); 503 | 504 | const data = desc.data orelse &[_]u8{}; 505 | return Buffer{ 506 | .id = js.createBuffer(device.id, js_desc, data.ptr, data.len), 507 | }; 508 | } 509 | 510 | pub fn deinitBuffer(_: *Device, buffer: *Buffer) void { 511 | js.destroyBuffer(buffer.id); 512 | } 513 | 514 | pub fn initTexture( 515 | device: *Device, 516 | desc: gfx.TextureDesc, 517 | usage: gfx.TextureUsage, 518 | ) !Texture { 519 | var js_desc = js.initDesc(); 520 | defer js.deinitDesc(js_desc); 521 | setDesc(js_desc, desc); 522 | setDescFieldValue(js_desc, "usage", usage); 523 | 524 | return Texture{ .id = js.createTexture(device.id, js_desc) }; 525 | } 526 | 527 | pub fn deinitTexture(_: *Device, texture: *Texture) void { 528 | js.destroyTexture(texture.id); 529 | } 530 | 531 | pub fn initTextureView(_: *Device, desc: gfx.TextureViewDesc) !TextureView { 532 | var js_desc = js.initDesc(); 533 | defer js.deinitDesc(js_desc); 534 | setDesc(js_desc, desc); 535 | 536 | return TextureView{ .id = js.createTextureView(js_desc) }; 537 | } 538 | 539 | pub fn deinitTextureView(_: *Device, texture_view: *TextureView) void { 540 | js.destroyTextureView(texture_view.id); 541 | } 542 | 543 | pub fn initSampler(device: *Device, desc: gfx.SamplerDesc) !Sampler { 544 | var js_desc = js.initDesc(); 545 | defer js.deinitDesc(js_desc); 546 | setDesc(js_desc, desc); 547 | 548 | return Sampler{ .id = js.createSampler(device.id, js_desc) }; 549 | } 550 | 551 | pub fn deinitSampler(_: *Device, sampler: *Sampler) void { 552 | js.destroySampler(sampler.id); 553 | } 554 | 555 | pub fn initBindGroupLayout( 556 | device: *Device, 557 | desc: gfx.BindGroupLayoutDesc, 558 | ) !BindGroupLayout { 559 | var js_desc = js.initDesc(); 560 | defer js.deinitDesc(js_desc); 561 | setDesc(js_desc, desc); 562 | 563 | return BindGroupLayout{ 564 | .id = js.createBindGroupLayout(device.id, js_desc), 565 | }; 566 | } 567 | 568 | pub fn deinitBindGroupLayout( 569 | _: *Device, 570 | bind_group_layout: *BindGroupLayout, 571 | ) void { 572 | js.destroyBindGroupLayout(bind_group_layout.id); 573 | } 574 | 575 | pub fn initBindGroup(device: *Device, desc: gfx.BindGroupDesc) !BindGroup { 576 | var js_desc = js.initDesc(); 577 | defer js.deinitDesc(js_desc); 578 | setDesc(js_desc, desc); 579 | 580 | return BindGroup{ .id = js.createBindGroup(device.id, js_desc) }; 581 | } 582 | 583 | pub fn deinitBindGroup(_: *Device, bind_group: *BindGroup) void { 584 | js.destroyBindGroup(bind_group.id); 585 | } 586 | 587 | pub fn initPipelineLayout( 588 | device: *Device, 589 | desc: gfx.PipelineLayoutDesc, 590 | ) !PipelineLayout { 591 | var js_desc = js.initDesc(); 592 | defer js.deinitDesc(js_desc); 593 | setDesc(js_desc, desc); 594 | 595 | return PipelineLayout{ .id = js.createPipelineLayout(device.id, js_desc) }; 596 | } 597 | 598 | pub fn deinitPipelineLayout(_: *Device, pipeline_layout: *PipelineLayout) void { 599 | js.destroyPipelineLayout(pipeline_layout.id); 600 | } 601 | 602 | pub fn initRenderPipeline( 603 | device: *Device, 604 | desc: gfx.RenderPipelineDesc, 605 | ) !RenderPipeline { 606 | defer js.deinitDesc(desc.impl.id); 607 | return RenderPipeline{ 608 | .id = js.createRenderPipeline(device.id, desc.impl.id), 609 | }; 610 | } 611 | 612 | pub fn deinitRenderPipeline(_: *Device, render_pipeline: *RenderPipeline) void { 613 | js.destroyRenderPipeline(render_pipeline.id); 614 | } 615 | 616 | pub fn initCommandEncoder(device: *Device) !CommandEncoder { 617 | return CommandEncoder{ .id = js.createCommandEncoder(device.id) }; 618 | } 619 | 620 | pub fn getQueue(device: *Device) Queue { 621 | return Queue{ .id = device.id }; 622 | } 623 | }; 624 | 625 | pub const Swapchain = struct { 626 | id: js.ContextId, 627 | view_desc: js.DescId, 628 | 629 | pub fn getCurrentTextureView(swapchain: *Swapchain) !TextureView { 630 | const tex_id = js.getContextCurrentTexture(swapchain.id); 631 | setDescFieldValue(swapchain.view_desc, "texture", tex_id); 632 | return TextureView{ .id = js.createTextureView(swapchain.view_desc) }; 633 | } 634 | 635 | pub fn present(_: *Swapchain) !void {} 636 | }; 637 | 638 | pub const Shader = struct { 639 | id: js.ShaderId, 640 | }; 641 | 642 | pub const Buffer = struct { 643 | id: js.BufferId, 644 | }; 645 | 646 | pub const Texture = struct { 647 | id: js.TextureId, 648 | 649 | pub fn createView(texture: *Texture) !TextureView { 650 | return TextureView{ .id = js.createTextureView(texture.id) }; 651 | } 652 | 653 | pub fn destroy(texture: *Texture) void { 654 | js.destroyTexture(texture.id); 655 | } 656 | }; 657 | 658 | pub const TextureView = struct { 659 | id: js.TextureViewId, 660 | }; 661 | 662 | pub const Sampler = struct { 663 | id: js.SamplerId, 664 | 665 | pub fn destroy(sampler: *Sampler) void { 666 | js.destroySampler(sampler.id); 667 | } 668 | }; 669 | 670 | pub const BindGroupLayout = struct { 671 | id: js.BindGroupLayoutId, 672 | 673 | pub fn destroy(bind_group_layout: *BindGroupLayout) void { 674 | js.destroyBindGroupLayout(bind_group_layout.id); 675 | } 676 | }; 677 | 678 | pub const BindGroup = struct { 679 | id: js.BindGroupId, 680 | 681 | pub fn destroy(bind_group: *BindGroup) void { 682 | js.destroyBindGroup(bind_group.id); 683 | } 684 | }; 685 | 686 | pub const PipelineLayout = struct { 687 | id: js.PipelineLayoutId, 688 | 689 | pub fn destroy(pipeline_layout: *PipelineLayout) void { 690 | js.destroyPipelineLayout(pipeline_layout.id); 691 | } 692 | }; 693 | 694 | pub const RenderPipelineDesc = struct { 695 | id: js.DescId = js.default_desc_id, 696 | 697 | pub fn setPipelineLayout( 698 | desc: *RenderPipelineDesc, 699 | pipeline_layout: *const gfx.PipelineLayout, 700 | ) void { 701 | if (desc.id == js.default_desc_id) { 702 | desc.id = js.initDesc(); 703 | } 704 | setDescFieldValue(desc.id, "layout", pipeline_layout); 705 | } 706 | 707 | pub fn setVertexState( 708 | desc: *RenderPipelineDesc, 709 | vertex_state: gfx.VertexState, 710 | ) void { 711 | if (desc.id == js.default_desc_id) { 712 | desc.id = js.initDesc(); 713 | } 714 | setDescFieldValue(desc.id, "vertex", vertex_state); 715 | } 716 | 717 | pub fn setPrimitiveState( 718 | desc: *RenderPipelineDesc, 719 | primitive_state: gfx.PrimitiveState, 720 | ) void { 721 | if (desc.id == js.default_desc_id) { 722 | desc.id = js.initDesc(); 723 | } 724 | setDescFieldValue(desc.id, "primitive", primitive_state); 725 | } 726 | 727 | pub fn setDepthStencilState( 728 | desc: *RenderPipelineDesc, 729 | depth_stencil_state: gfx.DepthStencilState, 730 | ) void { 731 | if (desc.id == js.default_desc_id) { 732 | desc.id = js.initDesc(); 733 | } 734 | setDescFieldValue(desc.id, "depthStencil", depth_stencil_state); 735 | } 736 | 737 | pub fn setMultisampleState( 738 | desc: *RenderPipelineDesc, 739 | multisample_state: gfx.MultisampleState, 740 | ) void { 741 | if (desc.id == js.default_desc_id) { 742 | desc.id = js.initDesc(); 743 | } 744 | setDescFieldValue(desc.id, "multisample", multisample_state); 745 | } 746 | 747 | pub fn setFragmentState( 748 | desc: *RenderPipelineDesc, 749 | fragment_state: gfx.FragmentState, 750 | ) void { 751 | if (desc.id == js.default_desc_id) { 752 | desc.id = js.initDesc(); 753 | } 754 | setDescFieldValue(desc.id, "fragment", fragment_state); 755 | } 756 | }; 757 | 758 | pub const RenderPipeline = struct { 759 | id: js.RenderPipelineId, 760 | 761 | pub fn destroy(render_pipeline: *RenderPipeline) void { 762 | js.destroyRenderPipeline(render_pipeline.id); 763 | } 764 | }; 765 | 766 | pub const CommandEncoder = struct { 767 | id: js.CommandEncoderId, 768 | 769 | pub fn beginRenderPass( 770 | encoder: *CommandEncoder, 771 | desc: gfx.RenderPassDesc, 772 | ) !RenderPass { 773 | defer js.deinitDesc(desc.impl.id); 774 | return RenderPass{ .id = js.beginRenderPass(encoder.id, desc.impl.id) }; 775 | } 776 | 777 | pub fn finish(encoder: *CommandEncoder) !CommandBuffer { 778 | return CommandBuffer{ .id = js.finishCommandEncoder(encoder.id) }; 779 | } 780 | }; 781 | 782 | pub const CommandBuffer = struct { 783 | id: js.CommandBufferId, 784 | }; 785 | 786 | pub const RenderPassDesc = struct { 787 | id: js.DescId = js.default_desc_id, 788 | 789 | pub fn setColorAttachments( 790 | desc: *RenderPassDesc, 791 | color_attachments: []const gfx.ColorAttachment, 792 | ) void { 793 | if (desc.id == js.default_desc_id) { 794 | desc.id = js.initDesc(); 795 | } 796 | setDescFieldValue(desc.id, "colorAttachments", color_attachments); 797 | } 798 | 799 | pub fn setDepthStencilAttachment( 800 | desc: *RenderPassDesc, 801 | depth_stencil_attachment: gfx.DepthStencilAttachment, 802 | ) void { 803 | if (desc.id == js.default_desc_id) { 804 | desc.id = js.initDesc(); 805 | } 806 | setDescFieldValue( 807 | desc.id, 808 | "depthStencilAttachment", 809 | depth_stencil_attachment, 810 | ); 811 | } 812 | }; 813 | 814 | pub const RenderPass = struct { 815 | id: js.RenderPassId, 816 | 817 | pub fn setPipeline( 818 | render_pass: *RenderPass, 819 | render_pipeline: *const RenderPipeline, 820 | ) !void { 821 | js.setPipeline(render_pass.id, render_pipeline.id); 822 | } 823 | 824 | pub fn setBindGroup( 825 | render_pass: *RenderPass, 826 | group_index: u32, 827 | group: *const BindGroup, 828 | dynamic_offsets: ?[]const u32, 829 | ) !void { 830 | const offsets = if (dynamic_offsets) |offsets| 831 | std.mem.sliceAsBytes(offsets) 832 | else 833 | &[_]u8{}; 834 | 835 | js.setBindGroup( 836 | render_pass.id, 837 | group_index, 838 | group.id, 839 | offsets.ptr, 840 | offsets.len, 841 | ); 842 | } 843 | 844 | pub fn setVertexBuffer( 845 | render_pass: *RenderPass, 846 | slot: u32, 847 | buffer: *const Buffer, 848 | offset: u32, 849 | size: usize, 850 | ) !void { 851 | js.setVertexBuffer(render_pass.id, slot, buffer.id, offset, size); 852 | } 853 | 854 | pub fn setIndexBuffer( 855 | render_pass: *RenderPass, 856 | buffer: *const Buffer, 857 | index_format: gfx.IndexFormat, 858 | offset: u32, 859 | size: usize, 860 | ) !void { 861 | const fmt_name = getEnumName(index_format); 862 | js.setIndexBuffer( 863 | render_pass.id, 864 | buffer.id, 865 | fmt_name.ptr, 866 | fmt_name.len, 867 | offset, 868 | size, 869 | ); 870 | } 871 | 872 | pub fn draw( 873 | render_pass: *RenderPass, 874 | vertex_count: usize, 875 | instance_count: usize, 876 | first_vertex: usize, 877 | first_instance: usize, 878 | ) !void { 879 | js.draw( 880 | render_pass.id, 881 | vertex_count, 882 | instance_count, 883 | first_vertex, 884 | first_instance, 885 | ); 886 | } 887 | 888 | pub fn drawIndexed( 889 | render_pass: *RenderPass, 890 | index_count: usize, 891 | instance_count: usize, 892 | first_index: usize, 893 | base_vertex: i32, 894 | first_instance: usize, 895 | ) !void { 896 | js.drawIndexed( 897 | render_pass.id, 898 | index_count, 899 | instance_count, 900 | first_index, 901 | base_vertex, 902 | first_instance, 903 | ); 904 | } 905 | 906 | pub fn end(render_pass: *RenderPass) !void { 907 | js.endRenderPass(render_pass.id); 908 | } 909 | }; 910 | 911 | pub const Queue = struct { 912 | id: js.DeviceId, 913 | 914 | pub fn writeBuffer( 915 | queue: *Queue, 916 | buffer: *const Buffer, 917 | buffer_offset: usize, 918 | data: []const u8, 919 | data_offset: usize, 920 | ) !void { 921 | js.queueWriteBuffer( 922 | queue.id, 923 | buffer.id, 924 | buffer_offset, 925 | data.ptr, 926 | data.len, 927 | data_offset, 928 | ); 929 | } 930 | 931 | pub fn writeTexture( 932 | queue: *Queue, 933 | destination: gfx.ImageCopyTexture, 934 | data: []const u8, 935 | data_layout: gfx.ImageDataLayout, 936 | size: gfx.Extent3d, 937 | ) !void { 938 | var destination_desc = js.initDesc(); 939 | defer js.deinitDesc(destination_desc); 940 | setDesc(destination_desc, destination); 941 | 942 | var data_layout_desc = js.initDesc(); 943 | defer js.deinitDesc(data_layout_desc); 944 | setDesc(data_layout_desc, data_layout); 945 | 946 | js.queueWriteTexture( 947 | queue.id, 948 | destination_desc, 949 | data.ptr, 950 | data.len, 951 | data_layout_desc, 952 | size.width, 953 | size.height, 954 | size.depth_or_array_layers, 955 | ); 956 | } 957 | 958 | pub fn submit(queue: *Queue, command_buffers: []const gfx.CommandBuffer) !void { 959 | for (command_buffers) |command_buffer| { 960 | js.queueSubmit(queue.id, command_buffer.impl.id); 961 | } 962 | } 963 | }; 964 | -------------------------------------------------------------------------------- /src/gfx_webgpu.js: -------------------------------------------------------------------------------- 1 | const _InvalidId = 0; 2 | const _DefaultDescId = 0; 3 | const _WholeSize = 0xFFFFFFFF; 4 | const _BindTypeBuffer = 0; 5 | const _BindTypeTextureView = 1; 6 | const _BindTypeSampler = 2; 7 | 8 | const _webgpu = { 9 | _descs: new _Objs(), 10 | _contexts: new _Objs(), 11 | _contexts: new _Objs(), 12 | _adapters: new _Objs(), 13 | _devices: new _Objs(), 14 | _shaders: new _Objs(), 15 | _bindGroupLayouts: new _Objs(), 16 | _bindGroups: new _Objs(), 17 | _pipelineLayouts: new _Objs(), 18 | _renderPipelines: new _Objs(), 19 | _buffers: new _Objs(), 20 | _textures: new _Objs(), 21 | _samplers: new _Objs(), 22 | _commandEncoders: new _Objs(), 23 | _commandBuffers: new _Objs(), 24 | _renderPassDescs: new _Objs(), 25 | _renderPasses: new _Objs(), 26 | _querySets: new _Objs(), 27 | 28 | initDesc() { 29 | return _webgpu._descs._insert({ 30 | _stack: [{ _obj: {}, _field: null, _array: false }], 31 | }); 32 | }, 33 | 34 | deinitDesc(_descId) { 35 | if (_descId != _DefaultDescId) { 36 | _webgpu._descs._remove(_descId); 37 | } 38 | }, 39 | 40 | setDescField(_descId, _fieldPtr, _fieldLen) { 41 | _webgpu._getDesc(_descId)._field = _utils._getString(_fieldPtr, _fieldLen); 42 | }, 43 | 44 | setDescString(_descId, _valuePtr, _valueLen) { 45 | _webgpu._setDescValue(_descId, _utils._getString(_valuePtr, _valueLen)); 46 | }, 47 | 48 | setDescBool(_descId, _value) { 49 | _webgpu._setDescValue(_descId, !!_value); 50 | }, 51 | 52 | setDescU32(_descId, _value) { 53 | _webgpu._setDescValue(_descId, _value >>> 0); 54 | }, 55 | 56 | setDescI32(_descId, _value) { 57 | _webgpu._setDescValue(_descId, _value); 58 | }, 59 | 60 | setDescF32(_descId, _value) { 61 | _webgpu._setDescValue(_descId, _value); 62 | }, 63 | 64 | beginDescArray(_descId) { 65 | const _desc = _webgpu._getDesc(_descId); 66 | _desc._obj[_desc._field] = []; 67 | _desc._array = true; 68 | }, 69 | 70 | endDescArray(_descId) { 71 | _webgpu._getDesc(_descId)._array = false; 72 | }, 73 | 74 | beginDescChild(_descId) { 75 | _webgpu._descs._get(_descId)._stack.push({ 76 | _obj: {}, 77 | _field: null, 78 | _array: false 79 | }); 80 | }, 81 | 82 | endDescChild(_descId) { 83 | const _desc = _webgpu._descs._get(_descId); 84 | const _obj = _desc._stack[_desc._stack.length - 1]._obj; 85 | _desc._stack.pop(); 86 | _webgpu._setDescValue(_descId, _obj); 87 | }, 88 | 89 | _setDescValue(_descId, _value) { 90 | const _desc = _webgpu._getDesc(_descId); 91 | if (_desc._array) { 92 | _desc._obj[_desc._field].push(_value); 93 | } else { 94 | _desc._obj[_desc._field] = _value; 95 | } 96 | }, 97 | 98 | _getDesc(_descId) { 99 | if (_descId === _DefaultDescId) { 100 | return { _obj: {} }; 101 | } 102 | const _desc = _webgpu._descs._get(_descId); 103 | return _desc._stack[_desc._stack.length - 1]; 104 | }, 105 | 106 | _getDescObj(_descId) { 107 | return _webgpu._getDesc(_descId)._obj; 108 | }, 109 | 110 | createContext(_canvasIdPtr, _canvasIdLen) { 111 | const _canvasId = _utils._getString(_canvasIdPtr, _canvasIdLen); 112 | return _webgpu._contexts._insert({ 113 | _obj: document.getElementById(_canvasId).getContext("webgpu"), 114 | _texId: _webgpu._textures._insert({}), 115 | }); 116 | }, 117 | 118 | destroyContext(_contextId) { 119 | _webgpu._textures._remove(_webgpu._contexts._get(_contextId)._texId); 120 | _webgpu._contexts._remove(_contextId); 121 | }, 122 | 123 | getContextCurrentTexture(_contextId) { 124 | const _context = _webgpu._contexts._get(_contextId); 125 | _webgpu._textures._set({ 126 | _obj: _context._obj.getCurrentTexture(), 127 | _views: new _Objs() 128 | }, _context._texId); 129 | return _context._texId; 130 | }, 131 | 132 | configure(_deviceId, _contextId, _descId) { 133 | const _desc = _webgpu._getDescObj(_descId); 134 | _desc.device = _webgpu._devices._get(_deviceId); 135 | _webgpu._contexts._getObj(_contextId).configure(_desc); 136 | }, 137 | 138 | getPreferredFormat() { 139 | const _format = navigator.gpu.getPreferredCanvasFormat(); 140 | 141 | // format must be one of the supported context formats: 142 | if (_format === "bgra8unorm") { 143 | return 22; 144 | } else if (_format === "rgba8unorm") { 145 | return 17; 146 | } else if (_format === "rgba16float") { 147 | return 32; 148 | } else { 149 | console.log("unexpected preferred format:", _format); 150 | return -1; 151 | } 152 | }, 153 | 154 | requestAdapter(_descId) { 155 | navigator.gpu.requestAdapter(_webgpu._getDescObj(_descId)) 156 | .then(_adapter => { 157 | _utils._getWasm().requestAdapterComplete( 158 | _webgpu._adapters._insert(_adapter) 159 | ); 160 | }) 161 | .catch(_err => { 162 | console.log(_err); 163 | _utils._getWasm().requestAdapterComplete(_InvalidId); 164 | }); 165 | }, 166 | 167 | destroyAdapter(_adapterId) { 168 | _webgpu._adapters._remove(_adapterId); 169 | }, 170 | 171 | requestDevice(_adapterId, _descId) { 172 | _webgpu._adapters._get(_adapterId) 173 | .requestDevice(_webgpu._getDescObj(_descId)) 174 | .then(_device => { 175 | _utils._getWasm().requestDeviceComplete( 176 | _webgpu._devices._insert(_device) 177 | ); 178 | }) 179 | .catch(_err => { 180 | console.log(_err); 181 | _utils._getWasm().requestDeviceComplete(_InvalidId); 182 | }); 183 | }, 184 | 185 | destroyDevice(_deviceId) { 186 | // device destroy should be in the api, 187 | // but it's not available in chrome canary yet... 188 | _webgpu._devices._remove(_deviceId); 189 | }, 190 | 191 | createShader(_deviceId, _codePtr, _codeLen) { 192 | const _desc = {}; 193 | _desc.code = _utils._getString(_codePtr, _codeLen); 194 | return _webgpu._shaders._insert( 195 | _webgpu._devices._get(_deviceId).createShaderModule(_desc) 196 | ); 197 | }, 198 | 199 | destroyShader(_shaderId) { 200 | _webgpu._shaders._remove(_shaderId); 201 | }, 202 | 203 | checkShaderCompile(_shaderId) { 204 | _webgpu._shaders._get(_shaderId).compilationInfo() 205 | .then(_info => { 206 | let _err = false; 207 | for (let i = 0; i < _info.messages.length; ++i) { 208 | const _msg = _info.messages[i]; 209 | console.log( 210 | "line:", 211 | _msg.lineNum, 212 | "col:", 213 | _msg.linePos, 214 | _msg.message 215 | ); 216 | _err |= _msg.type == "error"; 217 | } 218 | _utils._getWasm().checkShaderCompileComplete(_err); 219 | }); 220 | }, 221 | 222 | createBuffer(_deviceId, _descId, _dataPtr, _dataLen) { 223 | const _desc = _webgpu._getDescObj(_descId); 224 | _desc.mappedAtCreation = _dataLen > 0; 225 | const _buffer = _webgpu._devices._get(_deviceId).createBuffer(_desc); 226 | 227 | if (_desc.mappedAtCreation) { 228 | const _src = new Uint8Array(_utils._u8Array(_dataPtr, _dataLen)); 229 | const _dst = new Uint8Array(_buffer.getMappedRange()); 230 | _dst.set(_src); 231 | _buffer.unmap(); 232 | } 233 | 234 | return _webgpu._buffers._insert(_buffer); 235 | }, 236 | 237 | destroyBuffer(_bufferId) { 238 | _webgpu._buffers._get(_bufferId).destroy(); 239 | _webgpu._buffers._remove(_bufferId); 240 | }, 241 | 242 | createTexture(_deviceId, _descId) { 243 | const _desc = _webgpu._getDescObj(_descId); 244 | return _webgpu._textures._insert({ 245 | _obj: _webgpu._devices._get(_deviceId).createTexture(_desc), 246 | _views: new _Objs() 247 | }); 248 | }, 249 | 250 | destroyTexture(_textureId) { 251 | // texture destroy should be in the api, 252 | // but it's not available in chrome canary yet... 253 | _webgpu._textures._remove(_textureId); 254 | }, 255 | 256 | createTextureView(_descId) { 257 | const _desc = _webgpu._getDescObj(_descId); 258 | const _texture = _webgpu._textures._get(_desc.texture); 259 | const _view = _texture._views._insert(_texture._obj.createView(_desc)); 260 | return (_desc.texture << 16) | _view; 261 | }, 262 | 263 | destroyTextureView(_textureViewId) { 264 | _webgpu._textures._get(_textureViewId >>> 16)._views._remove( 265 | _textureViewId & 0x0000FFFF 266 | ); 267 | }, 268 | 269 | _getTextureView(_textureViewId) { 270 | return _webgpu._textures._get(_textureViewId >>> 16)._views._get( 271 | _textureViewId & 0x0000FFFF 272 | ); 273 | }, 274 | 275 | createSampler(_deviceId, _descId) { 276 | return _webgpu._samplers._insert( 277 | _webgpu._devices._get(_deviceId).createSampler( 278 | _webgpu._getDescObj(_descId) 279 | ) 280 | ); 281 | }, 282 | 283 | destroySampler(_samplerId) { 284 | _webgpu._samplers._remove(_samplerId); 285 | }, 286 | 287 | createBindGroupLayout(_deviceId, _descId) { 288 | const _desc = _webgpu._getDescObj(_descId); 289 | return _webgpu._bindGroupLayouts._insert( 290 | _webgpu._devices._get(_deviceId).createBindGroupLayout(_desc) 291 | ); 292 | }, 293 | 294 | destroyBindGroupLayout(_bindGroupLayoutId) { 295 | _webgpu._bindGroupLayouts._remove(_bindGroupLayoutId); 296 | }, 297 | 298 | createBindGroup(_deviceId, _descId) { 299 | const _desc = _webgpu._getDescObj(_descId); 300 | _desc.layout = _webgpu._bindGroupLayouts._get(_desc.layout); 301 | for (let i = 0; i < _desc.entries.length; ++i) { 302 | switch (_desc.entries[i].BindingResourceType) { 303 | case _BindTypeBuffer: 304 | _desc.entries[i].resource.buffer = _webgpu._buffers._get( 305 | _desc.entries[i].resource.buffer 306 | ); 307 | if (_desc.entries[i].resource.size == _WholeSize) { 308 | _desc.entries[i].resource.size = undefined; 309 | } 310 | break; 311 | case _BindTypeTextureView: 312 | _desc.entries[i].resource = _webgpu._getTextureView( 313 | _desc.entries[i].resource 314 | ); 315 | break; 316 | case _BindTypeSampler: 317 | _desc.entries[i].resource = _webgpu._samplers._get( 318 | _desc.entries[i].resource 319 | ); 320 | break; 321 | } 322 | } 323 | 324 | return _webgpu._bindGroups._insert( 325 | _webgpu._devices._get(_deviceId).createBindGroup(_desc) 326 | ); 327 | }, 328 | 329 | destroyBindGroup(_bindGroupId) { 330 | _webgpu._bindGroups._remove(_bindGroupId); 331 | }, 332 | 333 | createPipelineLayout(_deviceId, _descId) { 334 | const _desc = _webgpu._getDescObj(_descId); 335 | for (let i = 0; i < _desc.bindGroupLayouts.length; ++i) { 336 | _desc.bindGroupLayouts[i] = _webgpu._bindGroupLayouts._get( 337 | _desc.bindGroupLayouts[i] 338 | ); 339 | } 340 | return _webgpu._pipelineLayouts._insert( 341 | _webgpu._devices._get(_deviceId).createPipelineLayout(_desc) 342 | ); 343 | }, 344 | 345 | destroyPipelineLayout(_pipelineLayoutId) { 346 | _webgpu._pipelineLayouts._remove(_pipelineLayoutId); 347 | }, 348 | 349 | createRenderPipeline(_deviceId, _descId) { 350 | const _desc = _webgpu._getDescObj(_descId); 351 | if (_desc.layout !== undefined) { 352 | _desc.layout = _webgpu._pipelineLayouts._get(_desc.layout); 353 | } else { 354 | _desc.layout = "auto"; 355 | } 356 | _desc.vertex.module = _webgpu._shaders._get(_desc.vertex.module); 357 | if (_desc.fragment !== undefined) { 358 | _desc.fragment.module = _webgpu._shaders._get(_desc.fragment.module); 359 | } 360 | return _webgpu._renderPipelines._insert( 361 | _webgpu._devices._get(_deviceId).createRenderPipeline(_desc) 362 | ); 363 | }, 364 | 365 | destroyRenderPipeline(_renderPipelineId) { 366 | _webgpu._renderPipelines._remove(_renderPipelineId); 367 | }, 368 | 369 | createCommandEncoder(_deviceId) { 370 | return _webgpu._commandEncoders._insert( 371 | _webgpu._devices._get(_deviceId).createCommandEncoder() 372 | ); 373 | }, 374 | 375 | finishCommandEncoder(_commandEncoderId) { 376 | const _commandBufferId = _webgpu._commandBuffers._insert( 377 | _webgpu._commandEncoders._get(_commandEncoderId).finish() 378 | ); 379 | _webgpu._commandEncoders._remove(_commandEncoderId); 380 | return _commandBufferId; 381 | }, 382 | 383 | beginRenderPass(_commandEncoderId, _descId) { 384 | const _desc = _webgpu._getDescObj(_descId); 385 | for (let i = 0; i < _desc.colorAttachments.length; ++i) { 386 | _desc.colorAttachments[i].view = _webgpu._getTextureView( 387 | _desc.colorAttachments[i].view 388 | ); 389 | 390 | if (_desc.colorAttachments[i].resolveTarget !== undefined) { 391 | _desc.colorAttachments[i].resolveTarget = _webgpu._getTextureView( 392 | _desc.colorAttachments[i].resolveTarget 393 | ); 394 | } 395 | } 396 | 397 | if (_desc.depthStencilAttachment !== undefined) { 398 | _desc.depthStencilAttachment.view = _webgpu._getTextureView( 399 | _desc.depthStencilAttachment.view 400 | ); 401 | } 402 | 403 | if (_desc.occlusionQuerySet !== undefined) { 404 | _desc.occlusionQuerySet = _webgpu._querySets._get( 405 | _desc.occlusionQuerySet 406 | ); 407 | } 408 | 409 | if (_desc.timestampWrites !== undefined) { 410 | for (let i = 0; i < _desc.timestampWrites.length; ++i) { 411 | _desc.timestampWrites[i].querySet = _webgpu._querySets._get( 412 | _desc.timestampWrites[i].querySet 413 | ); 414 | } 415 | } 416 | 417 | return _webgpu._renderPasses._insert( 418 | _webgpu._commandEncoders._get(_commandEncoderId).beginRenderPass(_desc) 419 | ); 420 | }, 421 | 422 | setPipeline(_renderPassId, _pipelineId) { 423 | _webgpu._renderPasses._get(_renderPassId).setPipeline( 424 | _webgpu._renderPipelines._get(_pipelineId) 425 | ); 426 | }, 427 | 428 | setBindGroup( 429 | _renderPassId, 430 | _groupIndex, 431 | _bindGroupId, 432 | _dynOffsetsPtr, 433 | _dynOffsetsLen 434 | ) { 435 | const _dynOffsets = []; 436 | if (_dynOffsetsLen > 0) { 437 | _dynOffsets = _utils._u32Array(_dynOffsetsPtr, _dynOffsetsLen); 438 | } 439 | _webgpu._renderPasses._get(_renderPassId).setBindGroup( 440 | _groupIndex, 441 | _webgpu._bindGroups._get(_bindGroupId), 442 | _dynOffsets 443 | ); 444 | }, 445 | 446 | setVertexBuffer(_renderPassId, _slot, _bufferId, _offset, _size) { 447 | if ((_size >>> 0) === _WholeSize) { 448 | _size = undefined; 449 | } 450 | 451 | _webgpu._renderPasses._get(_renderPassId).setVertexBuffer( 452 | _slot, 453 | _webgpu._buffers._get(_bufferId), 454 | _offset, 455 | _size 456 | ); 457 | }, 458 | 459 | setIndexBuffer( 460 | _renderPassId, 461 | _bufferId, 462 | _indexFormatPtr, 463 | _indexFormatLen, 464 | _offset, 465 | _size 466 | ) { 467 | if ((_size >>> 0) === _WholeSize) { 468 | _size = undefined; 469 | } 470 | 471 | _webgpu._renderPasses._get(_renderPassId).setIndexBuffer( 472 | _webgpu._buffers._get(_bufferId), 473 | _utils._getString(_indexFormatPtr, _indexFormatLen), 474 | _offset, 475 | _size 476 | ); 477 | }, 478 | 479 | draw( 480 | _renderPassId, 481 | _vertexCount, 482 | _instanceCount, 483 | _firstVertex, 484 | _firstInstance 485 | ) { 486 | _webgpu._renderPasses._get(_renderPassId).draw( 487 | _vertexCount, 488 | _instanceCount, 489 | _firstVertex, 490 | _firstInstance 491 | ); 492 | }, 493 | 494 | drawIndexed( 495 | _renderPassId, 496 | _indexCount, 497 | _instCount, 498 | _firstIndex, 499 | _baseVertex, 500 | _firstInstance 501 | ) { 502 | _webgpu._renderPasses._get(_renderPassId).drawIndexed( 503 | _indexCount, 504 | _instCount, 505 | _firstIndex, 506 | _baseVertex, 507 | _firstInstance 508 | ); 509 | }, 510 | 511 | endRenderPass(_renderPassId) { 512 | _webgpu._renderPasses._get(_renderPassId).end(); 513 | _webgpu._renderPasses._remove(_renderPassId); 514 | }, 515 | 516 | queueSubmit(_deviceId, _commandBufferId) { 517 | _webgpu._devices._get(_deviceId).queue.submit( 518 | [_webgpu._commandBuffers._get(_commandBufferId)] 519 | ); 520 | _webgpu._commandBuffers._remove(_commandBufferId); 521 | }, 522 | 523 | queueWriteBuffer( 524 | _deviceId, 525 | _bufferId, 526 | _bufferOffset, 527 | _dataPtr, 528 | _dataLen, 529 | _dataOffset 530 | ) { 531 | _webgpu._devices._get(_deviceId).queue.writeBuffer( 532 | _webgpu._buffers._get(_bufferId), 533 | _bufferOffset, 534 | _utils._getWasm().memory.buffer, 535 | _dataPtr + _dataOffset, 536 | _dataLen 537 | ); 538 | }, 539 | 540 | queueWriteTexture( 541 | _deviceId, 542 | _destId, 543 | _dataPtr, 544 | _dataLen, 545 | _layoutId, 546 | _width, 547 | _height, 548 | _depth 549 | ) { 550 | const _dest = _webgpu._getDescObj(_destId); 551 | _dest.texture = _webgpu._textures._getObj(_dest.texture); 552 | const _layout = _webgpu._getDescObj(_layoutId); 553 | _layout.offset += _dataPtr; 554 | _webgpu._devices._get(_deviceId).queue.writeTexture( 555 | _dest, 556 | _utils._getWasm().memory.buffer, 557 | _layout, 558 | [_width, _height, _depth] 559 | ); 560 | } 561 | }; 562 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const api = switch (cfg.platform) { 2 | .web => @import("main_web.zig"), 3 | .win => @compileError("Not yet implemented!"), 4 | }; 5 | const app = @import("app"); 6 | const bo = @import("build_options"); 7 | const cfg = @import("cfg.zig"); 8 | const std = @import("std"); 9 | 10 | pub fn log( 11 | comptime message_level: std.log.Level, 12 | comptime scope: @Type(.EnumLiteral), 13 | comptime format: []const u8, 14 | args: anytype, 15 | ) void { 16 | if (log_enabled) { 17 | api.log(message_level, scope, format, args); 18 | } 19 | } 20 | 21 | const log_enabled = bo.log_enabled; 22 | pub const log_level = @intToEnum(std.log.Level, @enumToInt(bo.log_level)); 23 | 24 | const AppData = if (@hasDecl(app, "init")) 25 | block: { 26 | const return_type = @typeInfo(@TypeOf(app.init)).Fn.return_type.?; 27 | switch (@typeInfo(return_type)) { 28 | .ErrorUnion => |EU| break :block EU.payload, 29 | else => @compileError("init return type must be an error union!"), 30 | } 31 | } else void; 32 | 33 | var app_data: AppData = undefined; 34 | 35 | pub fn init() !void { 36 | if (@hasDecl(app, "init")) { 37 | if (AppData == void) { 38 | try app.init(); 39 | } else { 40 | app_data = try app.init(); 41 | } 42 | } 43 | } 44 | 45 | pub fn loop() !void { 46 | if (@hasDecl(app, "loop")) { 47 | if (AppData == void) { 48 | try app.loop(); 49 | } else { 50 | const app_data_ref = if (@typeInfo(AppData) == .Pointer) 51 | app_data 52 | else 53 | &app_data; 54 | try app.loop(app_data_ref); 55 | } 56 | } 57 | } 58 | 59 | pub fn deinit() !void { 60 | if (@hasDecl(app, "deinit")) { 61 | if (AppData == void) { 62 | try app.deinit(); 63 | } else { 64 | const app_data_ref = if (@typeInfo(AppData) == .Pointer) 65 | app_data 66 | else 67 | &app_data; 68 | try app.deinit(app_data_ref); 69 | } 70 | } 71 | } 72 | 73 | usingnamespace api.entry; 74 | -------------------------------------------------------------------------------- /src/main_web.js: -------------------------------------------------------------------------------- 1 | const _main = { 2 | _wasms: new _Objs(), 3 | _currentWasmId: 0, 4 | 5 | _run(_wasmPath) { 6 | const _imports = {}; 7 | _imports.env = { 8 | ..._main, 9 | ..._res, 10 | ..._time, 11 | ..._webgpu, 12 | ..._wnd, 13 | }; 14 | 15 | fetch(_wasmPath) 16 | .then(_response => _response.arrayBuffer()) 17 | .then(_arrayBuffer => WebAssembly.instantiate(_arrayBuffer, _imports)) 18 | .then(_mod => { 19 | _main._currentWasmId = _main._wasms._insert(_mod.instance.exports); 20 | _main._wasms._get(_main._currentWasmId).initApp(); 21 | window.requestAnimationFrame(_main._loop); 22 | }) 23 | .catch(_err => console.log(_err)); 24 | }, 25 | 26 | _loop(_timestamp) { 27 | // console.log(performance.memory.totalJSHeapSize); 28 | for (let i = _main._wasms._begin(); 29 | i < _main._wasms._end(); 30 | i = _main._wasms._next(i)) 31 | { 32 | _main._currentWasmId = i; 33 | _main._wasms._get(i).loopApp(); 34 | } 35 | window.requestAnimationFrame(_main._loop); 36 | }, 37 | 38 | logConsole(_msgPtr, _msgLen) { 39 | console.log(_utils._getString(_msgPtr, _msgLen)); 40 | }, 41 | }; 42 | 43 | function ccGetWasmModule() { 44 | return _main._wasms._get(_main._currentWasmId); 45 | } 46 | 47 | function ccRun(_wasmPath) { 48 | _main._run(_wasmPath); 49 | } 50 | -------------------------------------------------------------------------------- /src/main_web.zig: -------------------------------------------------------------------------------- 1 | const main = @import("main.zig"); 2 | const std = @import("std"); 3 | 4 | const js = struct { 5 | extern fn logConsole(msg_ptr: [*]const u8, msg_len: usize) void; 6 | }; 7 | 8 | pub fn log( 9 | comptime message_level: std.log.Level, 10 | comptime scope: @Type(.EnumLiteral), 11 | comptime format: []const u8, 12 | args: anytype, 13 | ) void { 14 | const level_txt = comptime message_level.asText(); 15 | const prefix = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): "; 16 | const msg = level_txt ++ prefix ++ format; 17 | 18 | var log_buf: [2048]u8 = undefined; 19 | const msg_buf = std.fmt.bufPrint(&log_buf, msg, args) catch return; 20 | js.logConsole(msg_buf.ptr, msg_buf.len); 21 | } 22 | 23 | pub const entry = struct { 24 | // store the async calls in these variables in order to prevent the stack from 25 | // reclaiming the frame memory once the export fn completes 26 | var init_frame: @Frame(initAppAsync) = undefined; 27 | var loop_frame: @Frame(loopAppAsync) = undefined; 28 | var deinit_frame: @Frame(deinitAppAsync) = undefined; 29 | 30 | // set to true when the current async function is complete 31 | var async_complete = false; 32 | 33 | pub export fn initApp() void { 34 | init_frame = async initAppAsync(); 35 | } 36 | 37 | fn initAppAsync() void { 38 | main.init() catch |err| handleError(err); 39 | async_complete = true; 40 | } 41 | 42 | pub export fn loopApp() void { 43 | if (async_complete) { 44 | loop_frame = async loopAppAsync(); 45 | } 46 | } 47 | 48 | fn loopAppAsync() void { 49 | async_complete = false; 50 | main.loop() catch |err| handleError(err); 51 | async_complete = true; 52 | } 53 | 54 | pub export fn deinitApp() void { 55 | deinit_frame = async deinitAppAsync(); 56 | } 57 | 58 | fn deinitAppAsync() void { 59 | main.deinit() catch |err| handleError(err); 60 | } 61 | 62 | fn handleError(err: anyerror) noreturn { 63 | std.log.err("{}", .{err}); 64 | @panic(""); 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /src/math.zig: -------------------------------------------------------------------------------- 1 | pub usingnamespace @import("zmath"); 2 | -------------------------------------------------------------------------------- /src/mem.zig: -------------------------------------------------------------------------------- 1 | const api = switch (cfg.platform) { 2 | .web => @import("mem_web.zig"), 3 | .win => @compileError("Not yet implemented!"), 4 | }; 5 | const cfg = @import("cfg.zig"); 6 | const std = @import("std"); 7 | 8 | pub const BumpAllocator = struct { 9 | buffer: []u8, 10 | index: usize, 11 | 12 | pub fn init(size: usize) !BumpAllocator { 13 | const buffer = try api.alloc(size); 14 | return BumpAllocator{ 15 | .buffer = buffer, 16 | .index = buffer.len, 17 | }; 18 | } 19 | 20 | pub fn deinit(ba: *BumpAllocator) void { 21 | api.free(ba.buffer); 22 | } 23 | 24 | pub fn reset(ba: *BumpAllocator) void { 25 | ba.index = ba.buffer.len; 26 | } 27 | 28 | pub fn allocator(ba: *BumpAllocator) std.mem.Allocator { 29 | return std.mem.Allocator.init( 30 | ba, 31 | alloc, 32 | std.mem.Allocator.NoResize(BumpAllocator).noResize, 33 | std.mem.Allocator.NoOpFree(BumpAllocator).noOpFree, 34 | ); 35 | } 36 | 37 | fn alloc(ba: *BumpAllocator, n: usize, ptr_align: u29, _: u29, _: usize) ![]u8 { 38 | ba.index = std.math.sub(usize, ba.index, n) catch return error.OutOfMemory; 39 | ba.index &= ~(ptr_align - 1); 40 | return ba.buffer[ba.index .. ba.index + n]; 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /src/mem_web.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn alloc(size: usize) ![]u8 { 4 | const page_size_aligned = std.mem.alignForward(size, std.mem.page_size); 5 | const num_pages = page_size_aligned / std.mem.page_size; 6 | const page_idx = @wasmMemorySize(0); 7 | const result = @wasmMemoryGrow(0, num_pages); 8 | if (page_idx != result) { 9 | return error.OutOfMemory; 10 | } 11 | return @intToPtr([*]u8, page_idx * std.mem.page_size)[0..size]; 12 | } 13 | 14 | pub fn free(_: []u8) void {} 15 | -------------------------------------------------------------------------------- /src/minify.zig: -------------------------------------------------------------------------------- 1 | const cfg = @import("cfg.zig"); 2 | const std = @import("std"); 3 | 4 | /// minifier that removes whitespace and replaces any words starting with _ 5 | /// with smaller allocated identifiers 6 | const Minify = struct { 7 | const Language = enum { 8 | js, 9 | wgsl, 10 | }; 11 | 12 | const CharType = enum { 13 | whitespace, // any whitespace character 14 | symbol, // any character in the symbols string, see below 15 | ident, // any non-whitespace, non-symbol character 16 | }; 17 | 18 | const symbols = "{}()[]=<>;:.,|/-+*!&?"; 19 | const str_symbols = "\"'`"; 20 | const max_ident_size = 2; 21 | const next_ident_symbols = "abcdfghjklmnpqrstvwxzABCDEFGHIJKLMNOPQRSTUVWXYZ"; 22 | 23 | allocator: std.mem.Allocator, 24 | src: []const u8, 25 | out: std.ArrayList(u8), 26 | start_index: usize, 27 | end_index: usize, 28 | prev_char_type: CharType, 29 | prev_char_type_write: CharType, 30 | ident_map: std.StringHashMap([]const u8), 31 | next_ident: [max_ident_size]u8, 32 | next_ident_index: [max_ident_size]usize, 33 | next_ident_size: usize, 34 | language: Language, 35 | opt_level: cfg.OptLevel, 36 | 37 | fn init( 38 | allocator: std.mem.Allocator, 39 | src: []const u8, 40 | language: Language, 41 | opt_level: cfg.OptLevel, 42 | ) Minify { 43 | var ctx = Minify{ 44 | .allocator = allocator, 45 | .src = src, 46 | .out = std.ArrayList(u8).init(allocator), 47 | .start_index = 0, 48 | .end_index = 0, 49 | .prev_char_type = .whitespace, 50 | .prev_char_type_write = .whitespace, 51 | .ident_map = std.StringHashMap([]const u8).init(allocator), 52 | .next_ident = [_]u8{next_ident_symbols[0]} ** max_ident_size, 53 | .next_ident_index = [_]usize{0} ** max_ident_size, 54 | .next_ident_size = 1, 55 | .language = language, 56 | .opt_level = opt_level, 57 | }; 58 | 59 | return ctx; 60 | } 61 | 62 | fn deinit(ctx: *Minify) void { 63 | var it = ctx.ident_map.iterator(); 64 | while (it.next()) |kv| { 65 | ctx.allocator.free(kv.value_ptr.*); 66 | } 67 | ctx.ident_map.deinit(); 68 | } 69 | 70 | fn minify(ctx: *Minify) ![]const u8 { 71 | while (ctx.end_index < ctx.src.len) { 72 | const char = ctx.src[ctx.end_index]; 73 | if (ctx.end_index < ctx.src.len - 1 and 74 | char == '/' and 75 | (ctx.src[ctx.end_index + 1] == '/' or 76 | ctx.src[ctx.end_index + 1] == '*')) 77 | { 78 | try ctx.handleComment(); 79 | } else if (std.mem.indexOfScalar(u8, symbols, char) != null) { 80 | try ctx.handleCharType(.symbol); 81 | } else if (std.mem.indexOfScalar(u8, str_symbols, char) != null) { 82 | try ctx.handleString(); 83 | } else if (std.mem.indexOfScalar(u8, &std.ascii.spaces, char) != null) { 84 | try ctx.handleCharType(.whitespace); 85 | } else { 86 | try ctx.handleCharType(.ident); 87 | } 88 | } 89 | 90 | try ctx.flush(); 91 | 92 | return ctx.out.toOwnedSlice(); 93 | } 94 | 95 | fn flush(ctx: *Minify) !void { 96 | try ctx.handleCharType(.whitespace); 97 | } 98 | 99 | fn handleCharType(ctx: *Minify, char_type: CharType) !void { 100 | // check for the end of a char type run 101 | if (char_type != ctx.prev_char_type) { 102 | if (ctx.prev_char_type == .symbol) { 103 | try ctx.appendSymbol(); 104 | } else if (ctx.prev_char_type == .ident) { 105 | try ctx.appendIdent(); 106 | } 107 | 108 | if (char_type != .whitespace) { 109 | // append a space between two different identifiers 110 | if (ctx.prev_char_type_write == .ident and char_type == .ident) { 111 | try ctx.out.append(' '); 112 | } 113 | 114 | // chrome wgsl parser is broken, this works around the issue... 115 | if (ctx.language == .wgsl and 116 | ctx.prev_char_type_write == .symbol and 117 | char_type == .ident) 118 | { 119 | const wgsl_skip = "{([]<>=:;,."; 120 | const last_char = ctx.out.items[ctx.out.items.len - 1]; 121 | if (std.mem.indexOfScalar(u8, wgsl_skip, last_char) == null) { 122 | try ctx.out.append(' '); 123 | } 124 | } 125 | 126 | // keep track of the last char type written 127 | ctx.prev_char_type_write = char_type; 128 | } 129 | ctx.prev_char_type = char_type; 130 | ctx.start_index = ctx.end_index; 131 | } 132 | ctx.end_index += 1; 133 | } 134 | 135 | fn handleString(ctx: *Minify) !void { 136 | try ctx.flush(); 137 | 138 | const str_marker = ctx.src[ctx.end_index - 1]; 139 | ctx.start_index = ctx.end_index - 1; 140 | while (ctx.end_index < ctx.src.len and 141 | (ctx.src[ctx.end_index] != str_marker or 142 | ctx.src[ctx.end_index - 1] == '\\')) 143 | { 144 | ctx.end_index += 1; 145 | } 146 | ctx.end_index += 1; 147 | 148 | try ctx.out.appendSlice(ctx.src[ctx.start_index..ctx.end_index]); 149 | } 150 | 151 | fn handleComment(ctx: *Minify) !void { 152 | try ctx.flush(); 153 | const char = ctx.src[ctx.end_index]; 154 | if (char == '/') { 155 | // line comment 156 | while (ctx.src[ctx.end_index] != '\n') { 157 | ctx.end_index += 1; 158 | } 159 | } else { 160 | // block comment 161 | while (ctx.end_index < ctx.src.len - 1 and 162 | ctx.src[ctx.end_index] != '*' and 163 | ctx.src[ctx.end_index + 1] != '/') 164 | { 165 | ctx.end_index += 1; 166 | } 167 | } 168 | } 169 | 170 | fn appendSymbol(ctx: *Minify) !void { 171 | try ctx.out.appendSlice(ctx.src[ctx.start_index..ctx.end_index]); 172 | } 173 | 174 | fn appendIdent(ctx: *Minify) !void { 175 | const ident = ctx.src[ctx.start_index..ctx.end_index]; 176 | // append the identifier if in debug mode or if the identifier starts with _ 177 | // digits also cannot be converted into a different identifier 178 | if (ctx.opt_level == .dbg or 179 | ctx.src[ctx.start_index] != '_' or 180 | std.ascii.isDigit(ident[0])) 181 | { 182 | try ctx.out.appendSlice(ident); 183 | } else if (ctx.ident_map.getEntry(ident)) |entry| { 184 | // check if the identifier has already been parsed. 185 | // reuse the replacement identifier in that case 186 | try ctx.out.appendSlice(entry.value_ptr.*); 187 | } else { 188 | // otherwise, get a new replacement identifier 189 | const next_ident = try ctx.nextIdent(); 190 | try ctx.ident_map.put(ident, next_ident); 191 | try ctx.out.appendSlice(next_ident); 192 | } 193 | } 194 | 195 | fn nextIdent(ctx: *Minify) ![]const u8 { 196 | // identifiers are allocated from a string of characters. 197 | // they start off as single character identifiers 198 | const next = ctx.allocator.dupe(u8, ctx.next_ident[0..ctx.next_ident_size]); 199 | 200 | var index = ctx.next_ident_size - 1; 201 | if (ctx.next_ident_index[index] != next_ident_symbols.len - 1) { 202 | // each time an identifier is allocated, the char in the string is set 203 | // to the next one in next_ident_symbols... 204 | ctx.setNextIdent(index, ctx.next_ident_index[index] + 1); 205 | } else { 206 | // if the size is greater than 1, try to increment 207 | // the identifier in a previous string index 208 | ctx.setNextIdent(index, 0); 209 | var out_of_idents = true; 210 | while (index != 0) { 211 | index -= 1; 212 | if (ctx.next_ident_index[index] == next_ident_symbols.len - 1) { 213 | ctx.setNextIdent(index, 0); 214 | } else { 215 | ctx.setNextIdent(index, ctx.next_ident_index[index] + 1); 216 | out_of_idents = false; 217 | break; 218 | } 219 | } 220 | 221 | // if that didn't work, we need to increase the size of the string 222 | if (out_of_idents) { 223 | ctx.next_ident_size += 1; 224 | if (ctx.next_ident_size > max_ident_size) { 225 | return error.MaxIdentsExceeded; 226 | } 227 | } 228 | } 229 | 230 | return next; 231 | } 232 | 233 | fn setNextIdent(ctx: *Minify, ident_index: usize, symbol_index: usize) void { 234 | ctx.next_ident_index[ident_index] = symbol_index; 235 | ctx.next_ident[ident_index] = next_ident_symbols[symbol_index]; 236 | } 237 | }; 238 | 239 | pub fn js( 240 | allocator: std.mem.Allocator, 241 | src: []const u8, 242 | opt_level: cfg.OptLevel, 243 | ) ![]const u8 { 244 | var ctx = Minify.init(allocator, src, .js, opt_level); 245 | defer ctx.deinit(); 246 | 247 | return try ctx.minify(); 248 | } 249 | 250 | pub fn shader( 251 | allocator: std.mem.Allocator, 252 | src: []const u8, 253 | platform: cfg.Platform, 254 | opt_level: cfg.OptLevel, 255 | ) ![]const u8 { 256 | const lang: Minify.Language = switch (platform) { 257 | .web => .wgsl, 258 | else => return error.InvalidPlatform, 259 | }; 260 | 261 | var ctx = Minify.init(allocator, src, lang, opt_level); 262 | defer ctx.deinit(); 263 | 264 | return try ctx.minify(); 265 | } 266 | -------------------------------------------------------------------------------- /src/qoi.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub const Format = enum(u8) { 4 | rgb = 3, 5 | rgba = 4, 6 | }; 7 | 8 | pub const Colorspace = enum(u8) { 9 | srgb = 0, 10 | linear = 1, 11 | }; 12 | 13 | pub const Image = struct { 14 | width: u32, 15 | height: u32, 16 | data: []const u8, 17 | format: Format = .rgba, 18 | colorspace: Colorspace = .srgb, 19 | }; 20 | 21 | const Result = struct { 22 | bytes: []u8, 23 | len: usize, 24 | }; 25 | 26 | const Color = packed struct { 27 | r: u8, 28 | g: u8, 29 | b: u8, 30 | a: u8, 31 | 32 | fn hash(color: Color) u8 { 33 | return @truncate( 34 | u8, 35 | ((@as(u32, color.r) * 3) + 36 | (@as(u32, color.g) * 5) + 37 | (@as(u32, color.b) * 7) + 38 | (@as(u32, color.a) * 11)) % 64, 39 | ); 40 | } 41 | }; 42 | 43 | const header_len = 14; 44 | const magic = [4]u8{ 'q', 'o', 'i', 'f' }; 45 | const end_marker = [8]u8{ 0, 0, 0, 0, 0, 0, 0, 1 }; 46 | const op_index: u8 = 0b00000000; 47 | const op_diff: u8 = 0b01000000; 48 | const op_luma: u8 = 0b10000000; 49 | const op_run: u8 = 0b11000000; 50 | const op_rgb: u8 = 0b11111110; 51 | const op_rgba: u8 = 0b11111111; 52 | const mask_op_code: u8 = 0b11000000; 53 | const mask_index: u8 = 0b00111111; 54 | const mask_diff_r: u8 = 0b00110000; 55 | const mask_diff_g: u8 = 0b00001100; 56 | const mask_diff_b: u8 = 0b00000011; 57 | const mask_luma_r: u8 = 0b11110000; 58 | const mask_luma_g: u8 = 0b00111111; 59 | const mask_luma_b: u8 = 0b00001111; 60 | const mask_run: u8 = 0b00111111; 61 | const max_bytes: usize = 4000000000; 62 | 63 | pub fn encode(image: Image, allocator: std.mem.Allocator) !Result { 64 | const max_size = image.width * 65 | image.height * 66 | (@enumToInt(image.format) + 1) + 67 | header_len + 68 | end_marker.len; 69 | if (max_size > max_bytes) { 70 | return error.MaxBytesExceeded; 71 | } 72 | const bytes = try allocator.alloc(u8, max_size); 73 | errdefer allocator.free(bytes); 74 | 75 | // encode header 76 | std.mem.copy(u8, bytes[0..4], &magic); 77 | std.mem.writeIntBig(u32, bytes[4..8], image.width); 78 | std.mem.writeIntBig(u32, bytes[8..12], image.height); 79 | bytes[12] = @enumToInt(image.format); 80 | bytes[13] = @enumToInt(image.colorspace); 81 | 82 | // encode each pixel 83 | var bytes_index: usize = header_len; 84 | var run: u8 = 0; 85 | var lut: [64]Color = std.mem.zeroes([64]Color); 86 | var prev_pixel: Color = .{ .r = 0, .g = 0, .b = 0, .a = 255 }; 87 | const pixels = std.mem.bytesAsSlice(Color, image.data); 88 | for (pixels) |pixel| { 89 | if (@bitCast(u32, pixel) == @bitCast(u32, prev_pixel)) { 90 | // if the pixel matches the prev pixel, we are in a run 91 | run += 1; 92 | 93 | // if we hit the max length of a run, reset the run 94 | if (run == 62) { 95 | try writeBytes(bytes, &bytes_index, &[_]u8{op_run | run - 1}); 96 | run = 0; 97 | } 98 | } else { 99 | // otherwise, we have a new pixel 100 | // end an existing run if necessary 101 | if (run > 0) { 102 | try writeBytes(bytes, &bytes_index, &[_]u8{op_run | run - 1}); 103 | run = 0; 104 | } 105 | 106 | // see if the new pixel is in the lookup table 107 | const hash = pixel.hash(); 108 | if (@bitCast(u32, pixel) == @bitCast(u32, lut[hash])) { 109 | // if we are, write the hash 110 | try writeBytes(bytes, &bytes_index, &[_]u8{op_index | hash}); 111 | } else { 112 | // otherwise write the pixel to the lookup table 113 | lut[hash] = pixel; 114 | 115 | // check if we can encode RGB diff or luma 116 | if (pixel.a == prev_pixel.a) { 117 | const diff_r = @as(i16, pixel.r) - @as(i16, prev_pixel.r); 118 | const diff_g = @as(i16, pixel.g) - @as(i16, prev_pixel.g); 119 | const diff_b = @as(i16, pixel.b) - @as(i16, prev_pixel.b); 120 | 121 | const diff_rg = diff_r - diff_g; 122 | const diff_rb = diff_b - diff_g; 123 | 124 | if (diff_r >= -2 and diff_r <= 1 and 125 | diff_g >= -2 and diff_g <= 1 and 126 | diff_b >= -2 and diff_b <= 1) 127 | { 128 | // we can encode using a diff (only takes 1 byte) 129 | try writeBytes( 130 | bytes, 131 | &bytes_index, 132 | &[_]u8{op_diff | 133 | (@intCast(u8, diff_r + 2) << 4) | 134 | (@intCast(u8, diff_g + 2) << 2) | 135 | (@intCast(u8, diff_b + 2) << 0)}, 136 | ); 137 | } else if (diff_g >= -32 and diff_g <= 31 and 138 | diff_rg >= -8 and diff_rg <= 7 and 139 | diff_rb >= -8 and diff_rb <= 7) 140 | { 141 | // we can encode using luma (only takes 2 bytes) 142 | try writeBytes( 143 | bytes, 144 | &bytes_index, 145 | &[_]u8{ 146 | op_luma | @intCast(u8, diff_g + 32), 147 | @intCast(u8, diff_rg + 8) << 4 | 148 | @intCast(u8, diff_rb + 8) << 0, 149 | }, 150 | ); 151 | } else { 152 | // otherwise, we encode using rgb (4 bytes) 153 | try writeBytes( 154 | bytes, 155 | &bytes_index, 156 | &[_]u8{ op_rgb, pixel.r, pixel.g, pixel.b }, 157 | ); 158 | } 159 | } else { 160 | // unique alpha channel requires encoding rgba (5 bytes) 161 | try writeBytes( 162 | bytes, 163 | &bytes_index, 164 | &[_]u8{ op_rgba, pixel.r, pixel.g, pixel.b, pixel.a }, 165 | ); 166 | } 167 | } 168 | } 169 | 170 | prev_pixel = pixel; 171 | } 172 | 173 | if (run > 0) { 174 | try writeBytes(bytes, &bytes_index, &[_]u8{op_run | run - 1}); 175 | run = 0; 176 | } 177 | 178 | // encode end marker 179 | try writeBytes(bytes, &bytes_index, &end_marker); 180 | 181 | return Result{ 182 | .bytes = bytes, 183 | .len = bytes_index, 184 | }; 185 | } 186 | 187 | fn writeBytes(bytes: []u8, bytes_index: *usize, data: []const u8) !void { 188 | if (bytes_index.* + data.len > bytes.len) { 189 | return error.InvalidData; 190 | } 191 | std.mem.copy(u8, bytes[bytes_index.*..], data); 192 | bytes_index.* += data.len; 193 | } 194 | 195 | pub fn decode(data: []const u8, allocator: std.mem.Allocator) !Image { 196 | if (data.len < header_len) { 197 | return error.InvalidHeader; 198 | } 199 | 200 | // decode header 201 | if (!std.mem.eql(u8, data[0..4], magic[0..4])) { 202 | return error.InvalidMagic; 203 | } 204 | const width = std.mem.readIntBig(u32, data[4..8]); 205 | const height = std.mem.readIntBig(u32, data[8..12]); 206 | const format = data[12]; 207 | const colorspace = data[13]; 208 | 209 | _ = std.meta.intToEnum(Format, format) catch return error.InvalidHeader; 210 | _ = std.meta.intToEnum(Colorspace, colorspace) catch return error.InvalidHeader; 211 | 212 | if (width * height * format > max_bytes) { 213 | return error.MaxBytesExceeded; 214 | } 215 | 216 | const bytes = try allocator.alloc(u8, width * height * format); 217 | errdefer allocator.free(bytes); 218 | 219 | // decode each pixel of the image 220 | var lut: [64]Color = std.mem.zeroes([64]Color); 221 | var pixel: Color = .{ .r = 0, .g = 0, .b = 0, .a = 255 }; 222 | var data_index: usize = header_len; 223 | var bytes_index: usize = 0; 224 | while (bytes_index < bytes.len) { 225 | if (data_index + 1 > data.len) { 226 | return error.InvalidDataLength; 227 | } 228 | const op = data[data_index]; 229 | if (op == op_rgb) { 230 | if (data_index + 4 > data.len) { 231 | return error.InvalidOpRgb; 232 | } 233 | pixel.r = data[data_index + 1]; 234 | pixel.g = data[data_index + 2]; 235 | pixel.b = data[data_index + 3]; 236 | data_index += 4; 237 | } else if (op == op_rgba) { 238 | if (data_index + 5 > data.len) { 239 | return error.InvalidOpRgba; 240 | } 241 | pixel.r = data[data_index + 1]; 242 | pixel.g = data[data_index + 2]; 243 | pixel.b = data[data_index + 3]; 244 | pixel.a = data[data_index + 4]; 245 | data_index += 5; 246 | } else { 247 | const op_code = op & mask_op_code; 248 | if (op_code == op_index) { 249 | pixel = lut[op & mask_index]; 250 | data_index += 1; 251 | } else if (op_code == op_diff) { 252 | // use wrapping adds to match the spec 253 | // even though the diffs are signed ints, they are still twos 254 | // complement numbers and the wrapping arithmetic works out 255 | pixel.r +%= ((op & mask_diff_r) >> 4) -% 2; 256 | pixel.g +%= ((op & mask_diff_g) >> 2) -% 2; 257 | pixel.b +%= ((op & mask_diff_b) >> 0) -% 2; 258 | data_index += 1; 259 | } else if (op_code == op_luma) { 260 | if (data_index + 2 > data.len) { 261 | return error.InvalidOpLuma; 262 | } 263 | // use wrapping adds to match the spec 264 | // even though the diffs are signed ints, they are still twos 265 | // complement numbers and the wrapping arithmetic works out 266 | const diff_g = (op & mask_luma_g) -% 32; 267 | const op_rb = data[data_index + 1]; 268 | pixel.r +%= diff_g +% ((op_rb & mask_luma_r) >> 4) -% 8; 269 | pixel.g +%= diff_g; 270 | pixel.b +%= diff_g +% ((op_rb & mask_luma_b) >> 0) -% 8; 271 | data_index += 2; 272 | } else if (op_code == op_run) { 273 | // a run is a continuous stream of the same pixel 274 | var run = (op & mask_run) + 1; 275 | if (bytes_index + format * (run - 1) > bytes.len) { 276 | return error.InvalidOpRun; 277 | } 278 | while (run > 1) : (run -= 1) { 279 | std.mem.copy( 280 | u8, 281 | bytes[bytes_index..], 282 | std.mem.asBytes(&pixel)[0..format], 283 | ); 284 | bytes_index += format; 285 | } 286 | data_index += 1; 287 | } 288 | } 289 | 290 | lut[pixel.hash()] = pixel; 291 | 292 | if (bytes_index + format > bytes.len) { 293 | return error.InvalidData; 294 | } 295 | std.mem.copy(u8, bytes[bytes_index..], std.mem.asBytes(&pixel)[0..format]); 296 | bytes_index += format; 297 | } 298 | 299 | // decode end marker 300 | if (data_index + 8 != data.len or 301 | !std.mem.eql(u8, data[data_index..], &end_marker)) 302 | { 303 | return error.InvalidEndMarker; 304 | } 305 | 306 | return Image{ .width = width, .height = height, .data = bytes }; 307 | } 308 | -------------------------------------------------------------------------------- /src/res.zig: -------------------------------------------------------------------------------- 1 | const api = switch (cfg.platform) { 2 | .web => @import("res_web.zig"), 3 | .win => @compileError("Not yet implemented!"), 4 | }; 5 | const cfg = @import("cfg.zig"); 6 | const serde = @import("serde.zig"); 7 | const std = @import("std"); 8 | 9 | pub const Res = struct { 10 | Type: type, 11 | data: Data, 12 | 13 | pub const Data = union(enum) { 14 | file: struct { 15 | path: []const u8, 16 | size: usize, 17 | }, 18 | embed: []const u8, 19 | }; 20 | }; 21 | 22 | pub const LoadDesc = struct { 23 | file_allocator: ?std.mem.Allocator = null, 24 | res_allocator: ?std.mem.Allocator = null, 25 | }; 26 | 27 | pub fn load(comptime res: Res, desc: LoadDesc) !res.Type { 28 | const bytes_are_embedded = comptime std.meta.activeTag(res.data) == .embed; 29 | const file_bytes = switch (res.data) { 30 | .embed => |e| e, 31 | .file => |f| block: { 32 | if (desc.file_allocator) |allocator| { 33 | break :block try readFile(allocator, f.path, f.size); 34 | } else { 35 | return error.AllocatorRequired; 36 | } 37 | }, 38 | }; 39 | defer if (!bytes_are_embedded) { 40 | desc.file_allocator.?.free(file_bytes); 41 | }; 42 | 43 | return try serde.deserialize( 44 | .{ 45 | .allocator = desc.res_allocator, 46 | .bytes_are_embedded = bytes_are_embedded, 47 | }, 48 | res.Type, 49 | file_bytes, 50 | ); 51 | } 52 | 53 | pub fn readFile( 54 | allocator: std.mem.Allocator, 55 | path: []const u8, 56 | size: usize, 57 | ) ![]const u8 { 58 | const data = try allocator.alloc(u8, size); 59 | try api.readFile(path, data); 60 | return data; 61 | } 62 | -------------------------------------------------------------------------------- /src/res_web.js: -------------------------------------------------------------------------------- 1 | const _res = { 2 | readFile(_namePtr, _nameLen, _filePtr, _fileLen) { 3 | fetch(_utils._getString(_namePtr, _nameLen)) 4 | .then(_response => _response.arrayBuffer()) 5 | .then(_arrayBuffer => { 6 | _utils._u8Array(_filePtr, _fileLen).set( 7 | new Uint8Array(_arrayBuffer) 8 | ); 9 | _utils._getWasm().readFileComplete(true); 10 | }) 11 | .catch(_err => { 12 | console.log(_err); 13 | _utils._getWasm().readFileComplete(false); 14 | }); 15 | } 16 | }; -------------------------------------------------------------------------------- /src/res_web.zig: -------------------------------------------------------------------------------- 1 | const js = struct { 2 | extern fn readFile( 3 | name_ptr: [*]const u8, 4 | name_len: usize, 5 | file_ptr: [*]const u8, 6 | file_len: usize, 7 | ) void; 8 | }; 9 | 10 | var read_file_frame: anyframe = undefined; 11 | var read_file_result: anyerror!void = undefined; 12 | 13 | pub fn readFile(name: []const u8, file: []u8) !void { 14 | try await async readFileAsync(name, file); 15 | } 16 | 17 | fn readFileAsync(name: []const u8, file: []u8) !void { 18 | js.readFile(name.ptr, name.len, file.ptr, file.len); 19 | suspend { 20 | read_file_frame = @frame(); 21 | } 22 | try read_file_result; 23 | } 24 | 25 | export fn readFileComplete(success: bool) void { 26 | if (success) { 27 | read_file_result = {}; 28 | } else { 29 | read_file_result = error.LoadFileFailed; 30 | } 31 | resume read_file_frame; 32 | } 33 | -------------------------------------------------------------------------------- /src/serde.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const serde_usize = u64; 4 | 5 | const SerdeError = error{ 6 | OutOfMemory, 7 | AllocatorRequired, 8 | }; 9 | 10 | pub fn serialize(allocator: std.mem.Allocator, value: anytype) ![]u8 { 11 | const Type = @TypeOf(value); 12 | if (@hasDecl(Type, "serialize")) { 13 | return try Type.serialize(allocator, value); 14 | } else { 15 | return try serializeGeneric(allocator, value); 16 | } 17 | } 18 | 19 | pub fn serializeGeneric(allocator: std.mem.Allocator, value: anytype) ![]u8 { 20 | const bytes = allocator.alloc(u8, serializeSize(value)) catch 21 | return error.OutOfMemory; 22 | _ = serializeBytes(value, bytes); 23 | return bytes; 24 | } 25 | 26 | fn serializeBytes(value: anytype, bytes: []u8) []u8 { 27 | var out_bytes = bytes; 28 | switch (@typeInfo(@TypeOf(value))) { 29 | .Void => {}, 30 | .Bool, .Int, .Float, .Enum => { 31 | const value_bytes = std.mem.toBytes(value); 32 | std.mem.copy(u8, out_bytes, &value_bytes); 33 | out_bytes = out_bytes[value_bytes.len..]; 34 | }, 35 | .Optional => { 36 | if (value) |v| { 37 | out_bytes = serializeBytes(true, out_bytes); 38 | out_bytes = serializeBytes(v, out_bytes); 39 | } else { 40 | out_bytes = serializeBytes(false, out_bytes); 41 | } 42 | }, 43 | .Pointer => |P| { 44 | switch (P.size) { 45 | .One => out_bytes = serializeBytes(value.*, out_bytes), 46 | .Slice => { 47 | out_bytes = serializeBytes( 48 | @intCast(serde_usize, value.len), 49 | out_bytes, 50 | ); 51 | switch (@typeInfo(P.child)) { 52 | .Bool, .Int, .Float, .Enum => { 53 | const value_bytes = std.mem.sliceAsBytes(value); 54 | std.mem.copy(u8, out_bytes, value_bytes); 55 | out_bytes = out_bytes[value_bytes.len..]; 56 | }, 57 | else => { 58 | for (value) |v| { 59 | out_bytes = serializeBytes(v, out_bytes); 60 | } 61 | }, 62 | } 63 | if (P.sentinel) |s| { 64 | out_bytes = serializeBytes(s, out_bytes); 65 | } 66 | }, 67 | else => |E| { 68 | @compileError( 69 | "Cannot serialize pointer size " ++ @tagName(E) ++ "!", 70 | ); 71 | }, 72 | } 73 | }, 74 | .Array => |A| { 75 | switch (@typeInfo(A.child)) { 76 | .Bool, .Int, .Float, .Enum => { 77 | const value_bytes = std.mem.sliceAsBytes(value[0..]); 78 | std.mem.copy(u8, out_bytes, value_bytes); 79 | out_bytes = out_bytes[value_bytes.len..]; 80 | }, 81 | else => { 82 | for (value) |v| { 83 | out_bytes = serializeBytes(v, out_bytes); 84 | } 85 | }, 86 | } 87 | if (A.sentinel) |s| { 88 | out_bytes = serializeBytes(s, out_bytes); 89 | } 90 | }, 91 | .Struct => |S| { 92 | inline for (S.fields) |field| { 93 | out_bytes = serializeBytes(@field(value, field.name), out_bytes); 94 | } 95 | }, 96 | .Union => |U| { 97 | const UnionTagType = U.tag_type orelse 98 | @compileError("Cannot serialize a union without a tag type!"); 99 | const tag = std.meta.activeTag(value); 100 | out_bytes = serializeBytes(tag, out_bytes); 101 | inline for (U.fields) |field| { 102 | if (@field(UnionTagType, field.name) == tag) { 103 | out_bytes = serializeBytes( 104 | @field(value, field.name), 105 | out_bytes, 106 | ); 107 | break; 108 | } 109 | } 110 | }, 111 | else => |E| @compileError("Cannot serialize type " ++ @tagName(E) ++ "!"), 112 | } 113 | return out_bytes; 114 | } 115 | 116 | pub fn serializeSize(value: anytype) usize { 117 | var size: usize = 0; 118 | switch (@typeInfo(@TypeOf(value))) { 119 | .Void => {}, 120 | .Bool, .Int, .Float, .Enum => size += @sizeOf(@TypeOf(value)), 121 | .Optional => { 122 | size += @sizeOf(bool); // null flag 123 | if (value) |v| { 124 | size += serializeSize(v); 125 | } 126 | }, 127 | .Pointer => |P| { 128 | switch (P.size) { 129 | .One => size += serializeSize(value.*), 130 | .Slice => { 131 | size += @sizeOf(serde_usize); // len 132 | switch (@typeInfo(P.child)) { 133 | .Bool, .Int, .Float, .Enum => { 134 | size += @sizeOf(P.child) * value.len; 135 | }, 136 | else => { 137 | for (value) |v| { 138 | size += serializeSize(v); 139 | } 140 | }, 141 | } 142 | if (P.sentinel) |s| { 143 | size += serializeSize(s); 144 | } 145 | }, 146 | else => |E| { 147 | @compileError( 148 | "Cannot serialize pointer size " ++ @tagName(E) ++ "!", 149 | ); 150 | }, 151 | } 152 | }, 153 | .Array => |A| { 154 | switch (@typeInfo(A.child)) { 155 | .Bool, .Int, .Float, .Enum => size += @sizeOf(A.child) * value.len, 156 | else => { 157 | for (value) |v| { 158 | size += serializeSize(v); 159 | } 160 | }, 161 | } 162 | if (A.sentinel) |s| { 163 | size += serializeSize(s); 164 | } 165 | }, 166 | .Struct => |S| { 167 | inline for (S.fields) |field| { 168 | size += serializeSize(@field(value, field.name)); 169 | } 170 | }, 171 | .Union => |U| { 172 | const UnionTagType = U.tag_type orelse 173 | @compileError("Cannot serialize a union without a tag type!"); 174 | const tag = std.meta.activeTag(value); 175 | size += @sizeOf(UnionTagType); 176 | inline for (U.fields) |field| { 177 | if (@field(UnionTagType, field.name) == tag) { 178 | size += serializeSize(@field(value, field.name)); 179 | break; 180 | } 181 | } 182 | }, 183 | else => |E| @compileError("Cannot serialize type " ++ @tagName(E) ++ "!"), 184 | } 185 | 186 | return size; 187 | } 188 | 189 | pub const DeserializeDesc = struct { 190 | allocator: ?std.mem.Allocator = null, 191 | bytes_are_embedded: bool = false, 192 | }; 193 | 194 | pub fn deserialize( 195 | desc: DeserializeDesc, 196 | comptime Type: type, 197 | bytes: []const u8, 198 | ) !Type { 199 | if (@hasDecl(Type, "deserialize")) { 200 | return try Type.deserialize(desc, bytes); 201 | } else { 202 | return try deserializeGeneric(desc, Type, bytes); 203 | } 204 | } 205 | 206 | pub fn deserializeGeneric( 207 | desc: DeserializeDesc, 208 | comptime Type: type, 209 | bytes: []const u8, 210 | ) !Type { 211 | var value: Type = undefined; 212 | _ = try deserializeBytes(desc, Type, &value, bytes); 213 | return value; 214 | } 215 | 216 | fn deserializeBytes( 217 | desc: DeserializeDesc, 218 | comptime Type: type, 219 | value: anytype, 220 | bytes: []const u8, 221 | ) SerdeError![]const u8 { 222 | var in_bytes = bytes; 223 | 224 | switch (@typeInfo(Type)) { 225 | .Void => {}, 226 | .Bool, .Int, .Float, .Enum => { 227 | value.* = std.mem.bytesAsSlice(Type, in_bytes[0..@sizeOf(Type)])[0]; 228 | in_bytes = in_bytes[@sizeOf(Type)..]; 229 | }, 230 | .Optional => |O| { 231 | var exists: bool = undefined; 232 | in_bytes = try deserializeBytes(desc, bool, &exists, in_bytes); 233 | if (exists) { 234 | in_bytes = try deserializeBytes(desc, O.child, value, in_bytes); 235 | } else { 236 | value.* = null; 237 | } 238 | }, 239 | .Pointer => |P| { 240 | switch (P.size) { 241 | .One => { 242 | if (desc.allocator) |a| { 243 | var ptr = a.create(P.child) catch return error.OutOfMemory; 244 | in_bytes = try deserializeBytes( 245 | desc, 246 | P.child, 247 | ptr, 248 | in_bytes, 249 | ); 250 | value.* = ptr; 251 | } else { 252 | return error.AllocatorRequired; 253 | } 254 | }, 255 | .Slice => { 256 | var serde_len: serde_usize = undefined; 257 | in_bytes = try deserializeBytes( 258 | desc, 259 | serde_usize, 260 | &serde_len, 261 | in_bytes, 262 | ); 263 | 264 | const len = @intCast(usize, serde_len); 265 | if (P.is_const and desc.bytes_are_embedded) { 266 | switch (@typeInfo(P.child)) { 267 | .Bool, .Int, .Float, .Enum => { 268 | value.* = std.mem.bytesAsSlice( 269 | P.child, 270 | in_bytes[0 .. len * @sizeOf(P.child)], 271 | ); 272 | in_bytes = in_bytes[len * @sizeOf(P.child) ..]; 273 | }, 274 | else => return error.AllocatorRequired, 275 | } 276 | } else if (desc.allocator) |a| { 277 | var slice = a.alloc(P.child, len) catch 278 | return error.OutOfMemory; 279 | switch (@typeInfo(P.child)) { 280 | .Bool, .Int, .Float, .Enum => { 281 | std.mem.copy( 282 | P.child, 283 | slice, 284 | std.mem.bytesAsSlice( 285 | P.child, 286 | in_bytes[0 .. len * @sizeOf(P.child)], 287 | ), 288 | ); 289 | in_bytes = in_bytes[len * @sizeOf(P.child) ..]; 290 | }, 291 | else => { 292 | for (slice) |*s| { 293 | in_bytes = try deserializeBytes( 294 | desc, 295 | P.child, 296 | s, 297 | in_bytes, 298 | ); 299 | } 300 | }, 301 | } 302 | if (P.sentinel) |_| { 303 | in_bytes = try deserializeBytes( 304 | desc, 305 | P.child, 306 | &slice[P.len], 307 | in_bytes, 308 | ); 309 | } 310 | value.* = slice; 311 | } else { 312 | return error.AllocatorRequired; 313 | } 314 | }, 315 | else => |E| { 316 | @compileError( 317 | "Cannot deserialize pointer size " ++ @tagName(E), 318 | ); 319 | }, 320 | } 321 | }, 322 | .Array => |A| { 323 | switch (@typeInfo(A.child)) { 324 | .Bool, .Int, .Float, .Enum => { 325 | std.mem.copy( 326 | A.child, 327 | value.*[0..], 328 | std.mem.bytesAsSlice( 329 | A.child, 330 | bytes[0 .. A.len * @sizeOf(A.child)], 331 | ), 332 | ); 333 | in_bytes = in_bytes[A.len * @sizeOf(A.child) ..]; 334 | }, 335 | else => { 336 | for (value.*) |*v| { 337 | in_bytes = try deserializeBytes(desc, A.child, v, in_bytes); 338 | } 339 | }, 340 | } 341 | if (A.sentinel) |_| { 342 | in_bytes = try deserializeBytes( 343 | desc, 344 | A.child, 345 | &value.*[A.len], 346 | in_bytes, 347 | ); 348 | } 349 | }, 350 | .Struct => |S| { 351 | inline for (S.fields) |field| { 352 | in_bytes = try deserializeBytes( 353 | desc, 354 | field.field_type, 355 | &@field(value.*, field.name), 356 | in_bytes, 357 | ); 358 | } 359 | }, 360 | .Union => |U| { 361 | const UnionTagType = U.tag_type orelse 362 | @compileError("Cannot deserialize a union without a tag!"); 363 | var tag: UnionTagType = undefined; 364 | in_bytes = try deserializeBytes(desc, UnionTagType, &tag, in_bytes); 365 | inline for (U.fields) |field| { 366 | if (@field(UnionTagType, field.name) == tag) { 367 | const UnionType = @TypeOf(@field(value.*, field.name)); 368 | var u: UnionType = undefined; 369 | in_bytes = try deserializeBytes( 370 | desc, 371 | field.field_type, 372 | &u, 373 | in_bytes, 374 | ); 375 | value.* = @unionInit(Type, field.name, u); 376 | break; 377 | } 378 | } 379 | }, 380 | else => |E| @compileError( 381 | "Cannot deserializeBytes desc, type " ++ @tagName(E), 382 | ), 383 | } 384 | 385 | return in_bytes; 386 | } 387 | 388 | pub fn deserializeFree(allocator: std.mem.Allocator, value: anytype) void { 389 | switch (@typeInfo(@TypeOf(value))) { 390 | .Void, .Bool, .Int, .Float, .Enum => {}, 391 | .Optional => { 392 | if (value) |v| { 393 | deserializeFree(allocator, v); 394 | } 395 | }, 396 | .Pointer => |P| { 397 | switch (P.size) { 398 | .One => { 399 | deserializeFree(allocator, value.*); 400 | allocator.destroy(value); 401 | }, 402 | .Slice => { 403 | switch (@typeInfo(P.child)) { 404 | .Bool, .Int, .Float, .Enum => {}, 405 | else => { 406 | for (value) |v| { 407 | deserializeFree(allocator, v); 408 | } 409 | }, 410 | } 411 | allocator.free(value); 412 | }, 413 | else => |E| { 414 | @compileError( 415 | "Cannot deserialize pointer size " ++ @tagName(E) ++ "!", 416 | ); 417 | }, 418 | } 419 | }, 420 | .Array => |A| { 421 | switch (@typeInfo(A.child)) { 422 | .Bool, .Int, .Float, .Enum => {}, 423 | else => { 424 | for (value) |v| { 425 | deserializeFree(allocator, v); 426 | } 427 | }, 428 | } 429 | }, 430 | .Struct => |S| { 431 | inline for (S.fields) |field| { 432 | deserializeFree(allocator, @field(value, field.name)); 433 | } 434 | }, 435 | .Union => |U| { 436 | inline for (U.fields) |field| { 437 | if (std.mem.eql(u8, field.name, @tagName(value))) { 438 | deserializeFree(allocator, @field(value, field.name)); 439 | break; 440 | } 441 | } 442 | }, 443 | else => |E| @compileError("Cannot deserialize type " ++ @tagName(E) ++ "!"), 444 | } 445 | } 446 | 447 | test "serde" { 448 | const TestEnum = enum { 449 | a_field, 450 | b_field, 451 | c_field, 452 | }; 453 | 454 | const TestInnerStruct = struct { 455 | i32_field: i32 = -12, 456 | u64_field: u64 = 16, 457 | }; 458 | 459 | const TestInnerUnion = union(enum) { 460 | f32_field: f32, 461 | void_field, 462 | }; 463 | 464 | const TestStruct = struct { 465 | bool_field: bool = false, 466 | u32_field: u32 = 0, 467 | u64_field: u64 = 8, 468 | f32_field: f32 = 1.0, 469 | enum_field: TestEnum = .b_field, 470 | optional_field: ?f32 = null, 471 | ptr_field: *const f32, 472 | slice_field: []const u8, 473 | array_field: [2]u8 = [_]u8{ 1, 2 }, 474 | struct_field: TestInnerStruct = .{}, 475 | union_field: TestInnerUnion = .void_field, 476 | }; 477 | 478 | const test_ptr = try std.testing.allocator.create(f32); 479 | defer std.testing.allocator.destroy(test_ptr); 480 | 481 | const test_slice = try std.testing.allocator.alloc(u8, 5); 482 | defer std.testing.allocator.free(test_slice); 483 | std.mem.copy(u8, test_slice, "hello"); 484 | 485 | const value: TestStruct = .{ 486 | .ptr_field = test_ptr, 487 | .slice_field = test_slice, 488 | }; 489 | 490 | const bytes = try serialize(std.testing.allocator, value); 491 | defer std.testing.allocator.free(bytes); 492 | 493 | var deser = try deserialize( 494 | .{ .allocator = std.testing.allocator }, 495 | TestStruct, 496 | bytes, 497 | ); 498 | defer deserializeFree(std.testing.allocator, deser); 499 | 500 | try std.testing.expectEqual(value.bool_field, deser.bool_field); 501 | try std.testing.expectEqual(value.u32_field, deser.u32_field); 502 | try std.testing.expectEqual(value.u64_field, deser.u64_field); 503 | try std.testing.expectEqual(value.f32_field, deser.f32_field); 504 | try std.testing.expectEqual(value.enum_field, deser.enum_field); 505 | try std.testing.expectEqual(value.optional_field, deser.optional_field); 506 | try std.testing.expectEqual(value.ptr_field.*, deser.ptr_field.*); 507 | try std.testing.expectEqualSlices(u8, value.slice_field, deser.slice_field); 508 | try std.testing.expectEqualSlices(u8, &value.array_field, &deser.array_field); 509 | try std.testing.expectEqual(value.struct_field, deser.struct_field); 510 | try std.testing.expectEqual(value.union_field, deser.union_field); 511 | } 512 | -------------------------------------------------------------------------------- /src/time.zig: -------------------------------------------------------------------------------- 1 | const api = switch (cfg.platform) { 2 | .web => @import("time_web.zig"), 3 | .win => @compileError("Not yet implemented!"), 4 | }; 5 | const cfg = @import("cfg.zig"); 6 | const std = @import("std"); 7 | 8 | pub const Timer = struct { 9 | impl: api.Timer, 10 | 11 | pub fn start() !Timer { 12 | return Timer{ .impl = try api.Timer.start() }; 13 | } 14 | 15 | pub fn read(timer: Timer) u64 { 16 | return timer.impl.read(); 17 | } 18 | 19 | pub fn readSeconds(timer: Timer) f32 { 20 | return @floatCast(f32, @intToFloat(f64, timer.read()) / std.time.ns_per_s); 21 | } 22 | 23 | pub fn reset(timer: *Timer) void { 24 | timer.impl.reset(); 25 | } 26 | 27 | pub fn lap(timer: *Timer) u64 { 28 | return timer.impl.lap(); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /src/time_web.js: -------------------------------------------------------------------------------- 1 | const _time = { 2 | now() { 3 | return performance.now(); 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /src/time_web.zig: -------------------------------------------------------------------------------- 1 | const js = struct { 2 | const DomHighResTimeStamp = f64; 3 | 4 | extern fn now() DomHighResTimeStamp; 5 | }; 6 | 7 | pub const Timer = struct { 8 | start_time: js.DomHighResTimeStamp, 9 | 10 | pub fn start() !Timer { 11 | return Timer{ .start_time = js.now() }; 12 | } 13 | 14 | pub fn read(self: Timer) u64 { 15 | return timeStampToNs(js.now() - self.start_time); 16 | } 17 | 18 | pub fn reset(self: *Timer) void { 19 | self.start_time = js.now(); 20 | } 21 | 22 | pub fn lap(self: *Timer) u64 { 23 | var now = js.now(); 24 | var lap_time = self.timeStampToNs(now - self.start_time); 25 | self.start_time = now; 26 | return lap_time; 27 | } 28 | 29 | fn timeStampToNs(duration: js.DomHighResTimeStamp) u64 { 30 | return @floatToInt(u64, duration * 1000000.0); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/ui.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub const TextLayout = struct { 4 | x: f32 = 0.0, 5 | y: f32 = 0.0, 6 | width: f32 = 100.0, 7 | height: f32 = 25.0, 8 | font_size: f32 = 12.0, 9 | }; 10 | 11 | pub fn Context(comptime gfx_impl: anytype) type { 12 | return struct { 13 | const Self = @This(); 14 | 15 | gfx: gfx_impl.Context, 16 | 17 | pub fn init(gfx_desc: gfx_impl.ContextDesc, res_impl: anytype) !Self { 18 | const gfx = try gfx_impl.Context.init(gfx_desc, res_impl); 19 | return Self{ .gfx = gfx }; 20 | } 21 | 22 | pub fn deinit(self: *Self) void { 23 | self.gfx.deinit(); 24 | } 25 | 26 | pub fn render(self: *Self, render_data: gfx_impl.RenderData) !void { 27 | try self.gfx.render(render_data); 28 | } 29 | 30 | pub fn setViewport(self: *Self, width: f32, height: f32) void { 31 | self.gfx.setViewport(width, height); 32 | } 33 | 34 | pub fn debugText( 35 | self: *Self, 36 | layout: TextLayout, 37 | comptime fmt: []const u8, 38 | args: anytype, 39 | ) !void { 40 | _ = layout; 41 | _ = fmt; 42 | _ = args; 43 | 44 | var instance = try self.gfx.addInstance(); 45 | instance.setPos(300.0, 300.0); 46 | instance.setSize(600.0, 600.0); 47 | instance.setUvPos(0.0, 0.0); 48 | instance.setUvSize(1.0, 1.0); 49 | instance.setColor(1.0, 1.0, 1.0, 1.0); 50 | // var buf: [2048]u8 = undefined; 51 | // const msg = try std.fmt.bufPrint(&buf, fmt, args); 52 | 53 | // const scale = math.scaling(layout.font_size, layout.font_size, 1.0); 54 | 55 | // var start_x = layout.x; 56 | // var start_y = layout.y; 57 | // for (msg) |_| { 58 | // const m = math.mul(scale, math.translation(start_x, start_y, 0.0)); 59 | // try ctx.addInstance(.{ .mvp = math.transpose(math.mul(m, ctx.vp)) }); 60 | 61 | // start_x += layout.font_size; 62 | // } 63 | } 64 | 65 | pub fn reset(self: *Self) void { 66 | self.gfx.resetInstances(); 67 | } 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /src/ui_frag.wgsl: -------------------------------------------------------------------------------- 1 | struct _FragmentInput { 2 | @location(0) _color: vec4, 3 | @location(1) _uv: vec2, 4 | }; 5 | 6 | @group(0) @binding(1) var _samp: sampler; 7 | @group(0) @binding(2) var _tex: texture_2d; 8 | 9 | @fragment fn fs_main(_input: _FragmentInput) -> @location(0) vec4 { 10 | return textureSample(_tex, _samp, _input._uv) * _input._color; 11 | } -------------------------------------------------------------------------------- /src/ui_gfx.zig: -------------------------------------------------------------------------------- 1 | const gfx = @import("gfx.zig"); 2 | const std = @import("std"); 3 | 4 | pub const Quad = struct { 5 | pos_uv: [4]f32, 6 | }; 7 | 8 | pub const quad_vertices: []const Quad = &.{ 9 | .{ .pos_uv = [_]f32{ -0.5, -0.5, 0.0, 1.0 } }, 10 | .{ .pos_uv = [_]f32{ 0.5, -0.5, 1.0, 1.0 } }, 11 | .{ .pos_uv = [_]f32{ -0.5, 0.5, 0.0, 0.0 } }, 12 | .{ .pos_uv = [_]f32{ 0.5, 0.5, 1.0, 0.0 } }, 13 | }; 14 | pub const quad_indices: []const u16 = &.{ 0, 1, 2, 2, 1, 3 }; 15 | 16 | pub const Instance = struct { 17 | pos_size: [4]f32, 18 | uv_pos_size: [4]f32, 19 | color: [4]f32, 20 | 21 | pub fn setPos(instance: *Instance, x: f32, y: f32) void { 22 | instance.pos_size[0] = x; 23 | instance.pos_size[1] = y; 24 | } 25 | 26 | pub fn setSize(instance: *Instance, width: f32, height: f32) void { 27 | instance.pos_size[2] = width; 28 | instance.pos_size[3] = height; 29 | } 30 | 31 | pub fn setUvPos(instance: *Instance, x: f32, y: f32) void { 32 | instance.uv_pos_size[0] = x; 33 | instance.uv_pos_size[1] = y; 34 | } 35 | 36 | pub fn setUvSize(instance: *Instance, width: f32, height: f32) void { 37 | instance.uv_pos_size[2] = width; 38 | instance.uv_pos_size[3] = height; 39 | } 40 | 41 | pub fn setColor(instance: *Instance, r: f32, g: f32, b: f32, a: f32) void { 42 | instance.color[0] = r; 43 | instance.color[1] = g; 44 | instance.color[2] = b; 45 | instance.color[3] = a; 46 | } 47 | }; 48 | 49 | pub const Uniforms = struct { 50 | viewport: [2]f32, 51 | }; 52 | 53 | pub const RenderData = *gfx.RenderPass; 54 | 55 | pub const ContextDesc = struct { 56 | device: *gfx.Device, 57 | format: gfx.TextureFormat, 58 | max_instances: usize, 59 | allocator: std.mem.Allocator, 60 | }; 61 | 62 | pub const Context = struct { 63 | allocator: std.mem.Allocator, 64 | device: *gfx.Device, 65 | vertex_buffer: gfx.Buffer, 66 | index_buffer: gfx.Buffer, 67 | instance_buffer: gfx.Buffer, 68 | uniform_buffer: gfx.Buffer, 69 | font_atlas_texture: gfx.Texture, 70 | font_atlas_view: gfx.TextureView, 71 | sampler: gfx.Sampler, 72 | bind_group: gfx.BindGroup, 73 | render_pipeline: gfx.RenderPipeline, 74 | uniforms: Uniforms, 75 | instances: []Instance, 76 | instance_count: usize, 77 | 78 | pub fn init(desc: ContextDesc, res_impl: anytype) !Context { 79 | const vertex_buffer = try desc.device.initBufferSlice( 80 | quad_vertices, 81 | .{ .vertex = true }, 82 | ); 83 | const index_buffer = try desc.device.initBufferSlice( 84 | quad_indices, 85 | .{ .index = true }, 86 | ); 87 | const instance_buffer = try desc.device.initBuffer(.{ 88 | .size = @sizeOf(Instance) * desc.max_instances, 89 | .usage = .{ .vertex = true, .copy_dst = true }, 90 | }); 91 | const uniform_buffer = try desc.device.initBuffer(.{ 92 | .size = @sizeOf(Uniforms), 93 | .usage = .{ .uniform = true, .copy_dst = true }, 94 | }); 95 | 96 | const font_atlas_desc = try res_impl.loadFontAtlasDesc(desc.allocator); 97 | defer res_impl.freeFontAtlasDesc(desc.allocator, font_atlas_desc); 98 | var font_atlas_texture = try desc.device.initTexture( 99 | font_atlas_desc, 100 | .{ 101 | .copy_dst = true, 102 | .texture_binding = true, 103 | .render_attachment = true, 104 | }, 105 | ); 106 | const font_atlas_view = try desc.device.initTextureView(.{ 107 | .texture = &font_atlas_texture, 108 | .format = font_atlas_desc.format, 109 | }); 110 | 111 | const sampler = try desc.device.initSampler(.{ 112 | .mag_filter = .linear, 113 | .min_filter = .linear, 114 | }); 115 | 116 | const vert_shader_desc = try res_impl.loadVertShaderDesc(desc.allocator); 117 | defer res_impl.freeVertShaderDesc(desc.allocator, vert_shader_desc); 118 | var vert_shader = try desc.device.initShader(vert_shader_desc); 119 | defer desc.device.deinitShader(&vert_shader); 120 | 121 | const frag_shader_desc = try res_impl.loadFragShaderDesc(desc.allocator); 122 | defer res_impl.freeFragShaderDesc(desc.allocator, frag_shader_desc); 123 | var frag_shader = try desc.device.initShader(frag_shader_desc); 124 | defer desc.device.deinitShader(&frag_shader); 125 | 126 | var bind_group_layout = try desc.device.initBindGroupLayout(.{ 127 | // todo: zig #7607 128 | .entries = &[_]gfx.BindGroupLayoutEntry{ 129 | .{ 130 | .binding = 0, 131 | .visibility = .{ .vertex = true }, 132 | .buffer = .{}, 133 | }, 134 | .{ 135 | .binding = 1, 136 | .visibility = .{ .fragment = true }, 137 | .sampler = .{}, 138 | }, 139 | .{ 140 | .binding = 2, 141 | .visibility = .{ .fragment = true }, 142 | .texture = .{}, 143 | }, 144 | }, 145 | }); 146 | defer desc.device.deinitBindGroupLayout(&bind_group_layout); 147 | 148 | const bind_group = try desc.device.initBindGroup(.{ 149 | .layout = &bind_group_layout, 150 | .entries = &[_]gfx.BindGroupEntry{ 151 | .{ 152 | .binding = 0, 153 | .resource = .{ 154 | .buffer_binding = .{ .buffer = &uniform_buffer }, 155 | }, 156 | }, 157 | .{ 158 | .binding = 1, 159 | .resource = .{ .sampler = &sampler }, 160 | }, 161 | .{ 162 | .binding = 2, 163 | .resource = .{ .texture_view = &font_atlas_view }, 164 | }, 165 | }, 166 | }); 167 | 168 | var pipeline_layout = try desc.device.initPipelineLayout(.{ 169 | .bind_group_layouts = &[_]gfx.BindGroupLayout{bind_group_layout}, 170 | }); 171 | defer desc.device.deinitPipelineLayout(&pipeline_layout); 172 | 173 | var render_pipeline_desc = gfx.RenderPipelineDesc{}; 174 | render_pipeline_desc.setPipelineLayout(&pipeline_layout); 175 | render_pipeline_desc.setVertexState(.{ 176 | .module = &vert_shader, 177 | .entry_point = "vs_main", 178 | // todo: zig #7607 179 | .buffers = &[_]gfx.VertexBufferLayout{ 180 | gfx.getVertexBufferLayoutStruct(Quad, .vertex, 0), 181 | gfx.getVertexBufferLayoutStruct(Instance, .instance, 1), 182 | }, 183 | }); 184 | render_pipeline_desc.setFragmentState(.{ 185 | .module = &frag_shader, 186 | .entry_point = "fs_main", 187 | // todo: zig #7607 188 | .targets = &[_]gfx.ColorTargetState{.{ .format = desc.format }}, 189 | }); 190 | 191 | const render_pipeline = try desc.device.initRenderPipeline( 192 | render_pipeline_desc, 193 | ); 194 | 195 | const instances = try desc.allocator.alloc(Instance, desc.max_instances); 196 | const uniforms = Uniforms{ .viewport = [_]f32{ 0.0, 0.0 } }; 197 | 198 | return Context{ 199 | .allocator = desc.allocator, 200 | .device = desc.device, 201 | .vertex_buffer = vertex_buffer, 202 | .index_buffer = index_buffer, 203 | .instance_buffer = instance_buffer, 204 | .uniform_buffer = uniform_buffer, 205 | .font_atlas_texture = font_atlas_texture, 206 | .font_atlas_view = font_atlas_view, 207 | .sampler = sampler, 208 | .bind_group = bind_group, 209 | .render_pipeline = render_pipeline, 210 | .uniforms = uniforms, 211 | .instances = instances, 212 | .instance_count = 0, 213 | }; 214 | } 215 | 216 | pub fn render(ctx: *Context, pass: *gfx.RenderPass) !void { 217 | const uniform_bytes = std.mem.asBytes(&ctx.uniforms); 218 | const instance_slice = ctx.instances[0..ctx.instance_count]; 219 | const instance_bytes = std.mem.sliceAsBytes(instance_slice); 220 | var queue = ctx.device.getQueue(); 221 | try queue.writeBuffer(&ctx.uniform_buffer, 0, uniform_bytes, 0); 222 | try queue.writeBuffer(&ctx.instance_buffer, 0, instance_bytes, 0); 223 | try pass.setPipeline(&ctx.render_pipeline); 224 | try pass.setBindGroup(0, &ctx.bind_group, null); 225 | try pass.setVertexBuffer(0, &ctx.vertex_buffer, 0, gfx.whole_size); 226 | try pass.setVertexBuffer(1, &ctx.instance_buffer, 0, instance_bytes.len); 227 | try pass.setIndexBuffer(&ctx.index_buffer, .uint16, 0, gfx.whole_size); 228 | try pass.drawIndexed(quad_indices.len, ctx.instance_count, 0, 0, 0); 229 | } 230 | 231 | pub fn setViewport(ctx: *Context, width: f32, height: f32) void { 232 | ctx.uniforms.viewport[0] = width; 233 | ctx.uniforms.viewport[1] = height; 234 | } 235 | 236 | pub fn addInstance(ctx: *Context) !*Instance { 237 | if (ctx.instance_count >= ctx.instances.len) { 238 | return error.OutOfInstances; 239 | } 240 | 241 | const instance = &ctx.instances[ctx.instance_count]; 242 | ctx.instance_count += 1; 243 | return instance; 244 | } 245 | 246 | pub fn resetInstances(ctx: *Context) void { 247 | ctx.instance_count = 0; 248 | } 249 | 250 | pub fn deinit(ctx: *Context) void { 251 | ctx.allocator.free(ctx.instances); 252 | ctx.device.deinitRenderPipeline(&ctx.render_pipeline); 253 | ctx.device.deinitBindGroup(&ctx.bind_group); 254 | ctx.device.deinitSampler(&ctx.sampler); 255 | ctx.device.deinitTextureView(&ctx.font_atlas_view); 256 | ctx.device.deinitTexture(&ctx.font_atlas_texture); 257 | ctx.device.deinitBuffer(&ctx.uniform_buffer); 258 | ctx.device.deinitBuffer(&ctx.instance_buffer); 259 | ctx.device.deinitBuffer(&ctx.index_buffer); 260 | ctx.device.deinitBuffer(&ctx.vertex_buffer); 261 | } 262 | }; 263 | -------------------------------------------------------------------------------- /src/ui_res.zig: -------------------------------------------------------------------------------- 1 | const cc_bake = @import("cc_bake"); 2 | const gfx = @import("gfx.zig"); 3 | const res = @import("res.zig"); 4 | const serde = @import("serde.zig"); 5 | const std = @import("std"); 6 | 7 | pub fn loadVertShaderDesc(_: std.mem.Allocator) !gfx.ShaderDesc { 8 | return try res.load(cc_bake.ui_vert_shader, .{}); 9 | } 10 | 11 | pub fn freeVertShaderDesc(_: std.mem.Allocator, _: gfx.ShaderDesc) void {} 12 | 13 | pub fn loadFragShaderDesc(_: std.mem.Allocator) !gfx.ShaderDesc { 14 | return try res.load(cc_bake.ui_frag_shader, .{}); 15 | } 16 | 17 | pub fn freeFragShaderDesc(_: std.mem.Allocator, _: gfx.ShaderDesc) void {} 18 | 19 | pub fn loadFontAtlasDesc(allocator: std.mem.Allocator) !gfx.TextureDesc { 20 | return try res.load( 21 | cc_bake.ui_dbg_font_texture, 22 | .{ .res_allocator = allocator }, 23 | ); 24 | } 25 | 26 | pub fn freeFontAtlasDesc(allocator: std.mem.Allocator, desc: gfx.TextureDesc) void { 27 | serde.deserializeFree(allocator, desc); 28 | } 29 | -------------------------------------------------------------------------------- /src/ui_vert.wgsl: -------------------------------------------------------------------------------- 1 | struct _VertexInput { 2 | @location(0) _pos_uv: vec4, 3 | }; 4 | 5 | struct _InstanceInput { 6 | @location(1) _pos_size: vec4, 7 | @location(2) _uv_pos_size: vec4, 8 | @location(3) _color: vec4, 9 | }; 10 | 11 | struct _VertexOutput { 12 | @builtin(position) _pos : vec4, 13 | @location(0) _color: vec4, 14 | @location(1) _uv: vec2 15 | }; 16 | 17 | struct _Uniforms { 18 | _viewport: vec2, 19 | }; 20 | 21 | @group(0) @binding(0) var _uniforms : _Uniforms; 22 | 23 | @vertex 24 | fn vs_main(_vertex: _VertexInput, _instance: _InstanceInput) -> _VertexOutput { 25 | let _vertex_pos = _vertex._pos_uv.xy; 26 | let _vertex_uv = _vertex._pos_uv.zw; 27 | let _instance_pos = _instance._pos_size.xy; 28 | let _instance_size = _instance._pos_size.zw; 29 | let _instance_uv_pos = _instance._uv_pos_size.xy; 30 | let _instance_uv_size = _instance._uv_pos_size.zw; 31 | 32 | let _inst_pos = _instance_pos + _vertex_pos * _instance_size; 33 | let _pos = _inst_pos / _uniforms._viewport * 2.0 - 1.0; 34 | let _uv = _instance_uv_pos + _vertex_uv * _instance_uv_size; 35 | 36 | var _output: _VertexOutput; 37 | _output._pos = vec4(_pos, 0.0, 1.0); 38 | _output._color = _instance._color; 39 | _output._uv = _uv; 40 | return _output; 41 | } -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | class _Objs { 2 | _free = []; 3 | _objs = [null]; 4 | 5 | _insert(_obj) { 6 | if (this._free.length > 0) { 7 | const _objId = this._free.pop(); 8 | this._objs[_objId] = _obj; 9 | return _objId; 10 | } else { 11 | this._objs.push(_obj); 12 | return this._objs.length - 1; 13 | } 14 | } 15 | 16 | _remove(_objId) { 17 | if (_objId === this._objs.length - 1) { 18 | this._objs.pop(); 19 | } else { 20 | this._objs[_objId] = null; 21 | this._free.push(_objId); 22 | } 23 | } 24 | 25 | _get(_objId) { 26 | return this._objs[_objId]; 27 | } 28 | 29 | // useful for when an object has metadata and an underlying _obj field 30 | _getObj(_objId) { 31 | return this._get(_objId)._obj; 32 | } 33 | 34 | _set(_obj, _objId) { 35 | this._objs[_objId] = _obj; 36 | } 37 | 38 | _begin() { 39 | return 1; 40 | } 41 | 42 | _end() { 43 | return this._objs.length; 44 | } 45 | 46 | _next(_objId) { 47 | _objId++; 48 | while (_objId < this._objs.length && this._objs[_objId] == null) { 49 | _objId++; 50 | } 51 | return _objId; 52 | } 53 | }; 54 | 55 | const _utils = { 56 | _textDecoder: new TextDecoder(), 57 | 58 | _getWasm() { 59 | return ccGetWasmModule(); 60 | }, 61 | 62 | _u8Array(_ptr, _len) { 63 | return new Uint8Array(_utils._getWasm().memory.buffer, _ptr, _len); 64 | }, 65 | 66 | _u32Array(_ptr, _len) { 67 | return new Uint32Array(_utils._getWasm().memory.buffer, _ptr, _len / 4); 68 | }, 69 | 70 | _getString(_ptr, _len) { 71 | return _utils._textDecoder.decode(_utils._u8Array(_ptr, _len)); 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /src/wnd.zig: -------------------------------------------------------------------------------- 1 | const api = switch (cfg.platform) { 2 | .web => @import("wnd_web.zig"), 3 | .win => @compileError("Not yet implemented!"), 4 | }; 5 | const cfg = @import("cfg.zig"); 6 | 7 | pub const WindowDesc = struct { 8 | title: []const u8 = "", 9 | width: u32, 10 | height: u32, 11 | }; 12 | 13 | pub const Window = struct { 14 | impl: api.Window, 15 | 16 | pub fn init(desc: WindowDesc) !Window { 17 | return Window{ .impl = try api.Window.init(desc) }; 18 | } 19 | 20 | pub fn deinit(window: *Window) void { 21 | window.impl.deinit(); 22 | } 23 | 24 | pub fn getWidth(window: Window) u32 { 25 | return window.impl.getWidth(); 26 | } 27 | 28 | pub fn getHeight(window: Window) u32 { 29 | return window.impl.getHeight(); 30 | } 31 | 32 | pub fn getAspectRatio(window: Window) f32 { 33 | const widthf = @intToFloat(f32, window.getWidth()); 34 | const heightf = @intToFloat(f32, window.getHeight()); 35 | return widthf / heightf; 36 | } 37 | 38 | pub fn isVisible(window: Window) bool { 39 | return window.impl.isVisible(); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/wnd_gfx.zig: -------------------------------------------------------------------------------- 1 | const cfg = @import("cfg.zig"); 2 | const gfx = @import("gfx.zig"); 3 | const wnd = @import("wnd.zig"); 4 | 5 | pub fn getWindowSurfaceDesc(window: wnd.Window) gfx.SurfaceDesc { 6 | const window_info = switch (cfg.platform) { 7 | .web => .{ .canvas_id = window.impl.getCanvasId() }, 8 | else => @compileError("Unsupported platform!"), 9 | }; 10 | return gfx.SurfaceDesc{ .window_info = window_info }; 11 | } 12 | 13 | pub fn getWindowExtent(window: wnd.Window) gfx.Extent3d { 14 | return gfx.Extent3d{ .width = window.getWidth(), .height = window.getHeight() }; 15 | } 16 | 17 | pub fn getContextDesc(window: wnd.Window) gfx.ContextDesc { 18 | return gfx.ContextDesc{ 19 | .surface_desc = getWindowSurfaceDesc(window), 20 | .swapchain_size = getWindowExtent(window), 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/wnd_web.js: -------------------------------------------------------------------------------- 1 | const _BaseWindowTitle = document.title; 2 | 3 | const _wnd = { 4 | _canvases: new _Objs(), 5 | _canvasParent: document.body, 6 | _observer: new IntersectionObserver(function(_entries) { 7 | _entries.forEach(_entry => { 8 | for (let i = _wnd._canvases._begin(); 9 | i < _wnd._canvases._end(); 10 | i = _wnd._canvases._next(i)) 11 | { 12 | const _canvas = _wnd._canvases._get(i); 13 | if (_canvas._obj === _entry.target) { 14 | _canvas._isVisible = _entry.isIntersecting; 15 | } 16 | } 17 | }); 18 | }), 19 | 20 | setWindowTitle(_titlePtr, _titleLen) { 21 | document.title = _titleLen > 0 ? 22 | _utils._getString(_titlePtr, _titleLen) : 23 | _BaseWindowTitle; 24 | }, 25 | 26 | createCanvas(_width, _height) { 27 | const _canvas = document.createElement("canvas"); 28 | _canvas.width = _width; 29 | _canvas.height = _height; 30 | _wnd._canvasParent.appendChild(_canvas); 31 | _wnd._observer.observe(_canvas); 32 | const _canvasId = _wnd._canvases._insert({ 33 | _obj: _canvas, 34 | _parent: _wnd._canvasParent, 35 | _isVisible: false, 36 | }); 37 | _canvas.id = _canvasId; 38 | return _canvasId; 39 | }, 40 | 41 | destroyCanvas(_canvasId) { 42 | const _canvas = _wnd._canvases._get(_canvasId); 43 | _canvas._parent.removeChild(_canvas._obj); 44 | _wnd._canvases._remove(_canvasId); 45 | }, 46 | 47 | _setCanvasParent(_canvasParentId) { 48 | _wnd._canvasParent = _canvasParentId === null ? 49 | document.body : 50 | document.getElementById(_canvasParentId); 51 | }, 52 | 53 | isVisible(_canvasId) { 54 | return _wnd._canvases._get(_canvasId)._isVisible; 55 | } 56 | }; 57 | 58 | function ccSetCanvasParent(_canvasParent) { 59 | _wnd._setCanvasParent(_canvasParent); 60 | } 61 | -------------------------------------------------------------------------------- /src/wnd_web.zig: -------------------------------------------------------------------------------- 1 | const wnd = @import("wnd.zig"); 2 | 3 | const js = struct { 4 | const CanvasId = u32; 5 | 6 | extern fn setWindowTitle(title_ptr: [*]const u8, title_len: usize) void; 7 | extern fn createCanvas(width: u32, height: u32) CanvasId; 8 | extern fn destroyCanvas(canvas_id: CanvasId) void; 9 | extern fn isVisible(canvas_id: CanvasId) bool; 10 | }; 11 | 12 | pub const Window = struct { 13 | const max_window_id_digits = 10; // max 32 bit number is 4294967295 14 | id: js.CanvasId, 15 | id_str: [max_window_id_digits]u8, 16 | id_index: usize, 17 | width: u32, 18 | height: u32, 19 | 20 | pub fn init(desc: wnd.WindowDesc) !Window { 21 | if (desc.title.len > 0) { 22 | js.setWindowTitle(desc.title.ptr, desc.title.len); 23 | } 24 | var window = Window{ 25 | .id = js.createCanvas(desc.width, desc.height), 26 | .id_str = [_]u8{0} ** max_window_id_digits, 27 | .id_index = max_window_id_digits, 28 | .width = desc.width, 29 | .height = desc.height, 30 | }; 31 | 32 | var value = window.id; 33 | while (value > 0) : (value /= 10) { 34 | window.id_index -= 1; 35 | window.id_str[window.id_index] = @truncate(u8, value % 10) + '0'; 36 | } 37 | 38 | return window; 39 | } 40 | 41 | pub fn deinit(window: *Window) void { 42 | const empty: []const u8 = &.{}; 43 | js.setWindowTitle(empty.ptr, empty.len); 44 | js.destroyCanvas(window.id); 45 | } 46 | 47 | pub fn getWidth(window: Window) u32 { 48 | return window.width; 49 | } 50 | 51 | pub fn getHeight(window: Window) u32 { 52 | return window.height; 53 | } 54 | 55 | pub fn getCanvasId(window: Window) []const u8 { 56 | return window.id_str[window.id_index..]; 57 | } 58 | 59 | pub fn isVisible(window: Window) bool { 60 | return js.isVisible(window.id); 61 | } 62 | }; 63 | --------------------------------------------------------------------------------