From c6a68f907d77b49c4f64bded9d84e587eea4a28c Mon Sep 17 00:00:00 2001 From: Auguste Rame <19855629+SuperAuguste@users.noreply.github.com> Date: Wed, 10 Apr 2024 00:37:42 -0400 Subject: [PATCH] First iteration of badge sim (#13) --- .github/workflows/publish-simulator.yml | 47 + build.zig | 114 +- build.zig.zon | 8 + samples/feature_test.zig | 91 + simulator/.eslintrc.json | 11 + simulator/.gitattributes | 1 + simulator/.gitignore | 5 + simulator/LICENSE | 3 + simulator/LICENSE-BADGE | 21 + simulator/LICENSE-WASM4 | 13 + simulator/README.md | 27 + simulator/assets/font.png | Bin 0 -> 1936 bytes simulator/index.html | 16 + simulator/package-lock.json | 3424 ++++++++++++++++++++ simulator/package.json | 19 + simulator/src/apu-worklet.ts | 250 ++ simulator/src/apu.ts | 43 + simulator/src/compositor.ts | 119 + simulator/src/constants.ts | 258 ++ simulator/src/framebuffer.ts | 306 ++ simulator/src/index.ts | 9 + simulator/src/runtime.ts | 299 ++ simulator/src/state.ts | 26 + simulator/src/styles.css | 10 + simulator/src/ui/app.ts | 478 +++ simulator/src/ui/leds.ts | 68 + simulator/src/ui/light-sensor.ts | 58 + simulator/src/ui/menu-overlay.ts | 236 ++ simulator/src/ui/notifications.ts | 67 + simulator/src/ui/utils.ts | 56 + simulator/src/ui/virtual-gamepad.ts | 246 ++ simulator/src/webgl-constants.ts | 3783 +++++++++++++++++++++++ simulator/src/z85.ts | 78 + simulator/tsconfig.json | 20 + src/wasm4.zig | 477 +-- src/watch/404.html | 9 + src/watch/Reloader.zig | 203 ++ src/watch/main.zig | 291 ++ src/watch/watcher/LinuxWatcher.zig | 448 +++ src/watch/watcher/MacosWatcher.zig | 124 + src/watch/watcher/WindowsWatcher.zig | 226 ++ 41 files changed, 11782 insertions(+), 206 deletions(-) create mode 100644 .github/workflows/publish-simulator.yml create mode 100644 samples/feature_test.zig create mode 100644 simulator/.eslintrc.json create mode 100644 simulator/.gitattributes create mode 100644 simulator/.gitignore create mode 100644 simulator/LICENSE create mode 100644 simulator/LICENSE-BADGE create mode 100644 simulator/LICENSE-WASM4 create mode 100644 simulator/README.md create mode 100644 simulator/assets/font.png create mode 100644 simulator/index.html create mode 100644 simulator/package-lock.json create mode 100644 simulator/package.json create mode 100644 simulator/src/apu-worklet.ts create mode 100644 simulator/src/apu.ts create mode 100644 simulator/src/compositor.ts create mode 100644 simulator/src/constants.ts create mode 100644 simulator/src/framebuffer.ts create mode 100644 simulator/src/index.ts create mode 100644 simulator/src/runtime.ts create mode 100644 simulator/src/state.ts create mode 100644 simulator/src/styles.css create mode 100644 simulator/src/ui/app.ts create mode 100644 simulator/src/ui/leds.ts create mode 100644 simulator/src/ui/light-sensor.ts create mode 100644 simulator/src/ui/menu-overlay.ts create mode 100644 simulator/src/ui/notifications.ts create mode 100644 simulator/src/ui/utils.ts create mode 100644 simulator/src/ui/virtual-gamepad.ts create mode 100644 simulator/src/webgl-constants.ts create mode 100644 simulator/src/z85.ts create mode 100644 simulator/tsconfig.json create mode 100644 src/watch/404.html create mode 100644 src/watch/Reloader.zig create mode 100644 src/watch/main.zig create mode 100644 src/watch/watcher/LinuxWatcher.zig create mode 100644 src/watch/watcher/MacosWatcher.zig create mode 100644 src/watch/watcher/WindowsWatcher.zig diff --git a/.github/workflows/publish-simulator.yml b/.github/workflows/publish-simulator.yml new file mode 100644 index 0000000..9b84d98 --- /dev/null +++ b/.github/workflows/publish-simulator.yml @@ -0,0 +1,47 @@ +name: Publish Simulator +on: + push: + branches: [main] + paths: + - simulator/** + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Use Node.js 16.x + uses: actions/setup-node@v3 + with: + node-version: 16.x + + - name: Build simulator + run: | + cd simulator + npm run build + touch dist/.nojekyll + + - name: Upload Artifacts + uses: actions/upload-pages-artifact@v1 + with: + # this should match the `pages` option in your adapter-static options + path: "dist/" + + deploy: + needs: build + runs-on: ubuntu-latest + + permissions: + pages: write + id-token: write + + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Deploy + id: deployment + uses: actions/deploy-pages@v1 \ No newline at end of file diff --git a/build.zig b/build.zig index 2d317c3..385580d 100644 --- a/build.zig +++ b/build.zig @@ -24,10 +24,20 @@ pub const sycl_badge_2024 = MicroZig.Target{ pub fn build(b: *Build) void { const mz = MicroZig.init(b, .{}); + const optimize = b.standardOptimizeOption(.{}); - //const fw_options = b.addOptions(); - //fw_options.addOption(bool, "have_cart", false); + _ = b.addModule("wasm4", .{ .root_source_file = .{ .path = "src/wasm4.zig" } }); + + var dep: std.Build.Dependency = .{ .builder = b }; + const cart = add_cart(&dep, b, .{ + .name = "sample", + .optimize = optimize, + .root_source_file = .{ .path = "samples/feature_test.zig" }, + }); + + const watch_step = b.step("watch", ""); + watch_step.dependOn(&cart.watch_run_cmd.step); //const modified_memory_regions = b.allocator.dupe(MicroZig.MemoryRegion, py_badge.chip.memory_regions) catch @panic("out of memory"); //for (modified_memory_regions) |*memory_region| { @@ -81,8 +91,10 @@ pub fn build(b: *Build) void { } pub const Cart = struct { - mz: *MicroZig, - fw: *MicroZig.Firmware, + // mz: *MicroZig, + // fw: *MicroZig.Firmware, + + watch_run_cmd: *std.Build.Step.Run, }; pub const CartOptions = struct { @@ -96,36 +108,86 @@ pub fn add_cart( b: *Build, options: CartOptions, ) *Cart { - const cart_lib = b.addStaticLibrary(.{ + const lib = b.addExecutable(.{ .name = "cart", .root_source_file = options.root_source_file, - .target = py_badge.chip.cpu.getDescriptor().target, + .target = b.resolveTargetQuery(.{ + .cpu_arch = .wasm32, + .os_tag = .freestanding, + }), .optimize = options.optimize, - .link_libc = false, - .single_threaded = true, - .use_llvm = true, - .use_lld = true, }); - cart_lib.addModule("wasm4", d.builder.createModule(.{ .root_source_file = .{ .path = "src/wasm4.zig" } })); - - const fw_options = b.addOptions(); - fw_options.addOption(bool, "have_cart", true); - - const mz = MicroZig.init(d.builder, "microzig"); - const fw = mz.addFirmware(d.builder, .{ - .name = options.name, - .target = py_badge, - .optimize = .Debug, // TODO - .root_source_file = .{ .path = "src/main.zig" }, - .linker_script = .{ .root_source_file = .{ .path = "src/cart.ld" } }, + b.installArtifact(lib); + + lib.entry = .disabled; + lib.import_memory = true; + lib.initial_memory = 65536; + lib.max_memory = 65536; + lib.stack_size = 14752; + lib.global_base = 160 * 128 * 2 + 0x1e; + + lib.rdynamic = true; + + lib.root_module.addImport("wasm4", d.module("wasm4")); + + const watch = d.builder.addExecutable(.{ + .name = "watch", + .root_source_file = .{ .path = "src/watch/main.zig" }, + .target = b.resolveTargetQuery(.{}), + .optimize = options.optimize, + }); + watch.root_module.addImport("ws", d.builder.dependency("ws", .{}).module("websocket")); + watch.root_module.addImport("mime", d.builder.dependency("mime", .{}).module("mime")); + + const watch_run_cmd = b.addRunArtifact(watch); + watch_run_cmd.step.dependOn(b.getInstallStep()); + + watch_run_cmd.addArgs(&.{ + "serve", + b.graph.zig_exe, + "--zig-out-bin-dir", + b.pathJoin(&.{ b.install_path, "bin" }), + "--input-dir", + options.root_source_file.dirname().getPath(b), }); - fw.artifact.linkLibrary(cart_lib); - fw.artifact.step.dependOn(&fw_options.step); - fw.modules.app.dependencies.put("options", fw_options.createModule()) catch @panic("out of memory"); const cart: *Cart = b.allocator.create(Cart) catch @panic("out of memory"); - cart.* = .{ .mz = mz, .fw = fw }; + cart.* = .{ + .watch_run_cmd = watch_run_cmd, + }; return cart; + + // const cart_lib = b.addStaticLibrary(.{ + // .name = "cart", + // .root_source_file = options.source_file, + // .target = py_badge.chip.cpu.getDescriptor().target, + // .optimize = options.optimize, + // .link_libc = false, + // .single_threaded = true, + // .use_llvm = true, + // .use_lld = true, + // }); + // cart_lib.addModule("wasm4", d.module("wasm4")); + + // const fw_options = b.addOptions(); + // fw_options.addOption(bool, "have_cart", true); + + // const mz = MicroZig.init(d.builder, "microzig"); + + // const fw = mz.addFirmware(d.builder, .{ + // .name = options.name, + // .target = py_badge, + // .optimize = .Debug, // TODO + // .source_file = .{ .path = "src/main.zig" }, + // .linker_script = .{ .source_file = .{ .path = "src/cart.ld" } }, + // }); + // fw.artifact.linkLibrary(cart_lib); + // fw.artifact.step.dependOn(&fw_options.step); + // fw.modules.app.dependencies.put("options", fw_options.createModule()) catch @panic("out of memory"); + + // const cart: *Cart = b.allocator.create(Cart) catch @panic("out of memory"); + // cart.* = .{ .mz = mz, .fw = fw }; + // return cart; } pub fn install_cart(b: *Build, cart: *Cart) void { diff --git a/build.zig.zon b/build.zig.zon index 8202c4c..382a1c0 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -15,5 +15,13 @@ .@"microzig/bsp/microchip/atsam" = .{ .path = "../microzig/bsp/microchip/atsam", }, + .mime = .{ + .url = "git+https://github.com/andrewrk/mime.git#bf80e2e8b1d9413ddf9abe7d94537388b54c83ab", + .hash = "122074ce2f776dc9dc4a0d2588a5d76b97212a2c311ef8125d4aadffa4b84e5b8b3e", + }, + .ws = .{ + .url = "git+https://github.com/karlseguin/websocket.zig.git#1f2c4a56c642dab52fe12cdda1bd3f56865d1f86", + .hash = "1220ce168e550f8904364acd0a72f5cafd40caa08a50ba83aac50b97ba700d7bcf20", + }, }, } diff --git a/samples/feature_test.zig b/samples/feature_test.zig new file mode 100644 index 0000000..0d0757f --- /dev/null +++ b/samples/feature_test.zig @@ -0,0 +1,91 @@ +const std = @import("std"); +const wasm4 = @import("wasm4"); + +export fn start() void {} + +var green_565: u6 = 0; + +var offset: u16 = 0; + +fn read_stored_number() u64 { + var dst: u64 = undefined; + std.debug.assert(wasm4.read_flash(0, std.mem.asBytes(&dst)) == @sizeOf(u64)); + return dst; +} + +fn write_stored_number(number: u64) void { + var page: [wasm4.flash_page_size]u8 = undefined; + // @as(*u64, @alignCast(@ptrCast(page[0..8]))).* = number; + std.mem.bytesAsSlice(u64, &page)[0] = number; + wasm4.write_flash_page(0, page); +} + +export fn update() void { + if (offset % (60 * 2) == 0) { + wasm4.tone(440, 20, 10, .{ + .channel = .pulse1, + .duty_cycle = .@"1/8", + .panning = .left, + }); + } + + offset +%= 1; + + var inputs_buf: [128]u8 = undefined; + var fbs = std.io.fixedBufferStream(&inputs_buf); + + fbs.writer().print("{d}\n", .{read_stored_number()}) catch unreachable; + + inline for (std.meta.fields(wasm4.Controls)) |control| { + if (comptime !std.mem.eql(u8, control.name, "padding")) { + if (@field(wasm4.controls.*, control.name)) { + fbs.writer().writeAll(control.name) catch unreachable; + fbs.writer().writeAll("\n") catch unreachable; + } + } + } + + if (wasm4.controls.up) { + green_565 +%= 1; + } else if (wasm4.controls.down) { + green_565 -%= 1; + } + + if (wasm4.controls.left) { + write_stored_number(read_stored_number() -| 1); + } else if (wasm4.controls.right) { + write_stored_number(read_stored_number() +| 1); + } + + wasm4.red_led.* = wasm4.controls.click; + + for (0..wasm4.screen_height) |y| { + for (0..wasm4.screen_width) |x| { + wasm4.framebuffer[y * wasm4.screen_width + x] = .{ + .red = @intFromFloat(@as(f32, @floatFromInt(x)) / wasm4.screen_width * 31), + .green = green_565, + .blue = @intFromFloat(@as(f32, @floatFromInt(y)) / wasm4.screen_height * 31), + }; + } + } + + for (wasm4.neopixels, 0..) |*np, i| { + np.* = .{ + .red = @intFromFloat(@as(f32, @floatFromInt(i)) / 5 * 255), + .green = @intFromFloat(@as(f32, @floatFromInt(wasm4.light_level.*)) / std.math.maxInt(u12) * 255), + .blue = @intFromFloat(@as(f32, @floatFromInt(i)) / 5 * 255), + }; + } + + // TODO: blit, blitSub + + wasm4.line(.{ .red = 0, .green = 63, .blue = 0 }, 50, 50, 70, 70); + + wasm4.hline(.{ .red = 31, .green = 0, .blue = 0 }, 30, 30, 20); + wasm4.vline(.{ .red = 31, .green = 0, .blue = 0 }, 30, 30, 20); + + wasm4.oval(.{ .red = 0, .green = 0, .blue = 31 }, .{ .red = 31, .green = 0, .blue = 31 }, 80, 80, 10, 10); + wasm4.rect(.{ .red = 31, .green = 31, .blue = 31 }, .{ .red = 0, .green = 63, .blue = 31 }, 100, 100, 10, 10); + + wasm4.text(.{ .red = 0, .green = 0, .blue = 0 }, .{ .red = 31, .green = 63, .blue = 31 }, fbs.getWritten(), 0, 0); +} diff --git a/simulator/.eslintrc.json b/simulator/.eslintrc.json new file mode 100644 index 0000000..13f8566 --- /dev/null +++ b/simulator/.eslintrc.json @@ -0,0 +1,11 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" + ], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ] +} diff --git a/simulator/.gitattributes b/simulator/.gitattributes new file mode 100644 index 0000000..71ff3e3 --- /dev/null +++ b/simulator/.gitattributes @@ -0,0 +1 @@ +*.nelua text linguist-language=lua \ No newline at end of file diff --git a/simulator/.gitignore b/simulator/.gitignore new file mode 100644 index 0000000..f574756 --- /dev/null +++ b/simulator/.gitignore @@ -0,0 +1,5 @@ +/dist +public/cart.wasm +src/**/*.generated.js +/.parcel-cache +node_modules diff --git a/simulator/LICENSE b/simulator/LICENSE new file mode 100644 index 0000000..96ddd7c --- /dev/null +++ b/simulator/LICENSE @@ -0,0 +1,3 @@ +Adapted from https://github.com/aduros/wasm4. + +See `LICENSE-WASM4` and `LICENSE-BADGE`. diff --git a/simulator/LICENSE-BADGE b/simulator/LICENSE-BADGE new file mode 100644 index 0000000..c5c170d --- /dev/null +++ b/simulator/LICENSE-BADGE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Auguste Rame + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/simulator/LICENSE-WASM4 b/simulator/LICENSE-WASM4 new file mode 100644 index 0000000..78e713b --- /dev/null +++ b/simulator/LICENSE-WASM4 @@ -0,0 +1,13 @@ +Copyright (c) Bruno Garcia + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/simulator/README.md b/simulator/README.md new file mode 100644 index 0000000..f23945f --- /dev/null +++ b/simulator/README.md @@ -0,0 +1,27 @@ +# SYCL 2024 Badge Simulator + +Simulate your badge on the interwebz. + +## Controls + +- Escape: open menu +- WASD/Arrow Keys + Shift: 4 direction pad + click +- Space: start +- Enter: select + +## Supported Hardware Components + +- [x] Light Sensor +- [x] 160x128 RGB screen +- [x] 5x RGB LEDs +- [x] 1x Red LED on the back +- [x] Speaker +- [x] Start/Option buttons +- [x] A/B buttons +- [x] A navstick with up/down/left/right. Unsure up/left will both be activated on diagonal +- [x] Navstick is pressable too +- [x] 2MB flash separate from the microcontroller's flash + +## Additional Features + +- [ ] Live reload diff --git a/simulator/assets/font.png b/simulator/assets/font.png new file mode 100644 index 0000000000000000000000000000000000000000..0697f291e72d2e0a3e1769810178e4bf3c3477be GIT binary patch literal 1936 zcmV;B2XFX^P)EX>4Tx04R}tkv&MmKpe$iQ^l&43U&~2$WWauh>AFB6^c+H)C#RSm|Xe=O&XFE z7e~Rh;NZt%)xpJCR|i)?5c~jfb8}L3krMyc6k5c1aNLh~_a1le0HIc5n$f_we!cF3PjK&;2?2)U3q-pGZ8*4AUmwAfDc| z4bJ<-VOEe;;&b9LlP*a7$aTf#H_rJ63p_Jyrjql-VPY}g#!4Hrf~gTt5l2)_r+gvp zvdVdjvsS9G<~{ifgIRrLnd>x%5yv8yAVGwJ3W_MfMwC{a6bnh(k9qiq8h(jf3b~44 z<+@xR(=zMXUj}f427pPYq=lj@k>L-Bz8MxA0{&EeN`6RvC z(jrGdXdAe=ZfVLMaJd5vJ{htpHzYqzA(sQ*&*+;nz`!lgv*z~J+{ftykfN>~IR?jNMAa%t{iWR0LARI;9m~2Qf3lGwEWG6k8++nI?1J3O2PNkBWvx zJ;NzUCO$!OhU{0W0yWfb0bm+Ci~1?7L@|-Zhq#mt$r+Yk*_W2X(d&t!i4zipEu~8A zUpJw`_=2}WVe4oT58SY_O!s_%hDcLAvB6Q!LFXtG&jQq0X`r^SHCAOjmU->SQxM8m z0{Ue_%{ZTp+!LH~T+CEmj@<2F%r#$hK@NJ4&Z!!X__Zjc>`q!y9OFkaYsOzW1mmmj z3j-(%DN}WvLBqxM^_J(OeX563h@-MNeF#d+ z9S#-c_3?ZbYE13UzT=Ql>Gna=XRzu8m|*ffd&*J?qbspCOnHiV-34=I+w(`&X!)*Z*pv4nZ1lP83kZ_$5B*x% zq%|nF$oTM{PpU%;W_8$l-8`M*u(U%FumW~;Hovqa3O;hUxHW`=EOez41ivgnLzVzc zu`KL*SzI+>YNla4Yz?bY+WQ|Hc6(et9a%t zi)LOvFdB`Y7S5qxF?gv@mwcvGH{;W-XBR}A;puqoGq)!}S0A^cuajHhZEZa1_mC|&umQ7&;4VE)96QrWZ0m}6xodi;crQO^5ISjz% z@Oq|^wwiq*GJd24q2{-a0va`G0lqF>=~|t&Qi`p@?qX_DffLGaUN(Ovu)I- z5nN$VRp;SsFqYEA;GL9Rpq}b4=5k&+@}0G@TyX09Pq3Occ_L*T(cWgeRHa!bn2cb0 zxrIX!SGD#1rRwMz0k?6};!}tT%8PVEE3%?ZRtmP2MRYLZ=!z4ZUcpKAbB&{&Ne}oK zjVZ&XJ}L`8;REx>%F6=1UT*~)sTMclY}Uz^9kx_#Muh}Dd0XMMvaX+7oUG~UDKleDCNJd|#w<`t}CUw^p>_~=u;vM_GRPGc;EG{OanT_(g|C0-+k_;M%IUR`dr~m`~4Va((h+D z^L{_oCpGo0&LIFCN;s7lD;#;9UDY8CQh6nvkYG{R5xLo4S2)x9Im4Oze#$2mK%$z* zMd>@ef|K;Q!#TRX#$mN-91IPc2V?EK?{}JCF8~*QHE?e0xtG_zo^SNI#A)?KPaoBY zI0MGvN%7V+Kt9r*Bn`XJC-~kfn?|4B;!OMW4CmzfTY35GPH-Zm!Mi@4-g+Pl^7#i> WjVBR^+U{)t0000 + + + + + + WASM-4 web runtime dev + + + + + + + diff --git a/simulator/package-lock.json b/simulator/package-lock.json new file mode 100644 index 0000000..acb0e2e --- /dev/null +++ b/simulator/package-lock.json @@ -0,0 +1,3424 @@ +{ + "name": "wasm4-runtime", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "wasm4-runtime", + "dependencies": { + "lit": "^2.2.0" + }, + "devDependencies": { + "@parcel/transformer-worklet": "^2.11.0", + "@types/audioworklet": "0.0.23", + "@types/node": "^16.11.10", + "eslint": "^8.11.0", + "parcel": "^2.11.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.23.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.23.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.3.1", + "globals": "^13.9.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.9.5", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@lezer/common": { + "version": "1.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@lezer/lr": { + "version": "1.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lit/reactive-element": { + "version": "1.3.0", + "license": "BSD-3-Clause" + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "2.8.5", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@mischnic/json-sourcemap": { + "version": "0.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0", + "@lezer/lr": "^1.0.0", + "json5": "^2.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.2", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@parcel/bundler-default": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.11.0", + "@parcel/graph": "3.1.0", + "@parcel/plugin": "2.11.0", + "@parcel/rust": "2.11.0", + "@parcel/utils": "2.11.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/cache": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/fs": "2.11.0", + "@parcel/logger": "2.11.0", + "@parcel/utils": "2.11.0", + "lmdb": "2.8.5" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.11.0" + } + }, + "node_modules/@parcel/codeframe": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/compressor-raw": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.11.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/config-default": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/bundler-default": "2.11.0", + "@parcel/compressor-raw": "2.11.0", + "@parcel/namer-default": "2.11.0", + "@parcel/optimizer-css": "2.11.0", + "@parcel/optimizer-htmlnano": "2.11.0", + "@parcel/optimizer-image": "2.11.0", + "@parcel/optimizer-svgo": "2.11.0", + "@parcel/optimizer-swc": "2.11.0", + "@parcel/packager-css": "2.11.0", + "@parcel/packager-html": "2.11.0", + "@parcel/packager-js": "2.11.0", + "@parcel/packager-raw": "2.11.0", + "@parcel/packager-svg": "2.11.0", + "@parcel/packager-wasm": "2.11.0", + "@parcel/reporter-dev-server": "2.11.0", + "@parcel/resolver-default": "2.11.0", + "@parcel/runtime-browser-hmr": "2.11.0", + "@parcel/runtime-js": "2.11.0", + "@parcel/runtime-react-refresh": "2.11.0", + "@parcel/runtime-service-worker": "2.11.0", + "@parcel/transformer-babel": "2.11.0", + "@parcel/transformer-css": "2.11.0", + "@parcel/transformer-html": "2.11.0", + "@parcel/transformer-image": "2.11.0", + "@parcel/transformer-js": "2.11.0", + "@parcel/transformer-json": "2.11.0", + "@parcel/transformer-postcss": "2.11.0", + "@parcel/transformer-posthtml": "2.11.0", + "@parcel/transformer-raw": "2.11.0", + "@parcel/transformer-react-refresh-wrap": "2.11.0", + "@parcel/transformer-svg": "2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.11.0" + } + }, + "node_modules/@parcel/core": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@mischnic/json-sourcemap": "^0.1.0", + "@parcel/cache": "2.11.0", + "@parcel/diagnostic": "2.11.0", + "@parcel/events": "2.11.0", + "@parcel/fs": "2.11.0", + "@parcel/graph": "3.1.0", + "@parcel/logger": "2.11.0", + "@parcel/package-manager": "2.11.0", + "@parcel/plugin": "2.11.0", + "@parcel/profiler": "2.11.0", + "@parcel/rust": "2.11.0", + "@parcel/source-map": "^2.1.1", + "@parcel/types": "2.11.0", + "@parcel/utils": "2.11.0", + "@parcel/workers": "2.11.0", + "abortcontroller-polyfill": "^1.1.9", + "base-x": "^3.0.8", + "browserslist": "^4.6.6", + "clone": "^2.1.1", + "dotenv": "^7.0.0", + "dotenv-expand": "^5.1.0", + "json5": "^2.2.0", + "msgpackr": "^1.9.9", + "nullthrows": "^1.1.1", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/diagnostic": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@mischnic/json-sourcemap": "^0.1.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/events": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/fs": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/rust": "2.11.0", + "@parcel/types": "2.11.0", + "@parcel/utils": "2.11.0", + "@parcel/watcher": "^2.0.7", + "@parcel/workers": "2.11.0" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.11.0" + } + }, + "node_modules/@parcel/graph": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/logger": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.11.0", + "@parcel/events": "2.11.0" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/markdown-ansi": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/namer-default": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.11.0", + "@parcel/plugin": "2.11.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/node-resolver-core": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@mischnic/json-sourcemap": "^0.1.0", + "@parcel/diagnostic": "2.11.0", + "@parcel/fs": "2.11.0", + "@parcel/rust": "2.11.0", + "@parcel/utils": "2.11.0", + "nullthrows": "^1.1.1", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/optimizer-css": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.11.0", + "@parcel/plugin": "2.11.0", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.11.0", + "browserslist": "^4.6.6", + "lightningcss": "^1.22.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/optimizer-htmlnano": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.11.0", + "htmlnano": "^2.0.0", + "nullthrows": "^1.1.1", + "posthtml": "^0.16.5", + "svgo": "^2.4.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/optimizer-htmlnano/node_modules/css-select": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/@parcel/optimizer-htmlnano/node_modules/css-tree": { + "version": "1.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@parcel/optimizer-htmlnano/node_modules/csso": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^1.1.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@parcel/optimizer-htmlnano/node_modules/mdn-data": { + "version": "2.0.14", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/@parcel/optimizer-htmlnano/node_modules/svgo": { + "version": "2.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^4.1.3", + "css-tree": "^1.1.3", + "csso": "^4.2.0", + "picocolors": "^1.0.0", + "stable": "^0.1.8" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@parcel/optimizer-image": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.11.0", + "@parcel/plugin": "2.11.0", + "@parcel/rust": "2.11.0", + "@parcel/utils": "2.11.0", + "@parcel/workers": "2.11.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.11.0" + } + }, + "node_modules/@parcel/optimizer-svgo": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.11.0", + "@parcel/plugin": "2.11.0", + "@parcel/utils": "2.11.0", + "svgo": "^2.4.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/optimizer-svgo/node_modules/css-select": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/@parcel/optimizer-svgo/node_modules/css-tree": { + "version": "1.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@parcel/optimizer-svgo/node_modules/csso": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^1.1.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@parcel/optimizer-svgo/node_modules/mdn-data": { + "version": "2.0.14", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/@parcel/optimizer-svgo/node_modules/svgo": { + "version": "2.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^4.1.3", + "css-tree": "^1.1.3", + "csso": "^4.2.0", + "picocolors": "^1.0.0", + "stable": "^0.1.8" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@parcel/optimizer-swc": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.11.0", + "@parcel/plugin": "2.11.0", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.11.0", + "@swc/core": "^1.3.36", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/package-manager": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.11.0", + "@parcel/fs": "2.11.0", + "@parcel/logger": "2.11.0", + "@parcel/node-resolver-core": "3.2.0", + "@parcel/types": "2.11.0", + "@parcel/utils": "2.11.0", + "@parcel/workers": "2.11.0", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.11.0" + } + }, + "node_modules/@parcel/packager-css": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.11.0", + "@parcel/plugin": "2.11.0", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.11.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/packager-html": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.11.0", + "@parcel/types": "2.11.0", + "@parcel/utils": "2.11.0", + "nullthrows": "^1.1.1", + "posthtml": "^0.16.5" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/packager-js": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.11.0", + "@parcel/plugin": "2.11.0", + "@parcel/rust": "2.11.0", + "@parcel/source-map": "^2.1.1", + "@parcel/types": "2.11.0", + "@parcel/utils": "2.11.0", + "globals": "^13.2.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/packager-raw": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.11.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/packager-svg": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.11.0", + "@parcel/types": "2.11.0", + "@parcel/utils": "2.11.0", + "posthtml": "^0.16.4" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/packager-wasm": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.11.0" + }, + "engines": { + "node": ">=12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/plugin": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/types": "2.11.0" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/profiler": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.11.0", + "@parcel/events": "2.11.0", + "chrome-trace-event": "^1.0.2" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/reporter-cli": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.11.0", + "@parcel/types": "2.11.0", + "@parcel/utils": "2.11.0", + "chalk": "^4.1.0", + "cli-progress": "^3.12.0", + "term-size": "^2.2.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/reporter-dev-server": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.11.0", + "@parcel/utils": "2.11.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/reporter-tracer": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.11.0", + "@parcel/utils": "2.11.0", + "chrome-trace-event": "^1.0.3", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/resolver-default": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/node-resolver-core": "3.2.0", + "@parcel/plugin": "2.11.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/runtime-browser-hmr": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.11.0", + "@parcel/utils": "2.11.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/runtime-js": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.11.0", + "@parcel/plugin": "2.11.0", + "@parcel/utils": "2.11.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/runtime-react-refresh": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.11.0", + "@parcel/utils": "2.11.0", + "react-error-overlay": "6.0.9", + "react-refresh": "^0.9.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/runtime-service-worker": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.11.0", + "@parcel/utils": "2.11.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/rust": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/source-map": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3" + }, + "engines": { + "node": "^12.18.3 || >=14" + } + }, + "node_modules/@parcel/transformer-babel": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.11.0", + "@parcel/plugin": "2.11.0", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.11.0", + "browserslist": "^4.6.6", + "json5": "^2.2.0", + "nullthrows": "^1.1.1", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-css": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.11.0", + "@parcel/plugin": "2.11.0", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.11.0", + "browserslist": "^4.6.6", + "lightningcss": "^1.22.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-html": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.11.0", + "@parcel/plugin": "2.11.0", + "@parcel/rust": "2.11.0", + "nullthrows": "^1.1.1", + "posthtml": "^0.16.5", + "posthtml-parser": "^0.10.1", + "posthtml-render": "^3.0.0", + "semver": "^7.5.2", + "srcset": "4" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-image": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.11.0", + "@parcel/utils": "2.11.0", + "@parcel/workers": "2.11.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "peerDependencies": { + "@parcel/core": "^2.11.0" + } + }, + "node_modules/@parcel/transformer-js": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.11.0", + "@parcel/plugin": "2.11.0", + "@parcel/rust": "2.11.0", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.11.0", + "@parcel/workers": "2.11.0", + "@swc/helpers": "^0.5.0", + "browserslist": "^4.6.6", + "nullthrows": "^1.1.1", + "regenerator-runtime": "^0.13.7", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.11.0" + } + }, + "node_modules/@parcel/transformer-json": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.11.0", + "json5": "^2.2.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-postcss": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.11.0", + "@parcel/plugin": "2.11.0", + "@parcel/rust": "2.11.0", + "@parcel/utils": "2.11.0", + "clone": "^2.1.1", + "nullthrows": "^1.1.1", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-posthtml": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.11.0", + "@parcel/utils": "2.11.0", + "nullthrows": "^1.1.1", + "posthtml": "^0.16.5", + "posthtml-parser": "^0.10.1", + "posthtml-render": "^3.0.0", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-raw": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.11.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-react-refresh-wrap": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/plugin": "2.11.0", + "@parcel/utils": "2.11.0", + "react-refresh": "^0.9.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-svg": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.11.0", + "@parcel/plugin": "2.11.0", + "@parcel/rust": "2.11.0", + "nullthrows": "^1.1.1", + "posthtml": "^0.16.5", + "posthtml-parser": "^0.10.1", + "posthtml-render": "^3.0.0", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/transformer-worklet": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@parcel/transformer-worklet/-/transformer-worklet-2.11.0.tgz", + "integrity": "sha512-smcB5MBqXtiAZOx8JCCYbNUk5kHMW8f39zAWNKkkrlbcPqUxS2vnXl4rEf3CCriarfc41Mfp5D0wOkhUAXu+rw==", + "dev": true, + "dependencies": { + "@parcel/plugin": "2.11.0" + }, + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/types": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/cache": "2.11.0", + "@parcel/diagnostic": "2.11.0", + "@parcel/fs": "2.11.0", + "@parcel/package-manager": "2.11.0", + "@parcel/source-map": "^2.1.1", + "@parcel/workers": "2.11.0", + "utility-types": "^3.10.0" + } + }, + "node_modules/@parcel/utils": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/codeframe": "2.11.0", + "@parcel/diagnostic": "2.11.0", + "@parcel/logger": "2.11.0", + "@parcel/markdown-ansi": "2.11.0", + "@parcel/rust": "2.11.0", + "@parcel/source-map": "^2.1.1", + "chalk": "^4.1.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.4.0", + "@parcel/watcher-darwin-arm64": "2.4.0", + "@parcel/watcher-darwin-x64": "2.4.0", + "@parcel/watcher-freebsd-x64": "2.4.0", + "@parcel/watcher-linux-arm-glibc": "2.4.0", + "@parcel/watcher-linux-arm64-glibc": "2.4.0", + "@parcel/watcher-linux-arm64-musl": "2.4.0", + "@parcel/watcher-linux-x64-glibc": "2.4.0", + "@parcel/watcher-linux-x64-musl": "2.4.0", + "@parcel/watcher-win32-arm64": "2.4.0", + "@parcel/watcher-win32-ia32": "2.4.0", + "@parcel/watcher-win32-x64": "2.4.0" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.4.0", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/workers": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.11.0", + "@parcel/logger": "2.11.0", + "@parcel/profiler": "2.11.0", + "@parcel/types": "2.11.0", + "@parcel/utils": "2.11.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.11.0" + } + }, + "node_modules/@swc/core": { + "version": "1.3.106", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.1", + "@swc/types": "^0.1.5" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.3.106", + "@swc/core-darwin-x64": "1.3.106", + "@swc/core-linux-arm-gnueabihf": "1.3.106", + "@swc/core-linux-arm64-gnu": "1.3.106", + "@swc/core-linux-arm64-musl": "1.3.106", + "@swc/core-linux-x64-gnu": "1.3.106", + "@swc/core-linux-x64-musl": "1.3.106", + "@swc/core-win32-arm64-msvc": "1.3.106", + "@swc/core-win32-ia32-msvc": "1.3.106", + "@swc/core-win32-x64-msvc": "1.3.106" + }, + "peerDependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.3.106", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.2", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.3", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@swc/types": { + "version": "0.1.5", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@types/audioworklet": { + "version": "0.0.23", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "16.11.22", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.2", + "license": "MIT" + }, + "node_modules/abortcontroller-polyfill": { + "version": "1.7.5", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.7.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/base-x": { + "version": "3.0.9", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.22.2", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001580", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cli-progress": { + "version": "3.12.0", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-select/node_modules/dom-serializer": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/css-select/node_modules/domhandler": { + "version": "5.0.3", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/css-select/node_modules/domutils": { + "version": "3.1.0", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/css-select/node_modules/entities": { + "version": "4.5.0", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "dev": true, + "license": "CC0-1.0", + "optional": true, + "peer": true + }, + "node_modules/debug": { + "version": "4.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "4.3.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "7.0.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=6" + } + }, + "node_modules/dotenv-expand": { + "version": "5.1.0", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/electron-to-chromium": { + "version": "1.4.645", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "3.0.1", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint/eslintrc": "^1.2.1", + "@humanwhocodes/config-array": "^0.9.2", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^6.0.1", + "globals": "^13.6.0", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.1.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/espree": { + "version": "9.3.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.7.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/esquery": { + "version": "1.4.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.5", + "dev": true, + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/get-port": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.13.0", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/htmlnano": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cosmiconfig": "^8.0.0", + "posthtml": "^0.16.5", + "timsort": "^0.3.0" + }, + "peerDependencies": { + "cssnano": "^6.0.0", + "postcss": "^8.3.11", + "purgecss": "^5.0.0", + "relateurl": "^0.2.7", + "srcset": "4.0.0", + "svgo": "^3.0.2", + "terser": "^5.10.0", + "uncss": "^0.17.3" + }, + "peerDependenciesMeta": { + "cssnano": { + "optional": true + }, + "postcss": { + "optional": true + }, + "purgecss": { + "optional": true + }, + "relateurl": { + "optional": true + }, + "srcset": { + "optional": true + }, + "svgo": { + "optional": true + }, + "terser": { + "optional": true + }, + "uncss": { + "optional": true + } + } + }, + "node_modules/htmlparser2": { + "version": "7.2.0", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.2", + "domutils": "^2.8.0", + "entities": "^3.0.1" + } + }, + "node_modules/ignore": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-json": { + "version": "2.0.1", + "dev": true, + "license": "ISC" + }, + "node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.23.0", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^1.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.23.0", + "lightningcss-darwin-x64": "1.23.0", + "lightningcss-freebsd-x64": "1.23.0", + "lightningcss-linux-arm-gnueabihf": "1.23.0", + "lightningcss-linux-arm64-gnu": "1.23.0", + "lightningcss-linux-arm64-musl": "1.23.0", + "lightningcss-linux-x64-gnu": "1.23.0", + "lightningcss-linux-x64-musl": "1.23.0", + "lightningcss-win32-x64-msvc": "1.23.0" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.23.0", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "dev": true, + "license": "MIT" + }, + "node_modules/lit": { + "version": "2.2.0", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^1.3.0", + "lit-element": "^3.2.0", + "lit-html": "^2.2.0" + } + }, + "node_modules/lit-element": { + "version": "3.2.0", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^1.3.0", + "lit-html": "^2.2.0" + } + }, + "node_modules/lit-html": { + "version": "2.2.0", + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, + "node_modules/lmdb": { + "version": "2.8.5", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "msgpackr": "^1.9.5", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.1.1", + "ordered-binary": "^1.4.1", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "2.8.5", + "@lmdb/lmdb-darwin-x64": "2.8.5", + "@lmdb/lmdb-linux-arm": "2.8.5", + "@lmdb/lmdb-linux-arm64": "2.8.5", + "@lmdb/lmdb-linux-x64": "2.8.5", + "@lmdb/lmdb-win32-x64": "2.8.5" + } + }, + "node_modules/lmdb/node_modules/node-addon-api": { + "version": "6.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "dev": true, + "license": "CC0-1.0", + "optional": true, + "peer": true + }, + "node_modules/micromatch": { + "version": "4.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/msgpackr": { + "version": "1.10.1", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.2", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.0.7" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.2", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.2", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.2", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.2", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.2", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.2" + } + }, + "node_modules/msgpackr-extract/node_modules/node-gyp-build-optional-packages": { + "version": "5.0.7", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16 || ^18 || >= 20" + } + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-gyp-build-optional-packages/node_modules/detect-libc": { + "version": "2.0.2", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "dev": true, + "license": "MIT" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nullthrows": { + "version": "1.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ordered-binary": { + "version": "1.5.1", + "dev": true, + "license": "MIT" + }, + "node_modules/parcel": { + "version": "2.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/config-default": "2.11.0", + "@parcel/core": "2.11.0", + "@parcel/diagnostic": "2.11.0", + "@parcel/events": "2.11.0", + "@parcel/fs": "2.11.0", + "@parcel/logger": "2.11.0", + "@parcel/package-manager": "2.11.0", + "@parcel/reporter-cli": "2.11.0", + "@parcel/reporter-dev-server": "2.11.0", + "@parcel/reporter-tracer": "2.11.0", + "@parcel/utils": "2.11.0", + "chalk": "^4.1.0", + "commander": "^7.0.0", + "get-port": "^4.2.0" + }, + "bin": { + "parcel": "lib/bin.js" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/posthtml": { + "version": "0.16.6", + "dev": true, + "license": "MIT", + "dependencies": { + "posthtml-parser": "^0.11.0", + "posthtml-render": "^3.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/posthtml-parser": { + "version": "0.10.2", + "dev": true, + "license": "MIT", + "dependencies": { + "htmlparser2": "^7.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/posthtml-render": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-json": "^2.0.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/posthtml/node_modules/posthtml-parser": { + "version": "0.11.0", + "dev": true, + "license": "MIT", + "dependencies": { + "htmlparser2": "^7.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react-error-overlay": { + "version": "6.0.9", + "dev": true, + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.9.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "dev": true, + "license": "MIT" + }, + "node_modules/regexpp": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.5.4", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/srcset": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stable": { + "version": "0.1.8", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-color/node_modules/has-flag": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/svgo": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/term-size": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/timsort": { + "version": "0.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utility-types": { + "version": "3.11.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/v8-compile-cache": { + "version": "2.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "dev": true, + "license": "ISC" + }, + "node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "license": "ISC" + } + } +} diff --git a/simulator/package.json b/simulator/package.json new file mode 100644 index 0000000..da30564 --- /dev/null +++ b/simulator/package.json @@ -0,0 +1,19 @@ +{ + "name": "wasm4-runtime", + "private": true, + "scripts": { + "lint": "eslint src --fix", + "dev": "parcel index.html", + "build": "parcel build index.html --no-optimize" + }, + "dependencies": { + "lit": "^2.2.0" + }, + "devDependencies": { + "@parcel/transformer-worklet": "^2.11.0", + "@types/audioworklet": "0.0.23", + "@types/node": "^16.11.10", + "eslint": "^8.11.0", + "parcel": "^2.11.0" + } +} diff --git a/simulator/src/apu-worklet.ts b/simulator/src/apu-worklet.ts new file mode 100644 index 0000000..d7caa02 --- /dev/null +++ b/simulator/src/apu-worklet.ts @@ -0,0 +1,250 @@ +"use strict"; + +// Audio worklet file: do not export anything directly. +const SAMPLE_RATE = 44100; +const MAX_VOLUME = 0.15; +const MAX_VOLUME_TRIANGLE = 0.25; + +class Channel { + /** Starting frequency. */ + freq1 = 0; + + /** Ending frequency, or zero for no frequency transition. */ + freq2 = 0; + + /** Time the tone was started. */ + startTime = 0; + + /** Time at the end of the attack period. */ + attackTime = 0; + + /** Time at the end of the decay period. */ + decayTime = 0; + + /** Time at the end of the sustain period. */ + sustainTime = 0; + + /** Time the tone should end. */ + releaseTime = 0; + + /** Sustain volume level. */ + sustainVolume = 0; + + /** Peak volume level at the end of the attack phase. */ + peakVolume = 0; + + /** Used for time tracking. */ + phase = 0; + + /** Tone panning. 0 = center, 1 = only left, 2 = only right. */ + pan = 0; + + /** Duty cycle for pulse channels. */ + pulseDutyCycle = 0; + + /** Noise generation state. */ + noiseSeed = 0x0001; + + /** The last generated random number, either -1 or 1. */ + noiseLastRandom = 0; +} + +function lerp (value1: number, value2: number, t: number) { + return value1 + t * (value2 - value1); +} + +function polyblep (phase: number, phaseInc: number) { + if (phase < phaseInc) { + const t = phase / phaseInc; + return t+t - t*t; + } else if (phase > 1 - phaseInc) { + const t = (phase - (1 - phaseInc)) / phaseInc; + return 1 - (t+t - t*t); + } else { + return 1; + } +} + +class APUProcessor extends AudioWorkletProcessor { + time: number; + channels: Channel[]; + + constructor () { + super(); + + this.time = 0; + this.channels = new Array(4); + for (let ii = 0; ii < 4; ++ii) { + this.channels[ii] = new Channel(); + } + + if (this.port != null) { + this.port.onmessage = (event: MessageEvent<[number, number, number, number]>) => { + this.tone(...event.data); + }; + } + } + + ramp (value1: number, value2: number, time1: number, time2: number) { + const t = (this.time - time1) / (time2 - time1); + return lerp(value1, value2, t); + } + + getCurrentFrequency (channel: Channel) { + if (channel.freq2 > 0) { + return this.ramp(channel.freq1, channel.freq2, channel.startTime, channel.releaseTime); + } else { + return channel.freq1; + } + } + + getCurrentVolume (channel: Channel) { + const time = this.time; + if (time >= channel.sustainTime) { + // Release + return this.ramp(channel.sustainVolume, 0, channel.sustainTime, channel.releaseTime); + } else if (time >= channel.decayTime) { + // Sustain + return channel.sustainVolume; + } else if (time >= channel.attackTime) { + // Decay + return this.ramp(channel.peakVolume, channel.sustainVolume, channel.attackTime, channel.decayTime); + } else { + // Attack + return this.ramp(0, channel.peakVolume, channel.startTime, channel.attackTime); + } + } + + tone (frequency: number, duration: number, volume: number, flags: number) { + const freq1 = frequency & 0xffff; + const freq2 = (frequency >> 16) & 0xffff; + + const sustain = (duration & 0xff); + const release = ((duration >> 8) & 0xff); + const decay = ((duration >> 16) & 0xff); + const attack = ((duration >> 24) & 0xff); + + const sustainVolume = Math.min(volume & 0xff, 100); + const peakVolume = Math.min((volume >> 8) & 0xff, 100); + + const channelIdx = flags & 0x3; + const mode = (flags >> 2) & 0x3; + const pan = (flags >> 4) & 0x3; + + const channel = this.channels[channelIdx]; + + // Restart the phase if this channel wasn't already playing + if (this.time > channel.releaseTime) { + channel.phase = (channelIdx == 2) ? 0.25 : 0; + } + + channel.freq1 = freq1; + channel.freq2 = freq2; + channel.startTime = this.time; + channel.attackTime = channel.startTime + ((SAMPLE_RATE*attack/60) >>> 0); + channel.decayTime = channel.attackTime + ((SAMPLE_RATE*decay/60) >>> 0); + channel.sustainTime = channel.decayTime + ((SAMPLE_RATE*sustain/60) >>> 0); + channel.releaseTime = channel.sustainTime + ((SAMPLE_RATE*release/60) >>> 0); + channel.pan = pan; + + const maxVolume = (channelIdx == 2) ? MAX_VOLUME_TRIANGLE : MAX_VOLUME; + channel.sustainVolume = maxVolume * sustainVolume/100; + channel.peakVolume = peakVolume ? maxVolume * peakVolume/100 : maxVolume; + + if (channelIdx == 0 || channelIdx == 1) { + switch (mode) { + case 0: + channel.pulseDutyCycle = 0.125; + break; + case 1: case 3: default: + channel.pulseDutyCycle = 0.25; + break; + case 2: + channel.pulseDutyCycle = 0.5; + break; + } + + } else if (channelIdx == 2) { + // For the triangle channel, prevent popping on hard stops by adding a 1 ms release + if (release == 0) { + channel.releaseTime += (SAMPLE_RATE/1000) >>> 0; + } + } + } + + process (_inputs: Float32Array[][], [[ outputLeft, outputRight ]]: Float32Array[][], _parameters: Record) { + for (let ii = 0, frames = outputLeft.length; ii < frames; ++ii, ++this.time) { + let mixLeft = 0, mixRight = 0; + + for (let channelIdx = 0; channelIdx < 4; ++channelIdx) { + const channel = this.channels[channelIdx]; + + if (this.time < channel.releaseTime) { + const freq = this.getCurrentFrequency(channel); + const volume = this.getCurrentVolume(channel); + let sample; + + if (channelIdx == 3) { + // Noise channel + channel.phase += freq * freq / (1000000/44100 * SAMPLE_RATE); + while (channel.phase > 0) { + channel.phase--; + let noiseSeed = channel.noiseSeed; + noiseSeed ^= noiseSeed >> 7; + noiseSeed ^= noiseSeed << 9; + noiseSeed ^= noiseSeed >> 13; + channel.noiseSeed = noiseSeed; + channel.noiseLastRandom = ((noiseSeed & 0x1) << 1) - 1; + } + sample = volume * channel.noiseLastRandom; + + } else { + const phaseInc = freq / SAMPLE_RATE; + let phase = channel.phase + phaseInc; + + if (phase >= 1) { + phase--; + } + channel.phase = phase; + + if (channelIdx == 2) { + // Triangle channel + sample = volume * (2*Math.abs(2*channel.phase - 1) - 1); + + } else { + // Pulse channel + let dutyPhase, dutyPhaseInc, multiplier; + + // Map duty to 0->1 + const pulseDutyCycle = channel.pulseDutyCycle; + if (phase < pulseDutyCycle) { + dutyPhase = phase / pulseDutyCycle; + dutyPhaseInc = phaseInc / pulseDutyCycle; + multiplier = volume; + } else { + dutyPhase = (phase - pulseDutyCycle) / (1 - pulseDutyCycle); + dutyPhaseInc = phaseInc / (1 - pulseDutyCycle); + multiplier = -volume; + } + sample = multiplier * polyblep(dutyPhase, dutyPhaseInc); + } + } + + if (channel.pan != 1) { + mixRight += sample; + } + if (channel.pan != 2) { + mixLeft += sample; + } + } + } + + outputLeft[ii] = mixLeft; + outputRight[ii] = mixRight; + } + + return true; + } +} + +registerProcessor("wasm4-apu", APUProcessor as unknown as AudioWorkletProcessorConstructor); diff --git a/simulator/src/apu.ts b/simulator/src/apu.ts new file mode 100644 index 0000000..2762478 --- /dev/null +++ b/simulator/src/apu.ts @@ -0,0 +1,43 @@ +// Created using `npm run build:apu-worklet` and +// is automatically generated in build and start scripts. +import worklet from "worklet:./apu-worklet"; + +export class APU { + audioCtx: AudioContext + processorPort?: MessagePort + + constructor() { + this.audioCtx = new (window.AudioContext || window.webkitAudioContext)({ + sampleRate: 44100, // must match SAMPLE_RATE in worklet + }); + } + + async init() { + const audioCtx = this.audioCtx; + await audioCtx.audioWorklet.addModule(worklet); + + const workletNode = new AudioWorkletNode(audioCtx, "wasm4-apu", { + outputChannelCount: [2], + }); + this.processorPort = workletNode.port; + workletNode.connect(audioCtx.destination); + } + + tone(frequency: number, duration: number, volume: number, flags: number) { + this.processorPort!.postMessage([frequency, duration, volume, flags]); + } + + unlockAudio() { + const audioCtx = this.audioCtx; + if (audioCtx.state == "suspended") { + audioCtx.resume(); + } + } + + pauseAudio() { + const audioCtx = this.audioCtx; + if (audioCtx.state == "running") { + audioCtx.suspend(); + } + } +} diff --git a/simulator/src/compositor.ts b/simulator/src/compositor.ts new file mode 100644 index 0000000..a7fd853 --- /dev/null +++ b/simulator/src/compositor.ts @@ -0,0 +1,119 @@ +import { WIDTH, HEIGHT } from "./constants"; +import * as constants from "./constants"; +import * as GL from "./webgl-constants"; +import type { Framebuffer } from "./framebuffer"; + +const PALETTE_SIZE = 4; + +export class WebGLCompositor { + constructor (public gl: WebGLRenderingContext) { + const canvas = gl.canvas; + canvas.addEventListener("webglcontextlost", event => { + event.preventDefault(); + }); + canvas.addEventListener("webglcontextrestored", () => { this.initGL() }); + + this.initGL(); + + // // Test WebGL context loss + // window.addEventListener("mousedown", () => { + // console.log("Losing context"); + // const ext = gl.getExtension('WEBGL_lose_context'); + // ext.loseContext(); + // setTimeout(() => { + // ext.restoreContext(); + // }, 0); + // }) + } + + initGL () { + const gl = this.gl; + + function createShader (type: number, source: string) { + const shader = gl.createShader(type)!; + + gl.shaderSource(shader, source); + gl.compileShader(shader); + if (gl.getShaderParameter(shader, GL.COMPILE_STATUS) == 0) { + throw new Error(gl.getShaderInfoLog(shader) + ''); + } + return shader; + } + + function createTexture (slot: number) { + const texture = gl.createTexture(); + gl.activeTexture(slot); + gl.bindTexture(GL.TEXTURE_2D, texture); + gl.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_WRAP_S, GL.CLAMP_TO_EDGE); + gl.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_WRAP_T, GL.CLAMP_TO_EDGE); + gl.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_MAG_FILTER, GL.NEAREST); + gl.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_MIN_FILTER, GL.NEAREST); + } + + const vertexShader = createShader(GL.VERTEX_SHADER, ` + attribute vec2 pos; + varying vec2 framebufferCoord; + + void main () { + framebufferCoord = pos*vec2(0.5, -0.5) + 0.5; + gl_Position = vec4(pos, 0, 1); + } + `); + + const fragmentShader = createShader(GL.FRAGMENT_SHADER, ` + precision mediump float; + uniform sampler2D framebuffer; + varying vec2 framebufferCoord; + + void main () { + gl_FragColor = texture2D(framebuffer, framebufferCoord); + } + `); + + // Setup shaders + const program = gl.createProgram()!; + + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + if (gl.getProgramParameter(program, GL.LINK_STATUS) == 0) { + throw new Error(gl.getProgramInfoLog(program) + ''); + } + gl.useProgram(program); + + gl.uniform1i(gl.getUniformLocation(program, "framebuffer"), 0); + + // Cleanup shaders + gl.detachShader(program, vertexShader); + gl.deleteShader(vertexShader); + gl.detachShader(program, fragmentShader); + gl.deleteShader(fragmentShader); + + // Create framebuffer texture + createTexture(GL.TEXTURE0); + gl.texImage2D(GL.TEXTURE_2D, 0, GL.RGB565, WIDTH, HEIGHT, 0, GL.RGB, GL.UNSIGNED_SHORT_5_6_5, null); + + // Setup static geometry + const positionAttrib = gl.getAttribLocation(program, "pos"); + const positionBuffer = gl.createBuffer(); + const positionData = new Float32Array([ + -1, -1, -1, +1, +1, +1, + +1, +1, +1, -1, -1, -1, + ]); + gl.bindBuffer(GL.ARRAY_BUFFER, positionBuffer); + gl.bufferData(GL.ARRAY_BUFFER, positionData, GL.STATIC_DRAW); + gl.enableVertexAttribArray(positionAttrib); + gl.vertexAttribPointer(positionAttrib, 2, GL.FLOAT, false, 0, 0); + } + + composite (framebuffer: Framebuffer) { + const gl = this.gl; + const bytes = framebuffer.bytes; + + // Upload framebuffer + gl.texImage2D(GL.TEXTURE_2D, 0, GL.RGB565, WIDTH, HEIGHT, 0, GL.RGB, GL.UNSIGNED_SHORT_5_6_5, bytes); + + // Draw the fullscreen quad + gl.drawArrays(GL.TRIANGLES, 0, 6); + } +} diff --git a/simulator/src/constants.ts b/simulator/src/constants.ts new file mode 100644 index 0000000..f346ffd --- /dev/null +++ b/simulator/src/constants.ts @@ -0,0 +1,258 @@ +export const WIDTH = 160; +export const HEIGHT = 128; + +export const FLASH_PAGE_SIZE = 256; +export const FLASH_PAGE_COUNT = 8000; + +export const CRASH_TITLE = "SYCL BADGE SIM"; + +// Memory layout +export const ADDR_CONTROLS = 0x04; +export const ADDR_LIGHT_LEVEL = 0x06; +export const ADDR_NEOPIXELS = 0x08; +export const ADDR_RED_LED = 0x1c; +export const ADDR_FRAMEBUFFER = 0x1e; + +export const CONTROLS_START = 1; +export const CONTROLS_SELECT = 2; +export const CONTROLS_A = 4; +export const CONTROLS_B = 8; + +export const CONTROLS_CLICK = 16; +export const CONTROLS_UP = 32; +export const CONTROLS_DOWN = 64; +export const CONTROLS_LEFT = 128; +export const CONTROLS_RIGHT = 256; + +// Flags for Runtime.pauseState +export const PAUSE_CRASHED = 1; +export const PAUSE_REBOOTING = 2; + +export const OPTIONAL_COLOR_NONE = -1; + +export const FONT = Uint8Array.of( + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xc7, 0xc7, 0xc7, 0xcf, 0xcf, 0xff, 0xcf, 0xff, + 0x93, 0x93, 0x93, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x93, 0x01, 0x93, 0x93, 0x93, 0x01, 0x93, 0xff, + 0xef, 0x83, 0x2f, 0x83, 0xe9, 0x03, 0xef, 0xff, + 0x9d, 0x5b, 0x37, 0xef, 0xd9, 0xb5, 0x73, 0xff, + 0x8f, 0x27, 0x27, 0x8f, 0x25, 0x33, 0x81, 0xff, + 0xcf, 0xcf, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xf3, 0xe7, 0xcf, 0xcf, 0xcf, 0xe7, 0xf3, 0xff, + 0x9f, 0xcf, 0xe7, 0xe7, 0xe7, 0xcf, 0x9f, 0xff, + 0xff, 0x93, 0xc7, 0x01, 0xc7, 0x93, 0xff, 0xff, + 0xff, 0xe7, 0xe7, 0x81, 0xe7, 0xe7, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xcf, 0xcf, 0x9f, + 0xff, 0xff, 0xff, 0x81, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xcf, 0xcf, 0xff, + 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f, 0xff, + 0xc7, 0xb3, 0x39, 0x39, 0x39, 0x9b, 0xc7, 0xff, + 0xe7, 0xc7, 0xe7, 0xe7, 0xe7, 0xe7, 0x81, 0xff, + 0x83, 0x39, 0xf1, 0xc3, 0x87, 0x1f, 0x01, 0xff, + 0x81, 0xf3, 0xe7, 0xc3, 0xf9, 0x39, 0x83, 0xff, + 0xe3, 0xc3, 0x93, 0x33, 0x01, 0xf3, 0xf3, 0xff, + 0x03, 0x3f, 0x03, 0xf9, 0xf9, 0x39, 0x83, 0xff, + 0xc3, 0x9f, 0x3f, 0x03, 0x39, 0x39, 0x83, 0xff, + 0x01, 0x39, 0xf3, 0xe7, 0xcf, 0xcf, 0xcf, 0xff, + 0x87, 0x3b, 0x1b, 0x87, 0x61, 0x79, 0x83, 0xff, + 0x83, 0x39, 0x39, 0x81, 0xf9, 0xf3, 0x87, 0xff, + 0xff, 0xcf, 0xcf, 0xff, 0xcf, 0xcf, 0xff, 0xff, + 0xff, 0xcf, 0xcf, 0xff, 0xcf, 0xcf, 0x9f, 0xff, + 0xf3, 0xe7, 0xcf, 0x9f, 0xcf, 0xe7, 0xf3, 0xff, + 0xff, 0xff, 0x01, 0xff, 0x01, 0xff, 0xff, 0xff, + 0x9f, 0xcf, 0xe7, 0xf3, 0xe7, 0xcf, 0x9f, 0xff, + 0x83, 0x01, 0x39, 0xf3, 0xc7, 0xff, 0xc7, 0xff, + 0x83, 0x7d, 0x45, 0x55, 0x41, 0x7f, 0x83, 0xff, + 0xc7, 0x93, 0x39, 0x39, 0x01, 0x39, 0x39, 0xff, + 0x03, 0x39, 0x39, 0x03, 0x39, 0x39, 0x03, 0xff, + 0xc3, 0x99, 0x3f, 0x3f, 0x3f, 0x99, 0xc3, 0xff, + 0x07, 0x33, 0x39, 0x39, 0x39, 0x33, 0x07, 0xff, + 0x01, 0x3f, 0x3f, 0x03, 0x3f, 0x3f, 0x01, 0xff, + 0x01, 0x3f, 0x3f, 0x03, 0x3f, 0x3f, 0x3f, 0xff, + 0xc1, 0x9f, 0x3f, 0x31, 0x39, 0x99, 0xc1, 0xff, + 0x39, 0x39, 0x39, 0x01, 0x39, 0x39, 0x39, 0xff, + 0x81, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0x81, 0xff, + 0xf9, 0xf9, 0xf9, 0xf9, 0xf9, 0x39, 0x83, 0xff, + 0x39, 0x33, 0x27, 0x0f, 0x07, 0x23, 0x31, 0xff, + 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x9f, 0x81, 0xff, + 0x39, 0x11, 0x01, 0x01, 0x29, 0x39, 0x39, 0xff, + 0x39, 0x19, 0x09, 0x01, 0x21, 0x31, 0x39, 0xff, + 0x83, 0x39, 0x39, 0x39, 0x39, 0x39, 0x83, 0xff, + 0x03, 0x39, 0x39, 0x39, 0x03, 0x3f, 0x3f, 0xff, + 0x83, 0x39, 0x39, 0x39, 0x21, 0x33, 0x85, 0xff, + 0x03, 0x39, 0x39, 0x31, 0x07, 0x23, 0x31, 0xff, + 0x87, 0x33, 0x3f, 0x83, 0xf9, 0x39, 0x83, 0xff, + 0x81, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xff, + 0x39, 0x39, 0x39, 0x39, 0x39, 0x39, 0x83, 0xff, + 0x39, 0x39, 0x39, 0x11, 0x83, 0xc7, 0xef, 0xff, + 0x39, 0x39, 0x29, 0x01, 0x01, 0x11, 0x39, 0xff, + 0x39, 0x11, 0x83, 0xc7, 0x83, 0x11, 0x39, 0xff, + 0x99, 0x99, 0x99, 0xc3, 0xe7, 0xe7, 0xe7, 0xff, + 0x01, 0xf1, 0xe3, 0xc7, 0x8f, 0x1f, 0x01, 0xff, + 0xc3, 0xcf, 0xcf, 0xcf, 0xcf, 0xcf, 0xc3, 0xff, + 0x7f, 0xbf, 0xdf, 0xef, 0xf7, 0xfb, 0xfd, 0xff, + 0x87, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0x87, 0xff, + 0xc7, 0x93, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, + 0xef, 0xf7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x83, 0xf9, 0x81, 0x39, 0x81, 0xff, + 0x3f, 0x3f, 0x03, 0x39, 0x39, 0x39, 0x83, 0xff, + 0xff, 0xff, 0x81, 0x3f, 0x3f, 0x3f, 0x81, 0xff, + 0xf9, 0xf9, 0x81, 0x39, 0x39, 0x39, 0x81, 0xff, + 0xff, 0xff, 0x83, 0x39, 0x01, 0x3f, 0x83, 0xff, + 0xf1, 0xe7, 0x81, 0xe7, 0xe7, 0xe7, 0xe7, 0xff, + 0xff, 0xff, 0x81, 0x39, 0x39, 0x81, 0xf9, 0x83, + 0x3f, 0x3f, 0x03, 0x39, 0x39, 0x39, 0x39, 0xff, + 0xe7, 0xff, 0xc7, 0xe7, 0xe7, 0xe7, 0x81, 0xff, + 0xf3, 0xff, 0xe3, 0xf3, 0xf3, 0xf3, 0xf3, 0x87, + 0x3f, 0x3f, 0x31, 0x03, 0x07, 0x23, 0x31, 0xff, + 0xc7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0x81, 0xff, + 0xff, 0xff, 0x03, 0x49, 0x49, 0x49, 0x49, 0xff, + 0xff, 0xff, 0x03, 0x39, 0x39, 0x39, 0x39, 0xff, + 0xff, 0xff, 0x83, 0x39, 0x39, 0x39, 0x83, 0xff, + 0xff, 0xff, 0x03, 0x39, 0x39, 0x03, 0x3f, 0x3f, + 0xff, 0xff, 0x81, 0x39, 0x39, 0x81, 0xf9, 0xf9, + 0xff, 0xff, 0x91, 0x8f, 0x9f, 0x9f, 0x9f, 0xff, + 0xff, 0xff, 0x83, 0x3f, 0x83, 0xf9, 0x03, 0xff, + 0xe7, 0xe7, 0x81, 0xe7, 0xe7, 0xe7, 0xe7, 0xff, + 0xff, 0xff, 0x39, 0x39, 0x39, 0x39, 0x81, 0xff, + 0xff, 0xff, 0x99, 0x99, 0x99, 0xc3, 0xe7, 0xff, + 0xff, 0xff, 0x49, 0x49, 0x49, 0x49, 0x81, 0xff, + 0xff, 0xff, 0x39, 0x01, 0xc7, 0x01, 0x39, 0xff, + 0xff, 0xff, 0x39, 0x39, 0x39, 0x81, 0xf9, 0x83, + 0xff, 0xff, 0x01, 0xe3, 0xc7, 0x8f, 0x01, 0xff, + 0xf3, 0xe7, 0xe7, 0xcf, 0xe7, 0xe7, 0xf3, 0xff, + 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xe7, 0xff, + 0x9f, 0xcf, 0xcf, 0xe7, 0xcf, 0xcf, 0x9f, 0xff, + 0xff, 0xff, 0x8f, 0x45, 0xe3, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x93, 0x93, 0xff, + 0x83, 0x29, 0x29, 0x11, 0x29, 0x29, 0x83, 0xff, + 0x83, 0x39, 0x09, 0x11, 0x21, 0x39, 0x83, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x83, 0x11, 0x21, 0x7d, 0x21, 0x11, 0x83, 0xff, + 0x83, 0x11, 0x09, 0x7d, 0x09, 0x11, 0x83, 0xff, + 0x83, 0x11, 0x39, 0x55, 0x11, 0x11, 0x83, 0xff, + 0x83, 0x11, 0x11, 0x55, 0x39, 0x11, 0x83, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xe7, 0xff, 0xe7, 0xe7, 0xc7, 0xc7, 0xc7, 0xff, + 0xef, 0x83, 0x29, 0x2f, 0x29, 0x83, 0xef, 0xff, + 0xc3, 0x99, 0x9f, 0x03, 0x9f, 0x9f, 0x01, 0xff, + 0xff, 0xa5, 0xdb, 0xdb, 0xdb, 0xa5, 0xff, 0xff, + 0x99, 0x99, 0xc3, 0x81, 0xe7, 0x81, 0xe7, 0xff, + 0xe7, 0xe7, 0xe7, 0xff, 0xe7, 0xe7, 0xe7, 0xff, + 0xc3, 0x99, 0x87, 0xdb, 0xe1, 0x99, 0xc3, 0xff, + 0x93, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xc3, 0xbd, 0x66, 0x5e, 0x5e, 0x66, 0xbd, 0xc3, + 0x87, 0xc3, 0x93, 0xc3, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xc9, 0x93, 0x27, 0x93, 0xc9, 0xff, 0xff, + 0xff, 0xff, 0x81, 0xf9, 0xf9, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xc3, 0xbd, 0x46, 0x5a, 0x46, 0x5a, 0xbd, 0xc3, + 0x83, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xef, 0xd7, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xe7, 0xe7, 0x81, 0xe7, 0xe7, 0xff, 0x81, 0xff, + 0xc7, 0xf3, 0xe7, 0xc3, 0xff, 0xff, 0xff, 0xff, + 0xc3, 0xe7, 0xf3, 0xc7, 0xff, 0xff, 0xff, 0xff, + 0xf7, 0xef, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x33, 0x33, 0x33, 0x33, 0x09, 0x3f, + 0xc1, 0x95, 0xb5, 0x95, 0xc1, 0xf5, 0xf5, 0xff, + 0xff, 0xff, 0xff, 0xcf, 0xcf, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf7, 0xcf, + 0xe7, 0xc7, 0xe7, 0xc3, 0xff, 0xff, 0xff, 0xff, + 0xc7, 0x93, 0x93, 0xc7, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x27, 0x93, 0xc9, 0x93, 0x27, 0xff, 0xff, + 0xbd, 0x3b, 0xb7, 0xad, 0xd9, 0xb1, 0x7d, 0xff, + 0xbd, 0x3b, 0xb7, 0xa9, 0xdd, 0xbb, 0x71, 0xff, + 0x1d, 0xbb, 0xd7, 0x2d, 0xd9, 0xb1, 0x7d, 0xff, + 0xc7, 0xff, 0xc7, 0x9f, 0x39, 0x01, 0x83, 0xff, + 0xdf, 0xef, 0xc7, 0x93, 0x39, 0x01, 0x39, 0xff, + 0xf7, 0xef, 0xc7, 0x93, 0x39, 0x01, 0x39, 0xff, + 0xc7, 0x93, 0xc7, 0x93, 0x39, 0x01, 0x39, 0xff, + 0xcb, 0xa7, 0xc7, 0x93, 0x39, 0x01, 0x39, 0xff, + 0x93, 0xff, 0xc7, 0x93, 0x39, 0x01, 0x39, 0xff, + 0xef, 0xd7, 0xc7, 0x93, 0x39, 0x01, 0x39, 0xff, + 0xc1, 0x87, 0x27, 0x21, 0x07, 0x27, 0x21, 0xff, + 0xc3, 0x99, 0x3f, 0x3f, 0x99, 0xc3, 0xf7, 0xcf, + 0xdf, 0xef, 0x01, 0x3f, 0x03, 0x3f, 0x01, 0xff, + 0xf7, 0xef, 0x01, 0x3f, 0x03, 0x3f, 0x01, 0xff, + 0xc7, 0x93, 0x01, 0x3f, 0x03, 0x3f, 0x01, 0xff, + 0x93, 0xff, 0x01, 0x3f, 0x03, 0x3f, 0x01, 0xff, + 0xef, 0xf7, 0x81, 0xe7, 0xe7, 0xe7, 0x81, 0xff, + 0xf7, 0xef, 0x81, 0xe7, 0xe7, 0xe7, 0x81, 0xff, + 0xe7, 0xc3, 0x81, 0xe7, 0xe7, 0xe7, 0x81, 0xff, + 0x99, 0xff, 0x81, 0xe7, 0xe7, 0xe7, 0x81, 0xff, + 0x87, 0x93, 0x99, 0x09, 0x99, 0x93, 0x87, 0xff, + 0xcb, 0xa7, 0x19, 0x09, 0x01, 0x21, 0x31, 0xff, + 0xdf, 0xef, 0x83, 0x39, 0x39, 0x39, 0x83, 0xff, + 0xf7, 0xef, 0x83, 0x39, 0x39, 0x39, 0x83, 0xff, + 0xc7, 0x93, 0x83, 0x39, 0x39, 0x39, 0x83, 0xff, + 0xcb, 0xa7, 0x83, 0x39, 0x39, 0x39, 0x83, 0xff, + 0x93, 0xff, 0x83, 0x39, 0x39, 0x39, 0x83, 0xff, + 0xff, 0xbb, 0xd7, 0xef, 0xd7, 0xbb, 0xff, 0xff, + 0x83, 0x39, 0x31, 0x29, 0x19, 0x39, 0x83, 0xff, + 0xdf, 0xef, 0x39, 0x39, 0x39, 0x39, 0x83, 0xff, + 0xf7, 0xef, 0x39, 0x39, 0x39, 0x39, 0x83, 0xff, + 0xc7, 0x93, 0xff, 0x39, 0x39, 0x39, 0x83, 0xff, + 0x93, 0xff, 0x39, 0x39, 0x39, 0x39, 0x83, 0xff, + 0xf7, 0xef, 0x99, 0x99, 0xc3, 0xe7, 0xe7, 0xff, + 0x3f, 0x03, 0x39, 0x39, 0x39, 0x03, 0x3f, 0xff, + 0xc3, 0x99, 0x99, 0x93, 0x99, 0x89, 0x93, 0xff, + 0xdf, 0xef, 0x83, 0xf9, 0x81, 0x39, 0x81, 0xff, + 0xf7, 0xef, 0x83, 0xf9, 0x81, 0x39, 0x81, 0xff, + 0xc7, 0x93, 0x83, 0xf9, 0x81, 0x39, 0x81, 0xff, + 0xcb, 0xa7, 0x83, 0xf9, 0x81, 0x39, 0x81, 0xff, + 0x93, 0xff, 0x83, 0xf9, 0x81, 0x39, 0x81, 0xff, + 0xef, 0xd7, 0x83, 0xf9, 0x81, 0x39, 0x81, 0xff, + 0xff, 0xff, 0x83, 0xe9, 0x81, 0x2f, 0x83, 0xff, + 0xff, 0xff, 0x81, 0x3f, 0x3f, 0x81, 0xf7, 0xcf, + 0xdf, 0xef, 0x83, 0x39, 0x01, 0x3f, 0x83, 0xff, + 0xf7, 0xef, 0x83, 0x39, 0x01, 0x3f, 0x83, 0xff, + 0xc7, 0x93, 0x83, 0x39, 0x01, 0x3f, 0x83, 0xff, + 0x93, 0xff, 0x83, 0x39, 0x01, 0x3f, 0x83, 0xff, + 0xdf, 0xef, 0xff, 0xc7, 0xe7, 0xe7, 0x81, 0xff, + 0xf7, 0xef, 0xff, 0xc7, 0xe7, 0xe7, 0x81, 0xff, + 0xc7, 0x93, 0xff, 0xc7, 0xe7, 0xe7, 0x81, 0xff, + 0x93, 0xff, 0xc7, 0xe7, 0xe7, 0xe7, 0x81, 0xff, + 0x9b, 0x87, 0x67, 0x83, 0x39, 0x39, 0x83, 0xff, + 0xcb, 0xa7, 0x03, 0x39, 0x39, 0x39, 0x39, 0xff, + 0xdf, 0xef, 0x83, 0x39, 0x39, 0x39, 0x83, 0xff, + 0xf7, 0xef, 0x83, 0x39, 0x39, 0x39, 0x83, 0xff, + 0xc7, 0x93, 0x83, 0x39, 0x39, 0x39, 0x83, 0xff, + 0xcb, 0xa7, 0x83, 0x39, 0x39, 0x39, 0x83, 0xff, + 0x93, 0xff, 0x83, 0x39, 0x39, 0x39, 0x83, 0xff, + 0xff, 0xe7, 0xff, 0x81, 0xff, 0xe7, 0xff, 0xff, + 0xff, 0xff, 0x83, 0x31, 0x29, 0x19, 0x83, 0xff, + 0xdf, 0xef, 0x39, 0x39, 0x39, 0x39, 0x81, 0xff, + 0xf7, 0xef, 0x39, 0x39, 0x39, 0x39, 0x81, 0xff, + 0xc7, 0x93, 0xff, 0x39, 0x39, 0x39, 0x81, 0xff, + 0x93, 0xff, 0x39, 0x39, 0x39, 0x39, 0x81, 0xff, + 0xf7, 0xef, 0x39, 0x39, 0x39, 0x81, 0xf9, 0x83, + 0x3f, 0x3f, 0x03, 0x39, 0x39, 0x03, 0x3f, 0x3f, + 0x93, 0xff, 0x39, 0x39, 0x39, 0x81, 0xf9, 0x83 +); diff --git a/simulator/src/framebuffer.ts b/simulator/src/framebuffer.ts new file mode 100644 index 0000000..436851d --- /dev/null +++ b/simulator/src/framebuffer.ts @@ -0,0 +1,306 @@ +import { + FONT, + WIDTH, + HEIGHT, + ADDR_FRAMEBUFFER, + OPTIONAL_COLOR_NONE, +} from "./constants"; +import { unpack565 } from "./ui/utils"; + +export class Framebuffer { + bytes: Uint16Array; + + constructor (memory: ArrayBuffer) { + this.bytes = new Uint16Array(memory, ADDR_FRAMEBUFFER, WIDTH * HEIGHT); + } + + fillScreen (color: number): void { + this.bytes.fill(color); + } + + drawPoint (color: number, x: number, y: number) { + this.bytes[WIDTH * y + x] = color; + } + + drawPointUnclipped (color: number, x: number, y: number) { + if (x >= 0 && x < WIDTH && y >= 0 && y < HEIGHT) { + this.drawPoint(color, x, y); + } + } + + drawHLineFast(color: number, startX: number, y: number, endX: number) { + const yOff = WIDTH * y; + this.bytes.fill(color, yOff + startX, yOff + endX); + } + + drawHLineUnclipped(color: number, startX: number, y: number, endX: number) { + if (y >= 0 && y < HEIGHT) { + if (startX < 0) { + startX = 0; + } + if (endX > WIDTH) { + endX = WIDTH; + } + if (startX < endX) { + this.drawHLineFast(color, startX, y, endX); + } + } + } + + drawHLine(color: number, x: number, y: number, len: number) { + this.drawHLineUnclipped(color, x, y, x + len); + } + + drawVLine(color: number, x: number, y: number, len: number) { + if (y + len <= 0 || x < 0 || x >= WIDTH) { + return; + } + + const startY = Math.max(0, y); + const endY = Math.min(HEIGHT, y + len); + + for (let yy = startY; yy < endY; yy++) { + this.drawPoint(color, x, yy); + } + } + + drawRect(strokeColor: number, fillColor: number, x: number, y: number, width: number, height: number) { + if (strokeColor === OPTIONAL_COLOR_NONE && fillColor === OPTIONAL_COLOR_NONE) { + return; + } + + const startX = Math.max(0, x); + const startY = Math.max(0, y); + const endXUnclamped = x + width; + const endYUnclamped = y + height; + const endX = Math.min(endXUnclamped, WIDTH); + const endY = Math.min(endYUnclamped, HEIGHT); + + if (fillColor !== OPTIONAL_COLOR_NONE) { + for (let yy = startY; yy < endY; ++yy) { + this.drawHLineFast(fillColor, startX, yy, endX); + } + } + + if (strokeColor !== OPTIONAL_COLOR_NONE) { + // Left edge + if (x >= 0 && x < WIDTH) { + for (let yy = startY; yy < endY; ++yy) { + this.drawPoint(strokeColor, x, yy); + } + } + + // Right edge + if (endXUnclamped > 0 && endXUnclamped <= WIDTH) { + for (let yy = startY; yy < endY; ++yy) { + this.drawPoint(strokeColor, endXUnclamped - 1, yy); + } + } + + // Top edge + if (y >= 0 && y < HEIGHT) { + this.drawHLineFast(strokeColor, startX, y, endX); + } + + // Bottom edge + if (endYUnclamped > 0 && endYUnclamped <= HEIGHT) { + this.drawHLineFast(strokeColor, startX, endYUnclamped - 1, endX); + } + } + } + + // Oval drawing function using a variation on the midpoint algorithm. + // TIC-80's ellipse drawing function used as reference. + // https://github.com/nesbox/TIC-80/blob/main/src/core/draw.c + // + // Javatpoint has a in depth academic explanation that mostly went over my head: + // https://www.javatpoint.com/computer-graphics-midpoint-ellipse-algorithm + // + // Draws the ellipse by "scanning" along the edge in one quadrant, and mirroring + // the movement for the other four quadrants. + // + // There are a lot of details to get correct while implementing this algorithm, + // so ensure the edge cases are covered when changing it. Long, thin ellipses + // are particularly susceptible to being drawn incorrectly. + drawOval (strokeColor: number, fillColor: number, x: number, y: number, width: number, height: number) { + if (strokeColor === OPTIONAL_COLOR_NONE && fillColor === OPTIONAL_COLOR_NONE) { + return; + } + + let a = width - 1; + const b = height - 1; + let b1 = b % 2; // Compensates for precision loss when dividing + + let north = y + Math.floor(height / 2); // Precision loss here + let west = x; + let east = x + width - 1; + let south = north - b1; // Compensation here. Moves the bottom line up by + // one (overlapping the top line) for even heights + + const a2 = a * a; + const b2 = b * b; + + // Error increments. Also known as the decision parameters + let dx = 4 * (1 - a) * b2; + let dy = 4 * (b1 + 1) * a2; + + // Error of 1 step + let err = dx + dy + b1 * a2; + + a = 8 * a2; + b1 = 8 * b2; + + do { + if (strokeColor !== OPTIONAL_COLOR_NONE) { + this.drawPointUnclipped(strokeColor, east, north); /* I. Quadrant */ + this.drawPointUnclipped(strokeColor, west, north); /* II. Quadrant */ + this.drawPointUnclipped(strokeColor, west, south); /* III. Quadrant */ + this.drawPointUnclipped(strokeColor, east, south); /* IV. Quadrant */ + } + + const start = west + 1; + const len = east - start; + + if (fillColor !== OPTIONAL_COLOR_NONE && len > 0) { // Only draw fill if the length from west to east is not 0 + this.drawHLineUnclipped(fillColor, start, north, east); /* I and III. Quadrant */ + this.drawHLineUnclipped(fillColor, start, south, east); /* II and IV. Quadrant */ + } + + const err2 = 2 * err; + + if (err2 <= dy) { + // Move vertical scan + north += 1; + south -= 1; + dy += a; + err += dy; + } + + if (err2 >= dx || err2 > dy) { + // Move horizontal scan + west += 1; + east -= 1; + dx += b1; + err += dx; + } + } while (west <= east); + + if (strokeColor !== OPTIONAL_COLOR_NONE) { + // Make sure north and south have moved the entire way so top/bottom aren't missing + while (north - south < height) { + this.drawPointUnclipped(strokeColor, west - 1, north); /* II. Quadrant */ + this.drawPointUnclipped(strokeColor, east + 1, north); /* I. Quadrant */ + north += 1; + this.drawPointUnclipped(strokeColor, west - 1, south); /* III. Quadrant */ + this.drawPointUnclipped(strokeColor, east + 1, south); /* IV. Quadrant */ + south -= 1; + } + } + } + + // From https://github.com/nesbox/TIC-80/blob/master/src/core/draw.c + drawLine (color: number, x1: number, y1: number, x2: number, y2: number) { + if (y1 > y2) { + let swap = x1; + x1 = x2; + x2 = swap; + + swap = y1; + y1 = y2; + y2 = swap; + } + + const dx = Math.abs(x2 - x1), sx = x1 < x2 ? 1 : -1; + const dy = y2 - y1; + let err = (dx > dy ? dx : -dy) / 2, e2; + + for (;;) { + this.drawPointUnclipped(color, x1, y1); + if (x1 === x2 && y1 === y2) { + break; + } + e2 = err; + if (e2 > -dx) { + err -= dy; + x1 += sx; + } + if (e2 < dy) { + err += dx; + y1++; + } + } + } + + drawText (textColor: number, backgroundColor: number, charArray: number[] | Uint8Array | Uint8ClampedArray | Uint16Array, x: number, y: number) { + let currentX = x; + for (let ii = 0, len = charArray.length; ii < len; ++ii) { + const charCode = charArray[ii]; + if (charCode === 10) { + y += 8; + currentX = x; + } else if (charCode >= 32 && charCode <= 255) { + this.blit([textColor, backgroundColor], FONT, currentX, y, 8, 8, 0, (charCode - 32) << 3, 8); + currentX += 8; + } else { + currentX += 8; + } + } + } + + blit ( + colors: [number, number] | [number, number, number, number], + sprite: Uint8Array, + dstX: number, dstY: number, + width: number, height: number, + srcX: number, srcY: number, + srcStride: number, + flipX: number | boolean = false, + flipY: number | boolean = false, + rotate: number | boolean = false + ) { + // Clip rectangle to screen + let clipXMin, clipYMin, clipXMax, clipYMax; + if (rotate) { + flipX = !flipX; + clipXMin = Math.max(0, dstY) - dstY; + clipYMin = Math.max(0, dstX) - dstX; + clipXMax = Math.min(width, HEIGHT - dstY); + clipYMax = Math.min(height, WIDTH - dstX); + } else { + clipXMin = Math.max(0, dstX) - dstX; + clipYMin = Math.max(0, dstY) - dstY; + clipXMax = Math.min(width, WIDTH - dstX); + clipYMax = Math.min(height, HEIGHT - dstY); + } + + // Iterate pixels in rectangle + for (let y = clipYMin; y < clipYMax; y++) { + for (let x = clipXMin; x < clipXMax; x++) { + // Calculate sprite target coords + const tx = dstX + (rotate ? y : x); + const ty = dstY + (rotate ? x : y); + + // Calculate sprite source coords + const sx = srcX + (flipX ? width - x - 1 : x); + const sy = srcY + (flipY ? height - y - 1 : y); + + // Sample the sprite to get a color index + let colorIdx; + const bitIndex = sy * srcStride + sx; + if (colors.length === 4) { + const byte = sprite[bitIndex >>> 2]; + const shift = 6 - ((bitIndex & 0x03) << 1); + colorIdx = (byte >>> shift) & 0b11; + } else { + const byte = sprite[bitIndex >>> 3]; + const shift = 7 - (bitIndex & 0x7); + colorIdx = (byte >>> shift) & 0b1; + } + + if (colors[colorIdx] !== OPTIONAL_COLOR_NONE) { + this.drawPoint(colors[colorIdx], tx, ty); + } + } + } + } +} diff --git a/simulator/src/index.ts b/simulator/src/index.ts new file mode 100644 index 0000000..10d6f3e --- /dev/null +++ b/simulator/src/index.ts @@ -0,0 +1,9 @@ +import "./styles.css"; + +export * from "./ui/app"; +export * from "./ui/menu-overlay"; +// TODO +// export * from "./ui/virtual-gamepad"; +export * from "./ui/notifications"; +export * from "./ui/leds"; +export * from "./ui/light-sensor"; diff --git a/simulator/src/runtime.ts b/simulator/src/runtime.ts new file mode 100644 index 0000000..cb1456c --- /dev/null +++ b/simulator/src/runtime.ts @@ -0,0 +1,299 @@ +import * as constants from "./constants"; +import * as z85 from "./z85"; +import { APU } from "./apu"; +import { Framebuffer } from "./framebuffer"; +import { WebGLCompositor } from "./compositor"; +import { pack565, unpack565 } from "./ui/utils"; + +export class Runtime { + canvas: HTMLCanvasElement; + memory: WebAssembly.Memory; + apu: APU; + compositor: WebGLCompositor; + data: DataView; + framebuffer: Framebuffer; + pauseState: number; + wasmBuffer: Uint8Array | null = null; + wasmBufferByteLen: number; + wasm: WebAssembly.Instance | null = null; + warnedFileSize = false; + + flashBuffer: ArrayBuffer; + + constructor () { + const canvas = document.createElement("canvas"); + canvas.width = constants.WIDTH; + canvas.height = constants.HEIGHT; + this.canvas = canvas; + + const gl = canvas.getContext("webgl2", { + alpha: false, + depth: false, + antialias: false, + }); + + if(!gl) { + throw new Error('web-runtime: could not create wegl context') // TODO(2021-08-01): Fallback to Canvas2DCompositor + } + + this.compositor = new WebGLCompositor(gl); + + this.apu = new APU(); + + this.flashBuffer = new ArrayBuffer(constants.FLASH_PAGE_SIZE); + + this.memory = new WebAssembly.Memory({initial: 1, maximum: 1}); + this.data = new DataView(this.memory.buffer); + + this.framebuffer = new Framebuffer(this.memory.buffer); + + this.reset(); + + this.pauseState = constants.PAUSE_REBOOTING; + this.wasmBufferByteLen = 0; + } + + async init () { + await this.apu.init(); + } + + setControls (controls: number) { + this.data.setUint16(constants.ADDR_CONTROLS, controls, true); + } + + setLightLevel (value: number) { + this.data.setUint16(constants.ADDR_LIGHT_LEVEL, value, true); + } + + getNeopixels (): [number, number, number, number, number] { + const mem32 = new Uint32Array(this.data.buffer, constants.ADDR_NEOPIXELS); + return [ + mem32[0] & 0b1111_1111_1111_1111_1111_1111, + mem32[1] & 0b1111_1111_1111_1111_1111_1111, + mem32[2] & 0b1111_1111_1111_1111_1111_1111, + mem32[3] & 0b1111_1111_1111_1111_1111_1111, + mem32[4] & 0b1111_1111_1111_1111_1111_1111 + ]; + } + + getRedLed(): boolean { + return this.data.getUint8(constants.ADDR_RED_LED) !== 0; + } + + unlockAudio () { + this.apu.unlockAudio(); + } + + pauseAudio() { + this.apu.pauseAudio(); + } + + reset (zeroMemory?: boolean) { + // Initialize default color table and palette + const mem32 = new Uint32Array(this.memory.buffer); + if (zeroMemory) { + mem32.fill(0); + } + this.pauseState &= ~constants.PAUSE_CRASHED; + } + + async load (wasmBuffer: Uint8Array, enforceSizeLimit = true) { + const limit = 1 << 16; + this.wasmBuffer = wasmBuffer; + this.wasmBufferByteLen = wasmBuffer.byteLength; + this.wasm = null; + + if (wasmBuffer.byteLength > limit) { + if (!this.warnedFileSize) { + this.warnedFileSize = true; + this.print(`Warning: Cart is larger than ${limit} bytes. Ensure the release build of your cart is small enough to be bundled.`); + } + } + + const env = { + memory: this.memory, + + rect: this.framebuffer.drawRect.bind(this.framebuffer), + oval: this.framebuffer.drawOval.bind(this.framebuffer), + line: this.framebuffer.drawLine.bind(this.framebuffer), + + hline: this.framebuffer.drawHLine.bind(this.framebuffer), + vline: this.framebuffer.drawVLine.bind(this.framebuffer), + + text: this.text.bind(this), + + blit: this.blit.bind(this), + blit_sub: this.blitSub.bind(this), + + tone: this.apu.tone.bind(this.apu), + + read_flash: this.read_flash.bind(this), + write_flash_page: this.write_flash_page.bind(this), + + trace: this.trace.bind(this), + }; + + await this.bluescreenOnError(async () => { + const module = await WebAssembly.instantiate(wasmBuffer, { env }); + this.wasm = module.instance; + + // Call the WASI _start/_initialize function (different from WASM-4's start callback!) + if (typeof this.wasm.exports["_start"] === 'function') { + this.wasm.exports._start(); + } + if (typeof this.wasm.exports["_initialize"] === 'function') { + this.wasm.exports._initialize(); + } + }); + } + + async bluescreenOnError (fn: Function) { + try { + await fn(); + } catch (err) { + if (err instanceof Error) { + const errorExplanation = errorToBlueScreenText(err); + this.blueScreen(errorExplanation); + } + + throw err; + } + } + + text (textColor: number, backgroundColor: number, textPtr: number, byteLength: number, x: number, y: number) { + const text = new Uint8Array(this.memory.buffer, textPtr, byteLength); + this.framebuffer.drawText(textColor, backgroundColor, text, x, y); + } + + blit (colorsPtr: number, spritePtr: number, x: number, y: number, width: number, height: number, flags: number) { + this.blitSub(colorsPtr, spritePtr, x, y, width, height, 0, 0, width, flags); + } + + blitSub (colorsPtr: number, spritePtr: number, x: number, y: number, width: number, height: number, srcX: number, srcY: number, stride: number, flags: number) { + const sprite = new Uint8Array(this.memory.buffer, spritePtr); + const bpp2 = (flags & 1); + const flipX = (flags & 2); + const flipY = (flags & 4); + const rotate = (flags & 8); + + const colors = new Uint16Array(this.memory.buffer, colorsPtr); + + this.framebuffer.blit(bpp2 ? [colors[0], colors[1], colors[2], colors[3]] : [colors[0], colors[1]], sprite, x, y, width, height, srcX, srcY, stride, flipX, flipY, rotate); + } + + read_flash (offset: number, dstPtr: number, length: number): number { + const src = new Uint8Array(this.flashBuffer, offset, length); + const dst = new Uint8Array(this.memory.buffer, dstPtr, length); + + dst.set(src); + + return src.length; + } + + write_flash_page (page: number, srcPtr: number) { + // TODO: Make dangerous write crash!! + + const src = new Uint8Array(this.memory.buffer, srcPtr, constants.FLASH_PAGE_SIZE); + const dst = new Uint8Array(this.flashBuffer, page * constants.FLASH_PAGE_SIZE, constants.FLASH_PAGE_SIZE); + + dst.set(src); + } + + getCString (ptr: number) { + let str = ""; + for (;;) { + const c = this.data.getUint8(ptr++); + if (c == 0) { + break; + } + str += String.fromCharCode(c); + } + return str; + } + + print (str: string) { + console.log(str); + } + + trace (strUtf8Ptr: number, byteLength: number) { + const strUtf8 = new Uint8Array(this.memory.buffer, strUtf8Ptr, byteLength); + const str = new TextDecoder().decode(strUtf8); + this.print(str); + } + + start () { + let start_function = this.wasm!.exports["start"]; + if (typeof start_function === "function") { + this.bluescreenOnError(start_function); + } + } + + update () { + if (this.pauseState != 0) { + return; + } + + let update_function = this.wasm!.exports["update"]; + if (typeof update_function === "function") { + this.bluescreenOnError(update_function); + } + } + + blueScreen (text: string) { + this.pauseState |= constants.PAUSE_CRASHED; + + const blue = pack565(5, 10, 5); + const grey = pack565(25, 50, 25); + + const toCharArr = (s: string) => [...s].map(x => x.charCodeAt(0)); + + const title = ` ${constants.CRASH_TITLE} `; + const headerTitle = title; + const headerWidth = (8 * title.length); + const headerX = (160 - (8 * title.length)) / 2; + const headerY = 20; + const messageX = 9; + const messageY = 60; + + this.framebuffer.fillScreen(blue); + + this.framebuffer.drawHLine(grey, headerX, headerY-1, headerWidth); + this.framebuffer.drawText(blue, grey, toCharArr(headerTitle), headerX, headerY); + this.framebuffer.drawText(grey, blue, toCharArr(text), messageX, messageY); + + this.composite(); + } + + composite () { + this.compositor.composite(this.framebuffer); + } +} + +function errorToBlueScreenText(err: Error) { + // hand written messages for specific errors + if (err instanceof WebAssembly.RuntimeError) { + let message; + if (err.message.match(/unreachable/)) { + message = "The cartridge has\nreached a code \nsegment marked as\nunreachable."; + } else if (err.message.match(/out of bounds/)) { + message = "The cartridge has\nattempted a memory\naccess that is\nout of bounds."; + } + return message + "\n\n\n\n\nHit R to reboot."; + } else if (err instanceof WebAssembly.LinkError) { + return "The cartridge has\ntried to import\na missing function.\n\n\n\nSee console for\nmore details."; + } else if (err instanceof WebAssembly.CompileError) { + return "The cartridge is\ncorrupted.\n\n\n\nSee console for\nmore details."; + } else if (err instanceof Wasm4Error) { + return err.wasm4Message; + } + return "Unknown error.\n\n\n\nSee console for\nmore details."; +} + +class Wasm4Error extends Error { + wasm4Message: string; + constructor(w4Message: string) { + super(w4Message.replace('\n', ' ')); + this.name = "Wasm4Error"; + this.wasm4Message = w4Message; + } +} diff --git a/simulator/src/state.ts b/simulator/src/state.ts new file mode 100644 index 0000000..4666b25 --- /dev/null +++ b/simulator/src/state.ts @@ -0,0 +1,26 @@ +import * as constants from "./constants"; +import { Runtime } from "./runtime"; + +export class State { + memory: ArrayBuffer; + flashBuffer: ArrayBuffer; + + // TODO(2022-03-17): APU state + + constructor () { + this.memory = new ArrayBuffer(1 << 16); + this.flashBuffer = new ArrayBuffer(constants.FLASH_PAGE_SIZE * constants.FLASH_PAGE_COUNT); + } + + read (runtime: Runtime) { + new Uint8Array(this.memory).set(new Uint8Array(runtime.memory.buffer)); + + new Uint8Array(this.flashBuffer).set(new Uint8Array(runtime.flashBuffer, 0)); + } + + write (runtime: Runtime) { + new Uint8Array(runtime.memory.buffer).set(new Uint8Array(this.memory)); + + new Uint8Array(runtime.flashBuffer).set(new Uint8Array(this.flashBuffer, 0)); + } +} diff --git a/simulator/src/styles.css b/simulator/src/styles.css new file mode 100644 index 0000000..3e6d8d1 --- /dev/null +++ b/simulator/src/styles.css @@ -0,0 +1,10 @@ +html, body { + height: 100%; + margin: 0; +} + +/* https://github.com/codeman38/PressStart2P, with most glyphs stripped */ +@font-face { + font-family: wasm4-font; + src: url(data:font/woff2;base64,d09GMgABAAAAAAegAA0AAAAAKHwAAAdOAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYGYAA0CAQRCArGHLlRC4FCAAE2AiQDgUIEIAWDJgcgG4ggUVRQhkQRbBylAWCR/eGAmzBgcbVSEQV1MFRs8Ts/TVirjdcmzZum+foZqYGq4TdA2FbzSDx8f5+ee7Ns74fim/BWoE4AP1Wwc6lakA20VO54RMrR2gpfsF97s/P3xKMD8UIkBZNFk2ondPz/n9v9G4ROvGcJ9ejaJw8RK4kYUOt8zO4KgUDgofBQGDwMCjsf2FAHKMuSxfQKGICKTqpw8TAYfHywFNs02zbbJDAgUAD+z7l4byADjcSJjXhAkX+V15z5PA0HfmEHdHqW5OfXT1PsVLlV5AkPgm14EPW/1ds0nWuhRkHhmb/7JBfdZazTpbTGMrxynHPs9XO634fF7LHGGYvKrld7xSsgpTyfJqABJJGiNEP0JXXyPy/gUd+Oq0QckHKAEocXne/KDv7oOwSG0MMrDDRgQ0HCAFZ0Ar7LFhgi+t39/2Lzv/+fFLDLD/G/EugBH+CBbj+wXA/giv6ofkm3ZAqiNhrXlgkd4AIQwJg18/ES0QEKIZXedNDpbH9FmDzLD3D97q2rzT3TRXdngPKMRazVYXWiUtlpVnZTprWt4J6wbQiLK7o/u+vhZr91OWHc2/gH+Qyq505NGWB7ui1DWkl94VWLPsznaf1Socl++32X0B1fLASbyLYi6TAO6LlJoPwXg5YAIDQZ2GxXd7yaLzKdTNrAjkIbD1Sca4fUrWmNr+IUViUJVbuuu66KDZXmnH4fMCQ5ukPCRJDdgit52b4Et5RuCK2ZjuBXWu6K6zJkW4qAxA6Y0BWJpS9UgZPLQ3iSuQWAodKrD1VK44bhgApzKGely9pyBS45qWsGAx7ELrGRNkqZX2rpG1no3KgrF9zv29HXdcAmEYpTeqXQunmcM1KNLri1l3ETKhr/jkuZpYQWCtMUkAOA+G31Ezc8TlQCcX0AFiJM8A/Ja5ANNF3s+Aj1HaR91PPZwCslbEBH81Bta7KBGjhAdpteqxIHxKrVrglxhbYG0qRrxPwYd+RA3LtpvfDZhqQKKExdOI4SSCr8hboPCCdUxuYcsLQ2TWw8XO+W8S8kg28tTsGhGLgAGRwq7qpSd5XiJn8+0m7km8BBpAhQ5rqdtpQ58nsk6UVIA1DbFmX+klm8NFI+mk+Ll5BXDAY1ZsGRNeDGIUiLnIISx8u1n++kPlx5KbJLJOSiIDP1dfY4drhKlPE7oTwc9gEI5vBFDpQ2ih/QCB4V+GrFeAMbdFltBSpSqZm9jTuVVM1CVTyG+Haj1uZhNOXIU0xda+WccPIV0vAyy13AXHS7odKqX6lPnNQW29uptFF7hjud+bei1obGQ3mmpvE7YI+21Nlt7iq+tVb3XTcUFG5q31u2uVI1qvk971yZ+/NvzpZV1dNa7Wrems+vXZ7G0bjYrBMdM44xvrdfytxyouoAmPMIUnc0H1njzFfrgVdARZ2VnWXVw+0qv/fX8iYj698zzrSTOlorbpYF7dsMJFE7yW5kV/NV/wwJ/6VW+S3vk6u7s/frQR4kpi7JSQUksyJhl0BSNPS65eUmD+i8UGEYHhd5zmQA0oU5xN+qs8fdwfJZAQaqNDFTgSowOASAVTi4nNRD5CwJVyFK13cHxDEeWVQDvZU3CfsgXuwhIGxxAGE3sxM1QNCL5oTRpAtVec0/pJXNfeLQLO+973quQzeYBhK7GMFx18+4DtE1AX90lFeaawcfPwKbh9CZncWGRig514Ca6r5aIcMk1u7R7OyRfIjVldpkv/8QTig8xMfDoNqXMmPq6rWqoKMWvmXNIhNcLI1TokIhQLNwOGTfZxXWtBtNJFOwSQNl+3DGVwiBVU4+Oq0FeJi5E8VTE1ABD05R60ZWTc49DS4M1nMCCzmZSm7M8UWDSoiGvZPrWrGYd4bKvbh8oXu1Pnv2vrfKud0bz5t2kT7Ti8FNP4L9IJy/PdAULtKZAlxz2FahwUZvcI6aZm61UI4qEJ3XUpHb3NZcIBs7HYrbHf7GgFYvCv4JTMlVK5o9e10zmwVpEoUbhD8pWxVbCX6xdKM4RGTiZ/2PaYEZMh7uAHi1vTTwNaeeqsmWeoX+fERAY8Dlq8IlZUVTrzi35D7ANkN8liq6UConAkh2mBUCA+jNZ4cnuXvJDuFGvEUlaWfzT5MjGonslkNYRjXtmwJEQgbrm1xCr4/AZMWbVPGZ3SvOH9q/AaXqXNGRhP+VuL2mvmu8/zv9BnB5l14cMu1MBSJ04++6QDnTv1bPp6oZnRul1ORTFVkMUlS1LsSh2h2L41VW44wWXcvRhJYwoLfn7pCnX7rB3Zs4otOwC568tbQs/egw8kfbjvhpM06nOc/rWqbg7EicnAydmY7O4nomxNHJws4WxYXFJFGl55716GhGRqqjKI9kp3x0+aiPQqzthaSFKiR6LmZAzFysLXdGb3rZfIi6TEU7jgcy2geINtNWQpaBCnYWTmxqWDw0wzgfrJvzphPhoIstcvGIzuZO4UN1requPbF1HWhqLtgPf3YiwnTA/xIgKAA=); +} diff --git a/simulator/src/ui/app.ts b/simulator/src/ui/app.ts new file mode 100644 index 0000000..9b6f4d6 --- /dev/null +++ b/simulator/src/ui/app.ts @@ -0,0 +1,478 @@ +import { LitElement, html, css } from "lit"; +import { customElement, state, query } from 'lit/decorators.js'; + +import * as constants from "../constants"; +import * as utils from "./utils"; +import * as z85 from "../z85"; +import { Runtime } from "../runtime"; +import { State } from "../state"; + +import { MenuOverlay } from "./menu-overlay"; +import { Notifications } from "./notifications"; +import { LEDs } from "./leds"; +import { LightSensor } from "./light-sensor"; + +@customElement("wasm4-app") +export class App extends LitElement { + static styles = css` + :host { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + + touch-action: none; + user-select: none; + -webkit-user-select: none; + -webkit-tap-highlight-color: transparent; + + background: #202020; + } + + .content { + width: 100vmin; + height: 100vmin; + overflow: hidden; + } + + /** Nudge the game upwards a bit in portrait to make space for the virtual gamepad. */ + @media (pointer: coarse) and (max-aspect-ratio: 2/3) { + .content { + position: absolute; + top: calc((100% - 220px - 100vmin)/2) + } + } + + .canvas-wrapper { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + } + + canvas { + width: 100%; + height: auto; + image-rendering: pixelated; + image-rendering: crisp-edges; + } + `; + + private readonly runtime: Runtime; + + @state() private showMenu = false; + + @query("wasm4-menu-overlay") private menuOverlay?: MenuOverlay; + @query("wasm4-notifications") private notifications!: Notifications; + @query("wasm4-leds") private hardwareComponents!: LEDs; + + private savedGameState?: State; + + public controls: number = 0; + public lightLevel: number = 0; + + private readonly gamepadUnavailableWarned = new Set(); + + readonly onPointerUp = (event: PointerEvent) => { + if (event.pointerType == "touch") { + // Try to go fullscreen on mobile + utils.requestFullscreen(); + } + + // Try to begin playing audio + this.runtime.unlockAudio(); + } + + constructor () { + super(); + + this.runtime = new Runtime(); + + this.init(); + } + + async init () { + const runtime = this.runtime; + await runtime.init(); + + const canvas = runtime.canvas; + + fetch("http://localhost:2468/cart.wasm").then(async res => { + await this.resetCart(new Uint8Array(await (res).arrayBuffer()), false); + }).catch(() => { + runtime.blueScreen("Watcher not found.\n\nStart and reload."); + }); + + const ws = new WebSocket(`ws://localhost:2468/ws`); + ws.onopen = w => { + setInterval(() => { + ws.send("spam"); + }, 1000); + } + + ws.onmessage = async m => { + await this.resetCart(new Uint8Array(await (await fetch("http://localhost:2468/cart.wasm")).arrayBuffer()), false); + } + + function takeScreenshot () { + // We need to render a frame first + runtime.composite(); + + canvas.toBlob(blob => { + const url = URL.createObjectURL(blob!); + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = "wasm4-screenshot.png"; + anchor.click(); + URL.revokeObjectURL(url); + }); + } + + let videoRecorder: MediaRecorder | null = null; + function recordVideo () { + if (videoRecorder != null) { + return; // Still recording, ignore + } + + const mimeType = "video/webm"; + const videoStream = canvas.captureStream(); + videoRecorder = new MediaRecorder(videoStream, { + mimeType, + videoBitsPerSecond: 25000000, + }); + + const chunks: Blob[] = []; + videoRecorder.ondataavailable = event => { + chunks.push(event.data); + }; + + videoRecorder.onstop = () => { + const blob = new Blob(chunks, { type: mimeType }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = "wasm4-animation.webm"; + anchor.click(); + URL.revokeObjectURL(url); + }; + + videoRecorder.start(); + setTimeout(() => { + if(videoRecorder) { + videoRecorder.requestData(); + videoRecorder.stop(); + videoRecorder = null; + } + }, 4000); + } + + canvas.addEventListener("contextmenu", event => { + event.preventDefault(); + }); + + const HOTKEYS: Record any> = { + "2": this.saveGameState.bind(this), + "4": this.loadGameState.bind(this), + "r": this.resetCart.bind(this), + "R": this.resetCart.bind(this), + "F9": takeScreenshot, + "F10": recordVideo, + "F11": utils.requestFullscreen, + "Escape": this.onMenuButtonPressed.bind(this), + }; + + const onKeyboardEvent = (event: KeyboardEvent) => { + if (event.ctrlKey || event.altKey) { + return; // Ignore ctrl/alt modified key presses because they may be the user trying to navigate + } + + if (event.srcElement instanceof HTMLElement && event.srcElement.tagName == "INPUT") { + return; // Ignore if we have an input element focused + } + + const down = (event.type == "keydown"); + + // Poke WebAudio + runtime.unlockAudio(); + + if (down) { + const hotkeyFn = HOTKEYS[event.key]; + if (hotkeyFn) { + hotkeyFn(); + event.preventDefault(); + return; + } + } + + let mask = 0; + switch (event.code) { + case "Space": + mask |= constants.CONTROLS_START; + break; + case "Enter": + mask |= constants.CONTROLS_SELECT; + break; + case "ShiftLeft": case "ShiftRight": + mask |= constants.CONTROLS_CLICK; + break; + case "ArrowUp": case "KeyW": + mask |= constants.CONTROLS_UP; + break; + case "ArrowDown": case "KeyS": + mask |= constants.CONTROLS_DOWN; + break; + case "ArrowLeft": case "KeyA": + mask |= constants.CONTROLS_LEFT; + break; + case "ArrowRight": case "KeyD": + mask |= constants.CONTROLS_RIGHT; + break; + } + + if (mask != 0) { + event.preventDefault(); + + // Set or clear the button bit from the next input state + if (down) { + this.controls |= mask; + } else { + this.controls &= ~mask; + } + } + }; + window.addEventListener("keydown", onKeyboardEvent); + window.addEventListener("keyup", onKeyboardEvent); + + // Also listen to the top frame when we're embedded in an iframe + if (top && top != window) { + try { + top.addEventListener("keydown", onKeyboardEvent); + top.addEventListener("keyup", onKeyboardEvent); + } catch { + // Ignore iframe security errors + } + } + + const pollPhysicalGamepads = () => { + // TODO + // if (!navigator.getGamepads) { + // return; // Browser doesn't support gamepads + // } + + // for (const gamepad of navigator.getGamepads()) { + // if (gamepad == null) { + // continue; // Disconnected gamepad + // } else if (gamepad.mapping != "standard") { + // // The gamepad is available, but nonstandard, so we don't actually know how to read it. + // // Let's warn once, and not use this gamepad afterwards. + // if (!this.gamepadUnavailableWarned.has(gamepad.id)) { + // this.gamepadUnavailableWarned.add(gamepad.id); + // this.notifications.show("Unsupported gamepad: " + gamepad.id); + // } + // continue; + // } + + // // https://www.w3.org/TR/gamepad/#remapping + // const buttons = gamepad.buttons; + // const axes = gamepad.axes; + + // let mask = 0; + // if (buttons[12].pressed || axes[1] < -0.5) { + // mask |= constants.BUTTON_UP; + // } + // if (buttons[13].pressed || axes[1] > 0.5) { + // mask |= constants.BUTTON_DOWN; + // } + // if (buttons[14].pressed || axes[0] < -0.5) { + // mask |= constants.BUTTON_LEFT; + // } + // if (buttons[15].pressed || axes[0] > 0.5) { + // mask |= constants.BUTTON_RIGHT; + // } + // if (buttons[0].pressed || buttons[3].pressed || buttons[5].pressed || buttons[7].pressed) { + // mask |= constants.BUTTON_X; + // } + // if (buttons[1].pressed || buttons[2].pressed || buttons[4].pressed || buttons[6].pressed) { + // mask |= constants.BUTTON_Z; + // } + + // if (buttons[9].pressed) { + // this.showMenu = true; + // } + + // this.inputState.gamepad[gamepad.index % 4] = mask; + // } + } + + // When we should perform the next update + let timeNextUpdate = performance.now(); + // Track the timestamp of the last frame + let lastTimeFrameStart = timeNextUpdate; + + const onFrame = (timeFrameStart: number) => { + requestAnimationFrame(onFrame); + + pollPhysicalGamepads(); + + if (this.menuOverlay != null) { + this.menuOverlay.applyInput(); + + return; // Pause updates and rendering + } + + let calledUpdate = false; + + // Prevent timeFrameStart from getting too far ahead and death spiralling + if (timeFrameStart - timeNextUpdate >= 200) { + timeNextUpdate = timeFrameStart; + } + + while (timeFrameStart >= timeNextUpdate) { + timeNextUpdate += 1000/60; + + // Pass inputs into runtime memory + runtime.setControls(this.controls); + runtime.setLightLevel(this.lightLevel); + runtime.update(); + calledUpdate = true; + + this.hardwareComponents.neopixels = runtime.getNeopixels(); + this.hardwareComponents.redLed = runtime.getRedLed(); + } + + if (calledUpdate) { + runtime.composite(); + + // if (import.meta.env.DEV) { + // // FIXED(2023-12-13): Pass the correct FPS for display + // devtoolsManager.updateCompleted(runtime, timeFrameStart - lastTimeFrameStart); + // lastTimeFrameStart = timeFrameStart; + // } + } + } + requestAnimationFrame(onFrame); + } + + onMenuButtonPressed () { + if (this.showMenu) { + // If the pause menu is already open, treat it as an X button + this.controls |= constants.CONTROLS_SELECT; + } else { + this.showMenu = true; + } + } + + closeMenu () { + if (this.showMenu) { + this.showMenu = false; + this.controls = 0; + } + } + + saveGameState () { + let state = this.savedGameState; + if (state == null) { + state = this.savedGameState = new State(); + } + state.read(this.runtime); + + this.notifications.show("State saved"); + } + + loadGameState () { + const state = this.savedGameState; + if (state != null) { + state.write(this.runtime); + this.notifications.show("State loaded"); + } else { + this.notifications.show("Need to save a state first"); + } + } + + importCart () { + const app = this; + const input = document.createElement("input"); + + input.style.display = "none"; + input.type = "file"; + input.accept = ".wasm"; + input.multiple = false; + + input.addEventListener("change", () => { + const files = input.files as FileList; + let reader = new FileReader(); + + reader.addEventListener("load", () => { + this.resetCart(new Uint8Array(reader.result as ArrayBuffer), false); + app.closeMenu(); + }); + + reader.readAsArrayBuffer(files[0]); + }); + + document.body.appendChild(input); + input.click(); + document.body.removeChild(input); + } + + async resetCart (wasmBuffer?: Uint8Array, preserveState: boolean = false) { + if (!wasmBuffer) { + wasmBuffer = this.runtime.wasmBuffer!; + } + + let state; + if (preserveState) { + // Take a snapshot + state = new State(); + state.read(this.runtime); + } + this.runtime.reset(true); + + + this.runtime.pauseState |= constants.PAUSE_REBOOTING; + await this.runtime.load(wasmBuffer); + this.runtime.pauseState &= ~constants.PAUSE_REBOOTING; + + if (state) { + // Restore the previous snapshot + state.write(this.runtime); + } else { + this.runtime.start(); + } + } + + connectedCallback () { + super.connectedCallback(); + + window.addEventListener("pointerup", this.onPointerUp); + } + + disconnectedCallback () { + window.removeEventListener("pointerup", this.onPointerUp); + + super.disconnectedCallback(); + } + + render () { + return html` + +
+ ${this.showMenu ? html``: ""} + +
+ ${this.runtime.canvas} +
+
+ + `; + // + } +} + +declare global { + interface HTMLElementTagNameMap { + "wasm4-app": App; + } +} diff --git a/simulator/src/ui/leds.ts b/simulator/src/ui/leds.ts new file mode 100644 index 0000000..531a2be --- /dev/null +++ b/simulator/src/ui/leds.ts @@ -0,0 +1,68 @@ +import { LitElement, html, css } from "lit"; +import { customElement, query, state } from 'lit/decorators.js'; +import { App } from "./app"; +import { unpack888 } from "./utils"; + +@customElement("wasm4-leds") +export class LEDs extends LitElement { + static styles = css` + :host { + position: absolute; + + bottom: 20px; + } + + .leds { + display: flex; + gap: 40px; + align-items: center; + } + + .neopixels { + display: flex; + gap: 10px; + align-items: center; + } + + .led-wrapper { + border: 3px solid black; + border-radius: 3px; + width: 40px; + height: 40px; + padding: 2.5px; + background-color: white; + } + + .led { + border-radius: 100px; + width: 100%; + height: 100%; + background-color: var(--color); + } + `; + + app!: App; + + @state() public neopixels: [number, number, number, number, number] = [0, 0, 0, 0, 0]; + @state() public redLed: boolean = false; + + render () { + return html` +
+
+ ${this.neopixels.map(_ => { + const [r, g, b] = unpack888(_); + return html`
`; + })} +
+
+
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "wasm4-leds": LEDs; + } +} diff --git a/simulator/src/ui/light-sensor.ts b/simulator/src/ui/light-sensor.ts new file mode 100644 index 0000000..4a2c60b --- /dev/null +++ b/simulator/src/ui/light-sensor.ts @@ -0,0 +1,58 @@ +import { LitElement, html, css } from "lit"; +import { customElement } from 'lit/decorators.js'; +import { App } from "./app"; + +@customElement("wasm4-light-sensor") +export class LightSensor extends LitElement { + static styles = css` + :host { + position: absolute; + + top: 30px; + } + + input[type="range"] { + position: relative; + -webkit-appearance: none; + appearance: none; + background: transparent; + cursor: pointer; + width: 15rem; + padding: 0px 0.1rem; + + height: 1.1rem; + border: 3px solid black; + border-radius: 20px; + background: linear-gradient(to right, black, yellow); + } + + input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + border: 3px solid black; + background-color: white; + height: 1.75rem; + width: 1rem; + z-index: 100; + border-radius: 3px; + } + `; + + app!: App; + + lightLevelChanged(event: Event) { + this.app.lightLevel = parseInt((event.target as HTMLInputElement).value); + } + + render () { + return html` + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "wasm4-light-sensor": LightSensor; + } +} diff --git a/simulator/src/ui/menu-overlay.ts b/simulator/src/ui/menu-overlay.ts new file mode 100644 index 0000000..cb8117b --- /dev/null +++ b/simulator/src/ui/menu-overlay.ts @@ -0,0 +1,236 @@ +import { LitElement, html, css } from "lit"; +import { customElement, state } from 'lit/decorators.js'; +import { map } from 'lit/directives/map.js'; + +import { App } from "./app"; +import * as constants from "../constants"; + +const optionContext = { + DEFAULT: 0, + DISK: 1, +}; + +const optionIndex = [ + { + CONTINUE: 0, + SAVE_STATE: 1, + LOAD_STATE: 2, + DISK_OPTIONS: 3, + LOAD_CART: 4, + // OPTIONS: null, + RESET_CART: 5, + }, + { + BACK: 0, + EXPORT_DISK: 1, + IMPORT_DISK: 2, + CLEAR_DISK: 3, + } +]; + +const options = [ + [ + "CONTINUE", + "SAVE STATE", + "LOAD STATE", + "DISK OPTIONS", + "LOAD CART", + // "OPTIONS", + "RESET CART", + ], + [ + "BACK", + "EXPORT DISK", + "IMPORT DISK", + "CLEAR DISK", + ] +]; + +@customElement("wasm4-menu-overlay") +export class MenuOverlay extends LitElement { + static styles = css` + :host { + width: 100vmin; + height: 100vmin; + position: absolute; + + color: #a0a0a0; + font: 16px wasm4-font; + + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + + background: rgba(0, 0, 0, 0.85); + } + + .menu { + border: 2px solid #f0f0f0; + padding: 0 1em 0 1em; + line-height: 2em; + } + + .ping-you { + color: #f0f0f0; + } + + .ping-good { + color: green; + } + + .ping-ok { + color: yellow; + } + + .ping-bad { + color: red; + } + + ul { + list-style: none; + padding-left: 0; + padding-right: 1em; + } + + li::before { + content: "\\00a0\\00a0"; + } + li.selected::before { + content: "> "; + } + li.selected { + color: #fff; + } + `; + + app!: App; + + private lastGamepad = 0; + + @state() private selectedIdx = 0; + + private optionContext: number = 0; + + private optionContextHistory: {context: number, index: number}[] = []; + + constructor () { + super(); + } + + get optionIndex (): any { + return optionIndex[this.optionContext]; + } + + get options (): string[] { + return options[this.optionContext]; + } + + previousContext () { + if(this.optionContextHistory.length > 0) { + const previousContext = this.optionContextHistory.pop() as {context: number, index: number}; + + this.resetInput(); + this.optionContext = previousContext.context; + this.selectedIdx = previousContext.index; + } + } + + switchContext (context: number, index: number = 0) { + this.optionContextHistory.push({ + context: this.optionContext, + index: this.selectedIdx + }); + + this.resetInput(); + this.optionContext = context; + this.selectedIdx = index; + } + + resetInput () { + this.app.controls = 0; + } + + applyInput () { + const controls = this.app.controls; + const pressedThisFrame = controls & (controls ^ this.lastGamepad); + this.lastGamepad = controls; + + if (pressedThisFrame & (constants.CONTROLS_SELECT)) { + if(this.optionContext === optionContext.DEFAULT) { + switch (this.selectedIdx) { + case this.optionIndex.CONTINUE: + this.app.closeMenu(); + break; + case this.optionIndex.SAVE_STATE: + this.app.saveGameState(); + this.app.closeMenu(); + break; + case this.optionIndex.LOAD_STATE: + this.app.loadGameState(); + this.app.closeMenu(); + break; + case this.optionIndex.DISK_OPTIONS: + this.switchContext(optionContext.DISK); + break; + case this.optionIndex.LOAD_CART: + this.app.importCart(); + this.app.closeMenu(); + break; + case this.optionIndex.RESET_CART: + this.app.resetCart(); + this.app.closeMenu(); + break; + } + } + else if(this.optionContext === optionContext.DISK) { + switch (this.selectedIdx) { + case this.optionIndex.BACK: + this.previousContext(); + break; + case this.optionIndex.EXPORT_DISK: + this.app.exportGameDisk(); + this.app.closeMenu(); + break; + case this.optionIndex.IMPORT_DISK: + this.resetInput(); + this.app.importGameDisk(); + break; + case this.optionIndex.CLEAR_DISK: + this.app.clearGameDisk(); + this.app.closeMenu(); + break; + } + } + } + + if (pressedThisFrame & constants.CONTROLS_DOWN) { + this.selectedIdx++; + } + if (pressedThisFrame & constants.CONTROLS_UP) { + this.selectedIdx--; + } + this.selectedIdx = (this.selectedIdx + this.options.length) % this.options.length; + } + + render () { + return html` + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "wasm4-menu-overlay": MenuOverlay; + } +} diff --git a/simulator/src/ui/notifications.ts b/simulator/src/ui/notifications.ts new file mode 100644 index 0000000..243d7bd --- /dev/null +++ b/simulator/src/ui/notifications.ts @@ -0,0 +1,67 @@ +import { LitElement, html, css } from "lit"; +import { customElement, state } from 'lit/decorators.js'; + +@customElement("wasm4-notifications") +export class Notifications extends LitElement { + static styles = css` + :host { + width: 100vmin; + height: 100vmin; + position: absolute; + pointer-events: none; + + color: #fff; + font: 24px wasm4-font; + + display: flex; + flex-direction: column; + } + + .notification { + background: rgba(0, 0, 0, 0.85); + padding: 0.5em; + /* animation: appear 0.5s ease-out, disappear 0.5s 4.5s ease-in; */ + /* animation-fill-mode: forwards; */ + animation: appear 0.5s ease-out; + } + + @keyframes appear { + from { + padding-left: 2em; + opacity: 0; + } + to { + opacity: 1; + padding-left: 0.5em; + } + } + + /*@keyframes disappear { + from { + opacity: 1; + } + to { + opacity: 0; + } + }*/ + `; + + @state() private notifications: string[] = []; + + show (text: string) { + this.notifications = this.notifications.concat([text]); + setTimeout(() => { + this.notifications = this.notifications.slice(1); + }, 5000); + } + + render () { + return this.notifications.map(text => html`
${text}
`); + } +} + +declare global { + interface HTMLElementTagNameMap { + "wasm4-notifications": Notifications; + } +} diff --git a/simulator/src/ui/utils.ts b/simulator/src/ui/utils.ts new file mode 100644 index 0000000..87596a2 --- /dev/null +++ b/simulator/src/ui/utils.ts @@ -0,0 +1,56 @@ +export function getUrlParam (name: string): string | null { + const url = new URL(location.href); + + // First try the URL query string + const value = url.searchParams.get(name); + if (value != null) { + return value; + } + + // Fallback to using the value in the hash + const hash = new URL(url.hash.substring(1), "https://x"); + return hash.searchParams.get(name); +} + +export function requestFullscreen () { + if (document.fullscreenElement == null) { + function expandIframe () { + // Fullscreen failed, try to maximize our own iframe. We don't yet have a button to go + // back to minimized, but this at least makes games on wasm4.org playable on iPhone + const iframe = window.frameElement as HTMLElement | null; + if (iframe) { + iframe.style.position = "fixed"; + iframe.style.top = "0"; + iframe.style.left = "0"; + iframe.style.zIndex = "99999"; + iframe.style.width = "100%"; + iframe.style.height = "100%"; + } + } + + const promise = document.body.requestFullscreen && document.body.requestFullscreen({navigationUI: "hide"}); + if (promise) { + promise.catch(expandIframe); + } else { + expandIframe(); + } + } +} + +/** + * @param red `0-31` + * @param green `0-63` + * @param blue `0-31` + * @returns RGB565 representation + */ +export function pack565(red: number, green: number, blue: number): number { + return blue | (green << 5) | (red << 11); +} + +export function unpack565(bgr565: number): [number, number, number] { + return [bgr565 >> 11, bgr565 >> 5 & 0b111111, bgr565 & 0b11111]; +} + +export function unpack888(bgr888: number): [number, number, number] { + return [bgr888 >> 16, bgr888 >> 8 & 0b11111111, bgr888 & 0b11111111]; +} diff --git a/simulator/src/ui/virtual-gamepad.ts b/simulator/src/ui/virtual-gamepad.ts new file mode 100644 index 0000000..96be892 --- /dev/null +++ b/simulator/src/ui/virtual-gamepad.ts @@ -0,0 +1,246 @@ +import { LitElement, html, css } from "lit"; +import { customElement, property, query } from 'lit/decorators.js'; + +import * as constants from "../constants"; +import * as utils from "./utils"; +import { App } from "./app"; + +function setClass (element: Element | null, className: string, enabled: boolean | number) { + if(!element) { + return; + } + if (enabled) { + element.classList.add(className); + } else { + element.classList.remove(className); + } +} + +function requestFullscreen () { + if (document.fullscreenElement == null) { + document.body.requestFullscreen({navigationUI: "hide"}); + } +} + +@customElement("wasm4-virtual-gamepad") +export class VirtualGamepad extends LitElement { + static styles = css` + :host { + display: none; + } + @media (hover: none) or (pointer: coarse) { + :host { + display: inherit; + } + } + + .dpad { + pointer-events: none; + position: absolute; + width: 39px; + height: 120px; + left: 69px; + bottom: 30px; + background: #444; + border-radius: 9px; + } + .dpad:before { + position: absolute; + width: 120px; + height: 39px; + top: 39px; + left: -39px; + background: #444; + border-radius: 9px; + content: ""; + } + .dpad:after { + position: absolute; + height: 39px; + width: 39px; + top: 39px; + border-radius: 100%; + background: #333; + content: ""; + } + .dpad.pressed-left:before { + border-left: 4px solid #A93671; + width: 116px; + } + .dpad.pressed-right:before { + border-right: 4px solid #A93671; + width: 116px; + } + .dpad.pressed-up { + border-top: 4px solid #A93671; + } + .dpad.pressed-down { + border-bottom: 4px solid #A93671; + height: 116px; + } + + .action1 { + right: 80px; + bottom: 30px; + } + .action2 { + right: 30px; + bottom: 90px; + } + .action1, .action2 { + pointer-events: none; + position: absolute; + width: 60px; + height: 60px; + border: 4px solid #A93671; + border-radius: 50px; + + /** TODO(2022-03-14): Button text should be centered but is off slightly. */ + color: #A93671; + font: 24px wasm4-font; + text-align: center; + line-height: 60px; + } + .action1.pressed, .action2.pressed { + background: #A93671; + } + + .menu { + position: absolute; + background: #444; + width: 60px; + height: 20px; + bottom: 200px; + right: 35px; + border-radius: 10px; + } + `; + + app!: App; + + @query(".dpad") dpad!: HTMLElement; + @query(".action1") action1!: HTMLElement; + @query(".action2") action2!: HTMLElement; + + readonly touchEvents = new Map(); + + readonly onPointerEvent = (event: PointerEvent) => { + if (event.pointerType != "touch") { + return; + } + event.preventDefault(); + + switch (event.type) { + case "pointerdown": case "pointermove": + this.touchEvents.set(event.pointerId, event); + break; + default: + this.touchEvents.delete(event.pointerId); + break; + } + + let buttons = 0; + if (this.touchEvents.size) { + const DPAD_MAX_DISTANCE = 100; + const DPAD_DEAD_ZONE = 10; + const BUTTON_MAX_DISTANCE = 50; + const DPAD_ACTIVE_ZONE = 3 / 5; // cos of active angle, greater that cos 60 (1/2) + + const dpadBounds = this.dpad!.getBoundingClientRect(); + const dpadX = dpadBounds.x + dpadBounds.width/2; + const dpadY = dpadBounds.y + dpadBounds.height/2; + + const action1Bounds = this.action1!.getBoundingClientRect(); + const action1X = action1Bounds.x + action1Bounds.width/2; + const action1Y = action1Bounds.y + action1Bounds.height/2; + + const action2Bounds = this.action2!.getBoundingClientRect(); + const action2X = action2Bounds.x + action2Bounds.width/2; + const action2Y = action2Bounds.y + action2Bounds.height/2; + + let x, y, dist, cosX, cosY; + for (const touch of this.touchEvents.values()) { + x = touch.clientX - dpadX; + y = touch.clientY - dpadY; + dist = Math.sqrt( x*x + y * y ); + + if (dist < DPAD_MAX_DISTANCE && dist > DPAD_DEAD_ZONE) { + cosX = x / dist; + cosY = y / dist; + + if (-cosX > DPAD_ACTIVE_ZONE) { + buttons |= constants.BUTTON_LEFT; + } else if (cosX > DPAD_ACTIVE_ZONE) { + buttons |= constants.BUTTON_RIGHT; + } + if (-cosY > DPAD_ACTIVE_ZONE) { + buttons |= constants.BUTTON_UP; + } else if (cosY > DPAD_ACTIVE_ZONE) { + buttons |= constants.BUTTON_DOWN; + } + } + + x = touch.clientX - action1X; + y = touch.clientY - action1Y; + if (x*x + y*y < BUTTON_MAX_DISTANCE*BUTTON_MAX_DISTANCE) { + buttons |= constants.BUTTON_X; + } + + x = touch.clientX - action2X; + y = touch.clientY - action2Y; + if (x*x + y*y < BUTTON_MAX_DISTANCE*BUTTON_MAX_DISTANCE) { + buttons |= constants.BUTTON_Z; + } + } + } + + setClass(this.action1, "pressed", buttons & constants.BUTTON_X); + setClass(this.action2, "pressed", buttons & constants.BUTTON_Z); + setClass(this.dpad, "pressed-left", buttons & constants.BUTTON_LEFT); + setClass(this.dpad, "pressed-right", buttons & constants.BUTTON_RIGHT); + setClass(this.dpad, "pressed-up", buttons & constants.BUTTON_UP); + setClass(this.dpad, "pressed-down", buttons & constants.BUTTON_DOWN); + + this.app.inputState.gamepad[0] = buttons; + } + + connectedCallback () { + super.connectedCallback(); + + window.addEventListener("pointercancel", this.onPointerEvent); + window.addEventListener("pointerdown", this.onPointerEvent); + window.addEventListener("pointermove", this.onPointerEvent); + window.addEventListener("pointerup", this.onPointerEvent); + } + + disconnectedCallback () { + window.removeEventListener("pointercancel", this.onPointerEvent); + window.removeEventListener("pointerdown", this.onPointerEvent); + window.removeEventListener("pointermove", this.onPointerEvent); + window.removeEventListener("pointerup", this.onPointerEvent); + + super.disconnectedCallback(); + } + + onMenuButtonPressed (event: Event) { + this.app.onMenuButtonPressed(); + + // Prevent the window handler from clearing our menu close button press + event.stopImmediatePropagation(); + } + + render () { + return html` + +
+
A
+
B
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "wasm4-virtual-gamepad": VirtualGamepad; + } +} diff --git a/simulator/src/webgl-constants.ts b/simulator/src/webgl-constants.ts new file mode 100644 index 0000000..d23b2fa --- /dev/null +++ b/simulator/src/webgl-constants.ts @@ -0,0 +1,3783 @@ +/** + * The following defined constants and descriptions are directly ported from https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Constants + * + * Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ + * + * Contributors + * + * See: https://developer.mozilla.org/en-US/profiles/Sheppy + * See: https://developer.mozilla.org/en-US/profiles/fscholz + * See: https://developer.mozilla.org/en-US/profiles/AtiX + * See: https://developer.mozilla.org/en-US/profiles/Sebastianz + * + * These constants are defined on the WebGLRenderingContext / WebGL2RenderingContext interface + */ + +// Clearing buffers +// Constants passed to WebGLRenderingContext.clear() to clear buffer masks + +/** + * Passed to clear to clear the current depth buffer + * @constant {number} + */ +export const DEPTH_BUFFER_BIT = 0x00000100; + +/** + * Passed to clear to clear the current stencil buffer + * @constant {number} + */ +export const STENCIL_BUFFER_BIT = 0x00000400; + +/** + * Passed to clear to clear the current color buffer + * @constant {number} + */ +export const COLOR_BUFFER_BIT = 0x00004000; + +// Rendering primitives +// Constants passed to WebGLRenderingContext.drawElements() or WebGLRenderingContext.drawArrays() to specify what kind of primitive to render + +/** + * Passed to drawElements or drawArrays to draw single points + * @constant {number} + */ +export const POINTS = 0x0000; + +/** + * Passed to drawElements or drawArrays to draw lines. Each vertex connects to the one after it + * @constant {number} + */ +export const LINES = 0x0001; + +/** + * Passed to drawElements or drawArrays to draw lines. Each set of two vertices is treated as a separate line segment + * @constant {number} + */ +export const LINE_LOOP = 0x0002; + +/** + * Passed to drawElements or drawArrays to draw a connected group of line segments from the first vertex to the last + * @constant {number} + */ +export const LINE_STRIP = 0x0003; + +/** + * Passed to drawElements or drawArrays to draw triangles. Each set of three vertices creates a separate triangle + * @constant {number} + */ +export const TRIANGLES = 0x0004; + +/** + * Passed to drawElements or drawArrays to draw a connected group of triangles + * @constant {number} + */ +export const TRIANGLE_STRIP = 0x0005; + +/** + * Passed to drawElements or drawArrays to draw a connected group of triangles. Each vertex connects to the previous and the first vertex in the fan + * @constant {number} + */ +export const TRIANGLE_FAN = 0x0006; + +// Blending modes +// Constants passed to WebGLRenderingContext.blendFunc() or WebGLRenderingContext.blendFuncSeparate() to specify the blending mode (for both, RBG and alpha, or separately) + +/** + * Passed to blendFunc or blendFuncSeparate to turn off a component + * @constant {number} + */ +export const ZERO = 0; + +/** + * Passed to blendFunc or blendFuncSeparate to turn on a component + * @constant {number} + */ +export const ONE = 1; + +/** + * Passed to blendFunc or blendFuncSeparate to multiply a component by the source elements color + * @constant {number} + */ +export const SRC_COLOR = 0x0300; + +/** + * Passed to blendFunc or blendFuncSeparate to multiply a component by one minus the source elements color + * @constant {number} + */ +export const ONE_MINUS_SRC_COLOR = 0x0301; + +/** + * Passed to blendFunc or blendFuncSeparate to multiply a component by the source's alpha + * @constant {number} + */ +export const SRC_ALPHA = 0x0302; + +/** + * Passed to blendFunc or blendFuncSeparate to multiply a component by one minus the source's alpha + * @constant {number} + */ +export const ONE_MINUS_SRC_ALPHA = 0x0303; + +/** + * Passed to blendFunc or blendFuncSeparate to multiply a component by the destination's alpha + * @constant {number} + */ +export const DST_ALPHA = 0x0304; + +/** + * Passed to blendFunc or blendFuncSeparate to multiply a component by one minus the destination's alpha + * @constant {number} + */ +export const ONE_MINUS_DST_ALPHA = 0x0305; + +/** + * Passed to blendFunc or blendFuncSeparate to multiply a component by the destination's color + * @constant {number} + */ +export const DST_COLOR = 0x0306; + +/** + * Passed to blendFunc or blendFuncSeparate to multiply a component by one minus the destination's color + * @constant {number} + */ +export const ONE_MINUS_DST_COLOR = 0x0307; + +/** + * Passed to blendFunc or blendFuncSeparate to multiply a component by the minimum of source's alpha or one minus the destination's alpha + * @constant {number} + */ +export const SRC_ALPHA_SATURATE = 0x0308; + +/** + * Passed to blendFunc or blendFuncSeparate to specify a constant color blend function + * @constant {number} + */ +export const CONSTANT_COLOR = 0x8001; + +/** + * Passed to blendFunc or blendFuncSeparate to specify one minus a constant color blend function + * @constant {number} + */ +export const ONE_MINUS_CONSTANT_COLOR = 0x8002; + +/** + * Passed to blendFunc or blendFuncSeparate to specify a constant alpha blend function + * @constant {number} + */ +export const CONSTANT_ALPHA = 0x8003; + +/** + * Passed to blendFunc or blendFuncSeparate to specify one minus a constant alpha blend function + * @constant {number} + */ +export const ONE_MINUS_CONSTANT_ALPHA = 0x8004; + +// Blending equations +// Constants passed to WebGLRenderingContext.blendEquation() or WebGLRenderingContext.blendEquationSeparate() to control how the blending is calculated (for both, RBG and alpha, or separately) + +/** + * Passed to blendEquation or blendEquationSeparate to set an addition blend function + * @constant {number} + */ +export const FUNC_ADD = 0x8006; + +/** + * Passed to blendEquation or blendEquationSeparate to specify a subtraction blend function (source - destination) + * @constant {number} + */ +export const FUNC_SUBSTRACT = 0x800a; + +/** + * Passed to blendEquation or blendEquationSeparate to specify a reverse subtraction blend function (destination - source) + * @constant {number} + */ +export const FUNC_REVERSE_SUBTRACT = 0x800b; + +// Getting GL parameter information +// Constants passed to WebGLRenderingContext.getParameter() to specify what information to return + +/** + * Passed to getParameter to get the current RGB blend function + * @constant {number} + */ +export const BLEND_EQUATION = 0x8009; + +/** + * Passed to getParameter to get the current RGB blend function. Same as BLEND_EQUATION + * @constant {number} + */ +export const BLEND_EQUATION_RGB = 0x8009; + +/** + * Passed to getParameter to get the current alpha blend function. Same as BLEND_EQUATION + * @constant {number} + */ +export const BLEND_EQUATION_ALPHA = 0x883d; + +/** + * Passed to getParameter to get the current destination RGB blend function + * @constant {number} + */ +export const BLEND_DST_RGB = 0x80c8; + +/** + * Passed to getParameter to get the current source RGB blend function + * @constant {number} + */ +export const BLEND_SRC_RGB = 0x80c9; + +/** + * Passed to getParameter to get the current destination alpha blend function + * @constant {number} + */ +export const BLEND_DST_ALPHA = 0x80ca; + +/** + * Passed to getParameter to get the current source alpha blend function + * @constant {number} + */ +export const BLEND_SRC_ALPHA = 0x80cb; + +/** + * Passed to getParameter to return a the current blend color + * @constant {number} + */ +export const BLEND_COLOR = 0x8005; + +/** + * Passed to getParameter to get the array buffer binding + * @constant {number} + */ +export const ARRAY_BUFFER_BINDING = 0x8894; + +/** + * Passed to getParameter to get the current element array buffer + * @constant {number} + */ +export const ELEMENT_ARRAY_BUFFER_BINDING = 0x8895; + +/** + * Passed to getParameter to get the current lineWidth (set by the lineWidth method) + * @constant {number} + */ +export const LINE_WIDTH = 0x0b21; + +/** + * Passed to getParameter to get the current size of a point drawn with gl.POINTS + * @constant {number} + */ +export const ALIASED_POINT_SIZE_RANGE = 0x846d; + +/** + * Passed to getParameter to get the range of available widths for a line. Returns a length-2 array with the lo value at 0, and hight at 1 + * @constant {number} + */ +export const ALIASED_LINE_WIDTH_RANGE = 0x846e; + +/** + * Passed to getParameter to get the current value of cullFace. Should return FRONT, BACK, or FRONT_AND_BACK + * @constant {number} + */ +export const CULL_FACE_MODE = 0x0b45; + +/** + * Passed to getParameter to determine the current value of frontFace. Should return CW or CCW + * @constant {number} + */ +export const FRONT_FACE = 0x0b46; + +/** + * Passed to getParameter to return a length-2 array of floats giving the current depth range + * @constant {number} + */ +export const DEPTH_RANGE = 0x0b70; + +/** + * Passed to getParameter to determine if the depth write mask is enabled + * @constant {number} + */ +export const DEPTH_WRITEMASK = 0x0b72; + +/** + * Passed to getParameter to determine the current depth clear value + * @constant {number} + */ +export const DEPTH_CLEAR_VALUE = 0x0b73; + +/** + * Passed to getParameter to get the current depth function. Returns NEVER, ALWAYS, LESS, EQUAL, LEQUAL, GREATER, GEQUAL, or NOTEQUAL + * @constant {number} + */ +export const DEPTH_FUNC = 0x0b74; + +/** + * Passed to getParameter to get the value the stencil will be cleared to + * @constant {number} + */ +export const STENCIL_CLEAR_VALUE = 0x0b91; + +/** + * Passed to getParameter to get the current stencil function. Returns NEVER, ALWAYS, LESS, EQUAL, LEQUAL, GREATER, GEQUAL, or NOTEQUAL + * @constant {number} + */ +export const STENCIL_FUNC = 0x0b92; + +/** + * Passed to getParameter to get the current stencil fail function. Should return KEEP, REPLACE, INCR, DECR, INVERT, INCR_WRAP, or DECR_WRAP + * @constant {number} + */ +export const STENCIL_FAIL = 0x0b94; + +/** + * Passed to getParameter to get the current stencil fail function should the depth buffer test fail. Should return KEEP, REPLACE, INCR, DECR, INVERT, INCR_WRAP, or DECR_WRAP + * @constant {number} + */ +export const STENCIL_PASS_DEPTH_FAIL = 0x0b95; + +/** + * Passed to getParameter to get the current stencil fail function should the depth buffer test pass. Should return KEEP, REPLACE, INCR, DECR, INVERT, INCR_WRAP, or DECR_WRAP + * @constant {number} + */ +export const STENCIL_PASS_DEPTH_PASS = 0x0b96; + +/** + * Passed to getParameter to get the reference value used for stencil tests + * @constant {number} + */ +export const STENCIL_REF = 0x0b97; + +/** + * @constant {number} + */ +export const STENCIL_VALUE_MASK = 0x0b93; + +/** + * @constant {number} + */ +export const STENCIL_WRITEMASK = 0x0b98; + +/** + * @constant {number} + */ +export const STENCIL_BACK_FUNC = 0x8800; + +/** + * @constant {number} + */ +export const STENCIL_BACK_FAIL = 0x8801; + +/** + * @constant {number} + */ +export const STENCIL_BACK_PASS_DEPTH_FAIL = 0x8802; + +/** + * @constant {number} + */ +export const STENCIL_BACK_PASS_DEPTH_PASS = 0x8803; + +/** + * @constant {number} + */ +export const STENCIL_BACK_REF = 0x8ca3; + +/** + * @constant {number} + */ +export const STENCIL_BACK_VALUE_MASK = 0x8ca4; + +/** + * @constant {number} + */ +export const STENCIL_BACK_WRITEMASK = 0x8ca5; + +/** + * Returns an Int32Array with four elements for the current viewport dimensions + * @constant {number} + */ +export const VIEWPORT = 0x0ba2; + +/** + * Returns an Int32Array with four elements for the current scissor box dimensions + * @constant {number} + */ +export const SCISSOR_BOX = 0x0c10; + +/** + * @constant {number} + */ +export const COLOR_CLEAR_VALUE = 0x0c22; + +/** + * @constant {number} + */ +export const COLOR_WRITEMASK = 0x0c23; + +/** + * @constant {number} + */ +export const UNPACK_ALIGNMENT = 0x0cf5; + +/** + * @constant {number} + */ +export const PACK_ALIGNMENT = 0x0d05; + +/** + * @constant {number} + */ +export const MAX_TEXTURE_SIZE = 0x0d33; + +/** + * @constant {number} + */ +export const MAX_VIEWPORT_DIMS = 0x0d3a; + +/** + * @constant {number} + */ +export const SUBPIXEL_BITS = 0x0d50; + +/** + * @constant {number} + */ +export const RED_BITS = 0x0d52; + +/** + * @constant {number} + */ +export const GREEN_BITS = 0x0d53; + +/** + * @constant {number} + */ +export const BLUE_BITS = 0x0d54; + +/** + * @constant {number} + */ +export const ALPHA_BITS = 0x0d55; + +/** + * @constant {number} + */ +export const DEPTH_BITS = 0x0d56; + +/** + * @constant {number} + */ +export const STENCIL_BITS = 0x0d57; + +/** + * @constant {number} + */ +export const POLYGON_OFFSET_UNITS = 0x2a00; + +/** + * @constant {number} + */ +export const POLYGON_OFFSET_FACTOR = 0x8038; + +/** + * @constant {number} + */ +export const TEXTURE_BINDING_2D = 0x8069; + +/** + * @constant {number} + */ +export const SAMPLE_BUFFERS = 0x80a8; + +/** + * @constant {number} + */ +export const SAMPLES = 0x80a9; + +/** + * @constant {number} + */ +export const SAMPLE_COVERAGE_VALUE = 0x80aa; + +/** + * @constant {number} + */ +export const SAMPLE_COVERAGE_INVERT = 0x80ab; + +/** + * @constant {number} + */ +export const COMPRESSED_TEXTURE_FORMATS = 0x86a3; + +/** + * @constant {number} + */ +export const VENDOR = 0x1f00; + +/** + * @constant {number} + */ +export const RENDERER = 0x1f01; + +/** + * @constant {number} + */ +export const VERSION = 0x1f02; + +/** + * @constant {number} + */ +export const IMPLEMENTATION_COLOR_READ_TYPE = 0x8b9a; + +/** + * @constant {number} + */ +export const IMPLEMENTATION_COLOR_READ_FORMAT = 0x8b9b; + +/** + * @constant {number} + */ +export const BROWSER_DEFAULT_WEBGL = 0x9244; + +// Buffers +// Constants passed to WebGLRenderingContext.bufferData(), WebGLRenderingContext.bufferSubData(), WebGLRenderingContext.bindBuffer(), or WebGLRenderingContext.getBufferParameter() + +/** + * Passed to bufferData as a hint about whether the contents of the buffer are likely to be used often and not change often + * @constant {number} + */ +export const STATIC_DRAW = 0x88e4; + +/** + * Passed to bufferData as a hint about whether the contents of the buffer are likely to not be used often + * @constant {number} + */ +export const STREAM_DRAW = 0x88e0; + +/** + * Passed to bufferData as a hint about whether the contents of the buffer are likely to be used often and change often + * @constant {number} + */ +export const DYNAMIC_DRAW = 0x88e8; + +/** + * Passed to bindBuffer or bufferData to specify the type of buffer being used + * @constant {number} + */ +export const ARRAY_BUFFER = 0x8892; + +/** + * Passed to bindBuffer or bufferData to specify the type of buffer being used + * @constant {number} + */ +export const ELEMENT_ARRAY_BUFFER = 0x8893; + +/** + * Passed to getBufferParameter to get a buffer's size + * @constant {number} + */ +export const BUFFER_SIZE = 0x8764; + +/** + * Passed to getBufferParameter to get the hint for the buffer passed in when it was created + * @constant {number} + */ +export const BUFFER_USAGE = 0x8765; + +// Vertex attributes +// Constants passed to WebGLRenderingContext.getVertexAttrib() + +/** + * Passed to getVertexAttrib to read back the current vertex attribute + * @constant {number} + */ +export const CURRENT_VERTEX_ATTRIB = 0x8626; + +/** + * @constant {number} + */ +export const VERTEX_ATTRIB_ARRAY_ENABLED = 0x8622; + +/** + * @constant {number} + */ +export const VERTEX_ATTRIB_ARRAY_SIZE = 0x8623; + +/** + * @constant {number} + */ +export const VERTEX_ATTRIB_ARRAY_STRIDE = 0x8624; + +/** + * @constant {number} + */ +export const VERTEX_ATTRIB_ARRAY_TYPE = 0x8625; + +/** + * @constant {number} + */ +export const VERTEX_ATTRIB_ARRAY_NORMALIZED = 0x886a; + +/** + * @constant {number} + */ +export const VERTEX_ATTRIB_ARRAY_POINTER = 0x8645; + +/** + * @constant {number} + */ +export const VERTEX_ATTRIB_ARRAY_BUFFER_BINDING = 0x889f; + +// Culling +// Constants passed to WebGLRenderingContext.cullFace() + +/** + * Passed to enable/disable to turn on/off culling. Can also be used with getParameter to find the current culling method + * @constant {number} + */ +export const CULL_FACE = 0x0b44; + +/** + * Passed to cullFace to specify that only front faces should be culled + * @constant {number} + */ +export const FRONT = 0x0404; + +/** + * Passed to cullFace to specify that only back faces should be culled + * @constant {number} + */ +export const BACK = 0x0405; + +/** + * Passed to cullFace to specify that front and back faces should be culled + * @constant {number} + */ +export const FRONT_AND_BACK = 0x0408; + +// Enabling and disabling +// Constants passed to WebGLRenderingContext.enable() or WebGLRenderingContext.disable() + +/** + * Passed to enable/disable to turn on/off blending. Can also be used with getParameter to find the current blending method + * @constant {number} + */ +export const BLEND = 0x0be2; + +/** + * Passed to enable/disable to turn on/off the depth test. Can also be used with getParameter to query the depth test + * @constant {number} + */ +export const DEPTH_TEST = 0x0b71; + +/** + * Passed to enable/disable to turn on/off dithering. Can also be used with getParameter to find the current dithering method + * @constant {number} + */ +export const DITHER = 0x0bd0; + +/** + * Passed to enable/disable to turn on/off the polygon offset. Useful for rendering hidden-line images, decals, and or solids with highlighted edges. Can also be used with getParameter to query the scissor test + * @constant {number} + */ +export const POLYGON_OFFSET_FILL = 0x8037; + +/** + * Passed to enable/disable to turn on/off the alpha to coverage. Used in multi-sampling alpha channels + * @constant {number} + */ +export const SAMPLE_ALPHA_TO_COVERAGE = 0x809e; + +/** + * Passed to enable/disable to turn on/off the sample coverage. Used in multi-sampling + * @constant {number} + */ +export const SAMPLE_COVERAGE = 0x80a0; + +/** + * Passed to enable/disable to turn on/off the scissor test. Can also be used with getParameter to query the scissor test + * @constant {number} + */ +export const SCISSOR_TEST = 0x0c11; + +/** + * Passed to enable/disable to turn on/off the stencil test. Can also be used with getParameter to query the stencil test + * @constant {number} + */ +export const STENCIL_TEST = 0x0b90; + +// Errors +// Constants returned from WebGLRenderingContext.getError() + +/** + * Returned from getError + * @constant {number} + */ +export const NO_ERROR = 0; + +/** + * Returned from getError + * @constant {number} + */ +export const INVALID_ENUM = 0x0500; + +/** + * Returned from getError + * @constant {number} + */ +export const INVALID_VALUE = 0x0501; + +/** + * Returned from getError + * @constant {number} + */ +export const INVALID_OPERATION = 0x0502; + +/** + * Returned from getError + * @constant {number} + */ +export const OUT_OF_MEMORY = 0x0505; + +/** + * Returned from getError + * @constant {number} + */ +export const CONTEXT_LOST_WEBGL = 0x9242; + +// Front face directions +// Constants passed to WebGLRenderingContext.frontFace() + +/** + * Passed to frontFace to specify the front face of a polygon is drawn in the clockwise direction, + * @constant {number} + */ +export const CW = 0x0900; + +/** + * Passed to frontFace to specify the front face of a polygon is drawn in the counter clockwise direction + * @constant {number} + */ +export const CCW = 0x0901; + +// Hints +// Constants passed to WebGLRenderingContext.hint() + +/** + * There is no preference for this behavior + * @constant {number} + */ +export const DONT_CARE = 0x1100; + +/** + * The most efficient behavior should be used + * @constant {number} + */ +export const FASTEST = 0x1101; + +/** + * The most correct or the highest quality option should be used + * @constant {number} + */ +export const NICEST = 0x1102; + +/** + * Hint for the quality of filtering when generating mipmap images with WebGLRenderingContext.generateMipmap() + * @constant {number} + */ +export const GENERATE_MIPMAP_HINT = 0x8192; + +// Data types + +/** + * @constant {number} + */ +export const BYTE = 0x1400; + +/** + * @constant {number} + */ +export const UNSIGNED_BYTE = 0x1401; + +/** + * @constant {number} + */ +export const SHORT = 0x1402; + +/** + * @constant {number} + */ +export const UNSIGNED_SHORT = 0x1403; + +/** + * @constant {number} + */ +export const INT = 0x1404; + +/** + * @constant {number} + */ +export const UNSIGNED_INT = 0x1405; + +/** + * @constant {number} + */ +export const FLOAT = 0x1406; + +// Pixel formats + +/** + * @constant {number} + */ +export const DEPTH_COMPONENT = 0x1902; + +/** + * @constant {number} + */ +export const ALPHA = 0x1906; + +/** + * @constant {number} + */ +export const RGB = 0x1907; + +/** + * @constant {number} + */ +export const RGBA = 0x1908; + +/** + * @constant {number} + */ +export const LUMINANCE = 0x1909; + +/** + * @constant {number} + */ +export const LUMINANCE_ALPHA = 0x190a; + +// Pixel types + +/** + * @constant {number} + */ +export const UNSIGNED_SHORT_4_4_4_4 = 0x8033; + +/** + * @constant {number} + */ +export const UNSIGNED_SHORT_5_5_5_1 = 0x8034; + +/** + * @constant {number} + */ +export const UNSIGNED_SHORT_5_6_5 = 0x8363; + +// Shaders +// Constants passed to WebGLRenderingContext.getShaderParameter() + +/** + * Passed to createShader to define a fragment shader + * @constant {number} + */ +export const FRAGMENT_SHADER = 0x8b30; + +/** + * Passed to createShader to define a vertex shader + * @constant {number} + */ +export const VERTEX_SHADER = 0x8b31; + +/** + * Passed to getShaderParamter to get the status of the compilation. Returns false if the shader was not compiled. You can then query getShaderInfoLog to find the exact error + * @constant {number} + */ +export const COMPILE_STATUS = 0x8b81; + +/** + * Passed to getShaderParamter to determine if a shader was deleted via deleteShader. Returns true if it was, false otherwise + * @constant {number} + */ +export const DELETE_STATUS = 0x8b80; + +/** + * Passed to getProgramParameter after calling linkProgram to determine if a program was linked correctly. Returns false if there were errors. Use getProgramInfoLog to find the exact error + * @constant {number} + */ +export const LINK_STATUS = 0x8b82; + +/** + * Passed to getProgramParameter after calling validateProgram to determine if it is valid. Returns false if errors were found + * @constant {number} + */ +export const VALIDATE_STATUS = 0x8b83; + +/** + * Passed to getProgramParameter after calling attachShader to determine if the shader was attached correctly. Returns false if errors occurred + * @constant {number} + */ +export const ATTACHED_SHADERS = 0x8b85; + +/** + * Passed to getProgramParameter to get the number of attributes active in a program + * @constant {number} + */ +export const ACTIVE_ATTRIBUTES = 0x8b89; + +/** + * Passed to getProgramParamter to get the number of uniforms active in a program + * @constant {number} + */ +export const ACTIVE_UNIFORMS = 0x8b86; + +/** + * The maximum number of entries possible in the vertex attribute list + * @constant {number} + */ +export const MAX_VERTEX_ATTRIBS = 0x8869; + +/** + * @constant {number} + */ +export const MAX_VERTEX_UNIFORM_VECTORS = 0x8dfb; + +/** + * @constant {number} + */ +export const MAX_VARYING_VECTORS = 0x8dfc; + +/** + * @constant {number} + */ +export const MAX_COMBINED_TEXTURE_IMAGE_UNITS = 0x8b4d; + +/** + * @constant {number} + */ +export const MAX_VERTEX_TEXTURE_IMAGE_UNITS = 0x8b4c; + +/** + * Implementation dependent number of maximum texture units. At least 8 + * @constant {number} + */ +export const MAX_TEXTURE_IMAGE_UNITS = 0x8872; + +/** + * @constant {number} + */ +export const MAX_FRAGMENT_UNIFORM_VECTORS = 0x8dfd; + +/** + * @constant {number} + */ +export const SHADER_TYPE = 0x8b4f; + +/** + * @constant {number} + */ +export const SHADING_LANGUAGE_VERSION = 0x8b8c; + +/** + * @constant {number} + */ +export const CURRENT_PROGRAM = 0x8b8d; + +// Depth or stencil tests +// Constants passed to WebGLRenderingContext.stencilFunc() + +/** + * Passed to depthFunction or stencilFunction to specify depth or stencil tests will never pass. i.e. Nothing will be drawn + * @constant {number} + */ +export const NEVER = 0x0200; + +/** + * Passed to depthFunction or stencilFunction to specify depth or stencil tests will always pass. i.e. Pixels will be drawn in the order they are drawn + * @constant {number} + */ +export const ALWAYS = 0x0207; + +/** + * Passed to depthFunction or stencilFunction to specify depth or stencil tests will pass if the new depth value is less than the stored value + * @constant {number} + */ +export const LESS = 0x0201; + +/** + * Passed to depthFunction or stencilFunction to specify depth or stencil tests will pass if the new depth value is equals to the stored value + * @constant {number} + */ +export const EQUAL = 0x0202; + +/** + * Passed to depthFunction or stencilFunction to specify depth or stencil tests will pass if the new depth value is less than or equal to the stored value + * @constant {number} + */ +export const LEQUAL = 0x0203; + +/** + * Passed to depthFunction or stencilFunction to specify depth or stencil tests will pass if the new depth value is greater than the stored value + * @constant {number} + */ +export const GREATER = 0x0204; + +/** + * Passed to depthFunction or stencilFunction to specify depth or stencil tests will pass if the new depth value is greater than or equal to the stored value + * @constant {number} + */ +export const GEQUAL = 0x0206; + +/** + * Passed to depthFunction or stencilFunction to specify depth or stencil tests will pass if the new depth value is not equal to the stored value + * @constant {number} + */ +export const NOTEQUAL = 0x0205; + +// Stencil actions +// Constants passed to WebGLRenderingContext.stencilOp() + +/** + * @constant {number} + */ +export const KEEP = 0x1e00; + +/** + * @constant {number} + */ +export const REPLACE = 0x1e01; + +/** + * @constant {number} + */ +export const INCR = 0x1e02; + +/** + * @constant {number} + */ +export const DECR = 0x1e03; + +/** + * @constant {number} + */ +export const INVERT = 0x150a; + +/** + * @constant {number} + */ +export const INCR_WRAP = 0x8507; + +/** + * @constant {number} + */ +export const DECR_WRAP = 0x8508; + +// Textures +// Constants passed to WebGLRenderingContext.texParameteri(), WebGLRenderingContext.texParameterf(), WebGLRenderingContext.bindTexture(), WebGLRenderingContext.texImage2D(), and others + +/** + * @constant {number} + */ +export const NEAREST = 0x2600; + +/** + * @constant {number} + */ +export const LINEAR = 0x2601; + +/** + * @constant {number} + */ +export const NEAREST_MIPMAP_NEAREST = 0x2700; + +/** + * @constant {number} + */ +export const LINEAR_MIPMAP_NEAREST = 0x2701; + +/** + * @constant {number} + */ +export const NEAREST_MIPMAP_LINEAR = 0x2702; + +/** + * @constant {number} + */ +export const LINEAR_MIPMAP_LINEAR = 0x2703; + +/** + * @constant {number} + */ +export const TEXTURE_MAG_FILTER = 0x2800; + +/** + * @constant {number} + */ +export const TEXTURE_MIN_FILTER = 0x2801; + +/** + * @constant {number} + */ +export const TEXTURE_WRAP_S = 0x2802; + +/** + * @constant {number} + */ +export const TEXTURE_WRAP_T = 0x2803; + +/** + * @constant {number} + */ +export const TEXTURE_2D = 0x0de1; + +/** + * @constant {number} + */ +export const TEXTURE = 0x1702; + +/** + * @constant {number} + */ +export const TEXTURE_CUBE_MAP = 0x8513; + +/** + * @constant {number} + */ +export const TEXTURE_BINDING_CUBE_MAP = 0x8514; + +/** + * @constant {number} + */ +export const TEXTURE_CUBE_MAP_POSITIVE_X = 0x8515; + +/** + * @constant {number} + */ +export const TEXTURE_CUBE_MAP_NEGATIVE_X = 0x8516; + +/** + * @constant {number} + */ +export const TEXTURE_CUBE_MAP_POSITIVE_Y = 0x8517; + +/** + * @constant {number} + */ +export const TEXTURE_CUBE_MAP_NEGATIVE_Y = 0x8518; + +/** + * @constant {number} + */ +export const TEXTURE_CUBE_MAP_POSITIVE_Z = 0x8519; + +/** + * @constant {number} + */ +export const TEXTURE_CUBE_MAP_NEGATIVE_Z = 0x851a; + +/** + * @constant {number} + */ +export const MAX_CUBE_MAP_TEXTURE_SIZE = 0x851c; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE0 = 0x84c0; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE1 = 0x84c1; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE2 = 0x84c2; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE3 = 0x84c3; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE4 = 0x84c4; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE5 = 0x84c5; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE6 = 0x84c6; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE7 = 0x84c7; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE8 = 0x84c8; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE9 = 0x84c9; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE10 = 0x84ca; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE11 = 0x84cb; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE12 = 0x84cc; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE13 = 0x84cd; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE14 = 0x84ce; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE15 = 0x84cf; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE16 = 0x84d0; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE17 = 0x84d1; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE18 = 0x84d2; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE19 = 0x84d3; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE20 = 0x84d4; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE21 = 0x84d5; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE22 = 0x84d6; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE23 = 0x84d7; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE24 = 0x84d8; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE25 = 0x84d9; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE26 = 0x84da; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE27 = 0x84db; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE28 = 0x84dc; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE29 = 0x84dd; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE30 = 0x84de; + +/** + * A texture unit + * @constant {number} + */ +export const TEXTURE31 = 0x84df; + +/** + * The current active texture unit + * @constant {number} + */ +export const ACTIVE_TEXTURE = 0x84e0; + +/** + * @constant {number} + */ +export const REPEAT = 0x2901; + +/** + * @constant {number} + */ +export const CLAMP_TO_EDGE = 0x812f; + +/** + * @constant {number} + */ +export const MIRRORED_REPEAT = 0x8370; + +// Uniform types + +/** + * @constant {number} + */ +export const FLOAT_VEC2 = 0x8b50; + +/** + * @constant {number} + */ +export const FLOAT_VEC3 = 0x8b51; + +/** + * @constant {number} + */ +export const FLOAT_VEC4 = 0x8b52; + +/** + * @constant {number} + */ +export const INT_VEC2 = 0x8b53; + +/** + * @constant {number} + */ +export const INT_VEC3 = 0x8b54; + +/** + * @constant {number} + */ +export const INT_VEC4 = 0x8b55; + +/** + * @constant {number} + */ +export const BOOL = 0x8b56; + +/** + * @constant {number} + */ +export const BOOL_VEC2 = 0x8b57; + +/** + * @constant {number} + */ +export const BOOL_VEC3 = 0x8b58; + +/** + * @constant {number} + */ +export const BOOL_VEC4 = 0x8b59; + +/** + * @constant {number} + */ +export const FLOAT_MAT2 = 0x8b5a; + +/** + * @constant {number} + */ +export const FLOAT_MAT3 = 0x8b5b; + +/** + * @constant {number} + */ +export const FLOAT_MAT4 = 0x8b5c; + +/** + * @constant {number} + */ +export const SAMPLER_2D = 0x8b5e; + +/** + * @constant {number} + */ +export const SAMPLER_CUBE = 0x8b60; + +// Shader precision-specified types + +/** + * @constant {number} + */ +export const LOW_FLOAT = 0x8df0; + +/** + * @constant {number} + */ +export const MEDIUM_FLOAT = 0x8df1; + +/** + * @constant {number} + */ +export const HIGH_FLOAT = 0x8df2; + +/** + * @constant {number} + */ +export const LOW_INT = 0x8df3; + +/** + * @constant {number} + */ +export const MEDIUM_INT = 0x8df4; + +/** + * @constant {number} + */ +export const HIGH_INT = 0x8df5; + +// Framebuffers and renderbuffers + +/** + * @constant {number} + */ +export const FRAMEBUFFER = 0x8d40; + +/** + * @constant {number} + */ +export const RENDERBUFFER = 0x8d41; + +/** + * @constant {number} + */ +export const RGBA4 = 0x8056; + +/** + * @constant {number} + */ +export const RGB5_A1 = 0x8057; + +/** + * @constant {number} + */ +export const RGB565 = 0x8d62; + +/** + * @constant {number} + */ +export const DEPTH_COMPONENT16 = 0x81a5; + +/** + * @constant {number} + */ +export const STENCIL_INDEX = 0x1901; + +/** + * @constant {number} + */ +export const STENCIL_INDEX8 = 0x8d48; + +/** + * @constant {number} + */ +export const DEPTH_STENCIL = 0x84f9; + +/** + * @constant {number} + */ +export const RENDERBUFFER_WIDTH = 0x8d42; + +/** + * @constant {number} + */ +export const RENDERBUFFER_HEIGHT = 0x8d43; + +/** + * @constant {number} + */ +export const RENDERBUFFER_INTERNAL_FORMAT = 0x8d44; + +/** + * @constant {number} + */ +export const RENDERBUFFER_RED_SIZE = 0x8d50; + +/** + * @constant {number} + */ +export const RENDERBUFFER_GREEN_SIZE = 0x8d51; + +/** + * @constant {number} + */ +export const RENDERBUFFER_BLUE_SIZE = 0x8d52; + +/** + * @constant {number} + */ +export const RENDERBUFFER_ALPHA_SIZE = 0x8d53; + +/** + * @constant {number} + */ +export const RENDERBUFFER_DEPTH_SIZE = 0x8d54; + +/** + * @constant {number} + */ +export const RENDERBUFFER_STENCIL_SIZE = 0x8d55; + +/** + * @constant {number} + */ +export const FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE = 0x8cd0; + +/** + * @constant {number} + */ +export const FRAMEBUFFER_ATTACHMENT_OBJECT_NAME = 0x8cd1; + +/** + * @constant {number} + */ +export const FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL = 0x8cd2; + +/** + * @constant {number} + */ +export const FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE = 0x8cd3; + +/** + * @constant {number} + */ +export const COLOR_ATTACHMENT0 = 0x8ce0; + +/** + * @constant {number} + */ +export const DEPTH_ATTACHMENT = 0x8d00; + +/** + * @constant {number} + */ +export const STENCIL_ATTACHMENT = 0x8d20; + +/** + * @constant {number} + */ +export const DEPTH_STENCIL_ATTACHMENT = 0x821a; + +/** + * @constant {number} + */ +export const NONE = 0; + +/** + * @constant {number} + */ +export const FRAMEBUFFER_COMPLETE = 0x8cd5; + +/** + * @constant {number} + */ +export const FRAMEBUFFER_INCOMPLETE_ATTACHMENT = 0x8cd6; + +/** + * @constant {number} + */ +export const FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT = 0x8cd7; + +/** + * @constant {number} + */ +export const FRAMEBUFFER_INCOMPLETE_DIMENSIONS = 0x8cd9; + +/** + * @constant {number} + */ +export const FRAMEBUFFER_UNSUPPORTED = 0x8cdd; + +/** + * @constant {number} + */ +export const FRAMEBUFFER_BINDING = 0x8ca6; + +/** + * @constant {number} + */ +export const RENDERBUFFER_BINDING = 0x8ca7; + +/** + * @constant {number} + */ +export const MAX_RENDERBUFFER_SIZE = 0x84e8; + +/** + * @constant {number} + */ +export const INVALID_FRAMEBUFFER_OPERATION = 0x0506; + +// Pixel storage modes +// Constants passed to WebGLRenderingContext.pixelStorei() + +/** + * @constant {number} + */ +export const UNPACK_FLIP_Y_WEBGL = 0x9240; + +/** + * @constant {number} + */ +export const UNPACK_PREMULTIPLY_ALPHA_WEBGL = 0x9241; + +/** + * @constant {number} + */ +export const UNPACK_COLORSPACE_CONVERSION_WEBGL = 0x9243; + +// Additional constants defined WebGL 2 +// These constants are defined on the WebGL2RenderingContext interface. All WebGL 1 constants are also available in a WebGL 2 context + +// Getting GL parameter information +// Constants passed to WebGLRenderingContext.getParameter() to specify what information to return + +/** + * @constant {number} + */ +export const READ_BUFFER = 0x0c02; + +/** + * @constant {number} + */ +export const UNPACK_ROW_LENGTH = 0x0cf2; + +/** + * @constant {number} + */ +export const UNPACK_SKIP_ROWS = 0x0cf3; + +/** + * @constant {number} + */ +export const UNPACK_SKIP_PIXELS = 0x0cf4; + +/** + * @constant {number} + */ +export const PACK_ROW_LENGTH = 0x0d02; + +/** + * @constant {number} + */ +export const PACK_SKIP_ROWS = 0x0d03; + +/** + * @constant {number} + */ +export const PACK_SKIP_PIXELS = 0x0d04; + +/** + * @constant {number} + */ +export const TEXTURE_BINDING_3D = 0x806a; + +/** + * @constant {number} + */ +export const UNPACK_SKIP_IMAGES = 0x806d; + +/** + * @constant {number} + */ +export const UNPACK_IMAGE_HEIGHT = 0x806e; + +/** + * @constant {number} + */ +export const MAX_3D_TEXTURE_SIZE = 0x8073; + +/** + * @constant {number} + */ +export const MAX_ELEMENTS_VERTICES = 0x80e8; + +/** + * @constant {number} + */ +export const MAX_ELEMENTS_INDICES = 0x80e9; + +/** + * @constant {number} + */ +export const MAX_TEXTURE_LOD_BIAS = 0x84fd; + +/** + * @constant {number} + */ +export const MAX_FRAGMENT_UNIFORM_COMPONENTS = 0x8b49; + +/** + * @constant {number} + */ +export const MAX_VERTEX_UNIFORM_COMPONENTS = 0x8b4a; + +/** + * @constant {number} + */ +export const MAX_ARRAY_TEXTURE_LAYERS = 0x88ff; + +/** + * @constant {number} + */ +export const MIN_PROGRAM_TEXEL_OFFSET = 0x8904; + +/** + * @constant {number} + */ +export const MAX_PROGRAM_TEXEL_OFFSET = 0x8905; + +/** + * @constant {number} + */ +export const MAX_VARYING_COMPONENTS = 0x8b4b; + +/** + * @constant {number} + */ +export const FRAGMENT_SHADER_DERIVATIVE_HINT = 0x8b8b; + +/** + * @constant {number} + */ +export const RASTERIZER_DISCARD = 0x8c89; + +/** + * @constant {number} + */ +export const VERTEX_ARRAY_BINDING = 0x85b5; + +/** + * @constant {number} + */ +export const MAX_VERTEX_OUTPUT_COMPONENTS = 0x9122; + +/** + * @constant {number} + */ +export const MAX_FRAGMENT_INPUT_COMPONENTS = 0x9125; + +/** + * @constant {number} + */ +export const MAX_SERVER_WAIT_TIMEOUT = 0x9111; + +/** + * @constant {number} + */ +export const MAX_ELEMENT_INDEX = 0x8d6b; + +// Textures +// Constants passed to WebGLRenderingContext.texParameteri(), WebGLRenderingContext.texParameterf(), WebGLRenderingContext.bindTexture(), WebGLRenderingContext.texImage2D(), and others + +/** + * @constant {number} + */ +export const RED = 0x1903; + +/** + * @constant {number} + */ +export const RGB8 = 0x8051; + +/** + * @constant {number} + */ +export const RGBA8 = 0x8058; + +/** + * @constant {number} + */ +export const RGB10_A2 = 0x8059; + +/** + * @constant {number} + */ +export const TEXTURE_3D = 0x806f; + +/** + * @constant {number} + */ +export const TEXTURE_WRAP_R = 0x8072; + +/** + * @constant {number} + */ +export const TEXTURE_MIN_LOD = 0x813a; + +/** + * @constant {number} + */ +export const TEXTURE_MAX_LOD = 0x813b; + +/** + * @constant {number} + */ +export const TEXTURE_BASE_LEVEL = 0x813c; + +/** + * @constant {number} + */ +export const TEXTURE_MAX_LEVEL = 0x813d; + +/** + * @constant {number} + */ +export const TEXTURE_COMPARE_MODE = 0x884c; + +/** + * @constant {number} + */ +export const TEXTURE_COMPARE_FUNC = 0x884d; + +/** + * @constant {number} + */ +export const SRGB = 0x8c40; + +/** + * @constant {number} + */ +export const SRGB8 = 0x8c41; + +/** + * @constant {number} + */ +export const SRGB8_ALPHA8 = 0x8c43; + +/** + * @constant {number} + */ +export const COMPARE_REF_TO_TEXTURE = 0x884e; + +/** + * @constant {number} + */ +export const RGBA32F = 0x8814; + +/** + * @constant {number} + */ +export const RGB32F = 0x8815; + +/** + * @constant {number} + */ +export const RGBA16F = 0x881a; + +/** + * @constant {number} + */ +export const RGB16F = 0x881b; + +/** + * @constant {number} + */ +export const TEXTURE_2D_ARRAY = 0x8c1a; + +/** + * @constant {number} + */ +export const TEXTURE_BINDING_2D_ARRAY = 0x8c1d; + +/** + * @constant {number} + */ +export const R11F_G11F_B10F = 0x8c3a; + +/** + * @constant {number} + */ +export const RGB9_E5 = 0x8c3d; + +/** + * @constant {number} + */ +export const RGBA32UI = 0x8d70; + +/** + * @constant {number} + */ +export const RGB32UI = 0x8d71; + +/** + * @constant {number} + */ +export const RGBA16UI = 0x8d76; + +/** + * @constant {number} + */ +export const RGB16UI = 0x8d77; + +/** + * @constant {number} + */ +export const RGBA8UI = 0x8d7c; + +/** + * @constant {number} + */ +export const RGB8UI = 0x8d7d; + +/** + * @constant {number} + */ +export const RGBA32I = 0x8d82; + +/** + * @constant {number} + */ +export const RGB32I = 0x8d83; + +/** + * @constant {number} + */ +export const RGBA16I = 0x8d88; + +/** + * @constant {number} + */ +export const RGB16I = 0x8d89; + +/** + * @constant {number} + */ +export const RGBA8I = 0x8d8e; + +/** + * @constant {number} + */ +export const RGB8I = 0x8d8f; + +/** + * @constant {number} + */ +export const RED_INTEGER = 0x8d94; + +/** + * @constant {number} + */ +export const RGB_INTEGER = 0x8d98; + +/** + * @constant {number} + */ +export const RGBA_INTEGER = 0x8d99; + +/** + * @constant {number} + */ +export const R8 = 0x8229; + +/** + * @constant {number} + */ +export const RG8 = 0x822b; + +/** + * @constant {number} + */ +export const R16F = 0x822d; + +/** + * @constant {number} + */ +export const R32F = 0x822e; + +/** + * @constant {number} + */ +export const RG16F = 0x822f; + +/** + * @constant {number} + */ +export const RG32F = 0x8230; + +/** + * @constant {number} + */ +export const R8I = 0x8231; + +/** + * @constant {number} + */ +export const R8UI = 0x8232; + +/** + * @constant {number} + */ +export const R16I = 0x8233; + +/** + * @constant {number} + */ +export const R16UI = 0x8234; + +/** + * @constant {number} + */ +export const R32I = 0x8235; + +/** + * @constant {number} + */ +export const R32UI = 0x8236; + +/** + * @constant {number} + */ +export const RG8I = 0x8237; + +/** + * @constant {number} + */ +export const RG8UI = 0x8238; + +/** + * @constant {number} + */ +export const RG16I = 0x8239; + +/** + * @constant {number} + */ +export const RG16UI = 0x823a; + +/** + * @constant {number} + */ +export const RG32I = 0x823b; + +/** + * @constant {number} + */ +export const RG32UI = 0x823c; + +/** + * @constant {number} + */ +export const R8_SNORM = 0x8f94; + +/** + * @constant {number} + */ +export const RG8_SNORM = 0x8f95; + +/** + * @constant {number} + */ +export const RGB8_SNORM = 0x8f96; + +/** + * @constant {number} + */ +export const RGBA8_SNORM = 0x8f97; + +/** + * @constant {number} + */ +export const RGB10_A2UI = 0x906f; + +/** + * @constant {number} + */ +export const TEXTURE_IMMUTABLE_FORMAT = 0x912f; + +/** + * @constant {number} + */ +export const TEXTURE_IMMUTABLE_LEVELS = 0x82df; + +// Pixel types + +/** + * @constant {number} + */ +export const UNSIGNED_INT_2_10_10_10_REV = 0x8368; + +/** + * @constant {number} + */ +export const UNSIGNED_INT_10F_11F_11F_REV = 0x8c3b; + +/** + * @constant {number} + */ +export const UNSIGNED_INT_5_9_9_9_REV = 0x8c3e; + +/** + * @constant {number} + */ +export const FLOAT_32_UNSIGNED_INT_24_8_REV = 0x8dad; + +/** + * @constant {number} + */ +export const UNSIGNED_INT_24_8 = 0x84fa; + +/** + * @constant {number} + */ +export const HALF_FLOAT = 0x140b; + +/** + * @constant {number} + */ +export const RG = 0x8227; + +/** + * @constant {number} + */ +export const RG_INTEGER = 0x8228; + +/** + * @constant {number} + */ +export const INT_2_10_10_10_REV = 0x8d9f; + +// Queries + +/** + * @constant {number} + */ +export const CURRENT_QUERY = 0x8865; + +/** + * @constant {number} + */ +export const QUERY_RESULT = 0x8866; + +/** + * @constant {number} + */ +export const QUERY_RESULT_AVAILABLE = 0x8867; + +/** + * @constant {number} + */ +export const ANY_SAMPLES_PASSED = 0x8c2f; + +/** + * @constant {number} + */ +export const ANY_SAMPLES_PASSED_CONSERVATIVE = 0x8d6a; + +// Draw buffers + +/** + * @constant {number} + */ +export const MAX_DRAW_BUFFERS = 0x8824; + +/** + * @constant {number} + */ +export const DRAW_BUFFER0 = 0x8825; + +/** + * @constant {number} + */ +export const DRAW_BUFFER1 = 0x8826; + +/** + * @constant {number} + */ +export const DRAW_BUFFER2 = 0x8827; + +/** + * @constant {number} + */ +export const DRAW_BUFFER3 = 0x8828; + +/** + * @constant {number} + */ +export const DRAW_BUFFER4 = 0x8829; + +/** + * @constant {number} + */ +export const DRAW_BUFFER5 = 0x882a; + +/** + * @constant {number} + */ +export const DRAW_BUFFER6 = 0x882b; + +/** + * @constant {number} + */ +export const DRAW_BUFFER7 = 0x882c; + +/** + * @constant {number} + */ +export const DRAW_BUFFER8 = 0x882d; + +/** + * @constant {number} + */ +export const DRAW_BUFFER9 = 0x882e; + +/** + * @constant {number} + */ +export const DRAW_BUFFER10 = 0x882f; + +/** + * @constant {number} + */ +export const DRAW_BUFFER11 = 0x8830; + +/** + * @constant {number} + */ +export const DRAW_BUFFER12 = 0x8831; + +/** + * @constant {number} + */ +export const DRAW_BUFFER13 = 0x8832; + +/** + * @constant {number} + */ +export const DRAW_BUFFER14 = 0x8833; + +/** + * @constant {number} + */ +export const DRAW_BUFFER15 = 0x8834; + +/** + * @constant {number} + */ +export const MAX_COLOR_ATTACHMENTS = 0x8cdf; + +/** + * @constant {number} + */ +export const COLOR_ATTACHMENT1 = 0x8ce1; + +/** + * @constant {number} + */ +export const COLOR_ATTACHMENT2 = 0x8ce2; + +/** + * @constant {number} + */ +export const COLOR_ATTACHMENT3 = 0x8ce3; + +/** + * @constant {number} + */ +export const COLOR_ATTACHMENT4 = 0x8ce4; + +/** + * @constant {number} + */ +export const COLOR_ATTACHMENT5 = 0x8ce5; + +/** + * @constant {number} + */ +export const COLOR_ATTACHMENT6 = 0x8ce6; + +/** + * @constant {number} + */ +export const COLOR_ATTACHMENT7 = 0x8ce7; + +/** + * @constant {number} + */ +export const COLOR_ATTACHMENT8 = 0x8ce8; + +/** + * @constant {number} + */ +export const COLOR_ATTACHMENT9 = 0x8ce9; + +/** + * @constant {number} + */ +export const COLOR_ATTACHMENT10 = 0x8cea; + +/** + * @constant {number} + */ +export const COLOR_ATTACHMENT11 = 0x8ceb; + +/** + * @constant {number} + */ +export const COLOR_ATTACHMENT12 = 0x8cec; + +/** + * @constant {number} + */ +export const COLOR_ATTACHMENT13 = 0x8ced; + +/** + * @constant {number} + */ +export const COLOR_ATTACHMENT14 = 0x8cee; + +/** + * @constant {number} + */ +export const COLOR_ATTACHMENT15 = 0x8cef; + +// Samplers + +/** + * @constant {number} + */ +export const SAMPLER_3D = 0x8b5f; + +/** + * @constant {number} + */ +export const SAMPLER_2D_SHADOW = 0x8b62; + +/** + * @constant {number} + */ +export const SAMPLER_2D_ARRAY = 0x8dc1; + +/** + * @constant {number} + */ +export const SAMPLER_2D_ARRAY_SHADOW = 0x8dc4; + +/** + * @constant {number} + */ +export const SAMPLER_CUBE_SHADOW = 0x8dc5; + +/** + * @constant {number} + */ +export const INT_SAMPLER_2D = 0x8dca; + +/** + * @constant {number} + */ +export const INT_SAMPLER_3D = 0x8dcb; + +/** + * @constant {number} + */ +export const INT_SAMPLER_CUBE = 0x8dcc; + +/** + * @constant {number} + */ +export const INT_SAMPLER_2D_ARRAY = 0x8dcf; + +/** + * @constant {number} + */ +export const UNSIGNED_INT_SAMPLER_2D = 0x8dd2; + +/** + * @constant {number} + */ +export const UNSIGNED_INT_SAMPLER_3D = 0x8dd3; + +/** + * @constant {number} + */ +export const UNSIGNED_INT_SAMPLER_CUBE = 0x8dd4; + +/** + * @constant {number} + */ +export const UNSIGNED_INT_SAMPLER_2D_ARRAY = 0x8dd7; + +/** + * @constant {number} + */ +export const MAX_SAMPLES = 0x8d57; + +/** + * @constant {number} + */ +export const SAMPLER_BINDING = 0x8919; + +// Buffers + +/** + * @constant {number} + */ +export const PIXEL_PACK_BUFFER = 0x88eb; + +/** + * @constant {number} + */ +export const PIXEL_UNPACK_BUFFER = 0x88ec; + +/** + * @constant {number} + */ +export const PIXEL_PACK_BUFFER_BINDING = 0x88ed; + +/** + * @constant {number} + */ +export const PIXEL_UNPACK_BUFFER_BINDING = 0x88ef; + +/** + * @constant {number} + */ +export const COPY_READ_BUFFER = 0x8f36; + +/** + * @constant {number} + */ +export const COPY_WRITE_BUFFER = 0x8f37; + +/** + * @constant {number} + */ +export const COPY_READ_BUFFER_BINDING = 0x8f36; + +/** + * @constant {number} + */ +export const COPY_WRITE_BUFFER_BINDING = 0x8f37; + +// Data types + +/** + * @constant {number} + */ +export const FLOAT_MAT2X3 = 0x8b65; + +/** + * @constant {number} + */ +export const FLOAT_MAT2X4 = 0x8b66; + +/** + * @constant {number} + */ +export const FLOAT_MAT3X2 = 0x8b67; + +/** + * @constant {number} + */ +export const FLOAT_MAT3X4 = 0x8b68; + +/** + * @constant {number} + */ +export const FLOAT_MAT4X2 = 0x8b69; + +/** + * @constant {number} + */ +export const FLOAT_MAT4X3 = 0x8b6a; + +/** + * @constant {number} + */ +export const UNSIGNED_INT_VEC2 = 0x8dc6; + +/** + * @constant {number} + */ +export const UNSIGNED_INT_VEC3 = 0x8dc7; + +/** + * @constant {number} + */ +export const UNSIGNED_INT_VEC4 = 0x8dc8; + +/** + * @constant {number} + */ +export const UNSIGNED_NORMALIZED = 0x8c17; + +/** + * @constant {number} + */ +export const SIGNED_NORMALIZED = 0x8f9c; + +// Vertex attributes + +/** + * @constant {number} + */ +export const VERTEX_ATTRIB_ARRAY_INTEGER = 0x88fd; + +/** + * @constant {number} + */ +export const VERTEX_ATTRIB_ARRAY_DIVISOR = 0x88fe; + +// Transform feedback + +/** + * @constant {number} + */ +export const TRANSFORM_FEEDBACK_BUFFER_MODE = 0x8c7f; + +/** + * @constant {number} + */ +export const MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS = 0x8c80; + +/** + * @constant {number} + */ +export const TRANSFORM_FEEDBACK_VARYINGS = 0x8c83; + +/** + * @constant {number} + */ +export const TRANSFORM_FEEDBACK_BUFFER_START = 0x8c84; + +/** + * @constant {number} + */ +export const TRANSFORM_FEEDBACK_BUFFER_SIZE = 0x8c85; + +/** + * @constant {number} + */ +export const TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN = 0x8c88; + +/** + * @constant {number} + */ +export const MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS = 0x8c8a; + +/** + * @constant {number} + */ +export const MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS = 0x8c8b; + +/** + * @constant {number} + */ +export const INTERLEAVED_ATTRIBS = 0x8c8c; + +/** + * @constant {number} + */ +export const SEPARATE_ATTRIBS = 0x8c8d; + +/** + * @constant {number} + */ +export const TRANSFORM_FEEDBACK_BUFFER = 0x8c8e; + +/** + * @constant {number} + */ +export const TRANSFORM_FEEDBACK_BUFFER_BINDING = 0x8c8f; + +/** + * @constant {number} + */ +export const TRANSFORM_FEEDBACK = 0x8e22; + +/** + * @constant {number} + */ +export const TRANSFORM_FEEDBACK_PAUSED = 0x8e23; + +/** + * @constant {number} + */ +export const TRANSFORM_FEEDBACK_ACTIVE = 0x8e24; + +/** + * @constant {number} + */ +export const TRANSFORM_FEEDBACK_BINDING = 0x8e25; + +// Framebuffers and renderbuffers + +/** + * @constant {number} + */ +export const FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING = 0x8210; + +/** + * @constant {number} + */ +export const FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE = 0x8211; + +/** + * @constant {number} + */ +export const FRAMEBUFFER_ATTACHMENT_RED_SIZE = 0x8212; + +/** + * @constant {number} + */ +export const FRAMEBUFFER_ATTACHMENT_GREEN_SIZE = 0x8213; + +/** + * @constant {number} + */ +export const FRAMEBUFFER_ATTACHMENT_BLUE_SIZE = 0x8214; + +/** + * @constant {number} + */ +export const FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE = 0x8215; + +/** + * @constant {number} + */ +export const FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE = 0x8216; + +/** + * @constant {number} + */ +export const FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE = 0x8217; + +/** + * @constant {number} + */ +export const FRAMEBUFFER_DEFAULT = 0x8218; + +/** + * @constant {number} + */ +export const DEPTH24_STENCIL8 = 0x88f0; + +/** + * @constant {number} + */ +export const DRAW_FRAMEBUFFER_BINDING = 0x8ca6; + +/** + * @constant {number} + */ +export const READ_FRAMEBUFFER = 0x8ca8; + +/** + * @constant {number} + */ +export const DRAW_FRAMEBUFFER = 0x8ca9; + +/** + * @constant {number} + */ +export const READ_FRAMEBUFFER_BINDING = 0x8caa; + +/** + * @constant {number} + */ +export const RENDERBUFFER_SAMPLES = 0x8cab; + +/** + * @constant {number} + */ +export const FRAMEBUFFER_ATTACHMENT_TEXTURE_LAYER = 0x8cd4; + +/** + * @constant {number} + */ +export const FRAMEBUFFER_INCOMPLETE_MULTISAMPLE = 0x8d56; + +// Uniforms + +/** + * @constant {number} + */ +export const UNIFORM_BUFFER = 0x8a11; + +/** + * @constant {number} + */ +export const UNIFORM_BUFFER_BINDING = 0x8a28; + +/** + * @constant {number} + */ +export const UNIFORM_BUFFER_START = 0x8a29; + +/** + * @constant {number} + */ +export const UNIFORM_BUFFER_SIZE = 0x8a2a; + +/** + * @constant {number} + */ +export const MAX_VERTEX_UNIFORM_BLOCKS = 0x8a2b; + +/** + * @constant {number} + */ +export const MAX_FRAGMENT_UNIFORM_BLOCKS = 0x8a2d; + +/** + * @constant {number} + */ +export const MAX_COMBINED_UNIFORM_BLOCKS = 0x8a2e; + +/** + * @constant {number} + */ +export const MAX_UNIFORM_BUFFER_BINDINGS = 0x8a2f; + +/** + * @constant {number} + */ +export const MAX_UNIFORM_BLOCK_SIZE = 0x8a30; + +/** + * @constant {number} + */ +export const MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS = 0x8a31; + +/** + * @constant {number} + */ +export const MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS = 0x8a33; + +/** + * @constant {number} + */ +export const UNIFORM_BUFFER_OFFSET_ALIGNMENT = 0x8a34; + +/** + * @constant {number} + */ +export const ACTIVE_UNIFORM_BLOCKS = 0x8a36; + +/** + * @constant {number} + */ +export const UNIFORM_TYPE = 0x8a37; + +/** + * @constant {number} + */ +export const UNIFORM_SIZE = 0x8a38; + +/** + * @constant {number} + */ +export const UNIFORM_BLOCK_INDEX = 0x8a3a; + +/** + * @constant {number} + */ +export const UNIFORM_OFFSET = 0x8a3b; + +/** + * @constant {number} + */ +export const UNIFORM_ARRAY_STRIDE = 0x8a3c; + +/** + * @constant {number} + */ +export const UNIFORM_MATRIX_STRIDE = 0x8a3d; + +/** + * @constant {number} + */ +export const UNIFORM_IS_ROW_MAJOR = 0x8a3e; + +/** + * @constant {number} + */ +export const UNIFORM_BLOCK_BINDING = 0x8a3f; + +/** + * @constant {number} + */ +export const UNIFORM_BLOCK_DATA_SIZE = 0x8a40; + +/** + * @constant {number} + */ +export const UNIFORM_BLOCK_ACTIVE_UNIFORMS = 0x8a42; + +/** + * @constant {number} + */ +export const UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES = 0x8a43; + +/** + * @constant {number} + */ +export const UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER = 0x8a44; + +/** + * @constant {number} + */ +export const UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER = 0x8a46; + +// Sync objects + +/** + * @constant {number} + */ +export const OBJECT_TYPE = 0x9112; + +/** + * @constant {number} + */ +export const SYNC_CONDITION = 0x9113; + +/** + * @constant {number} + */ +export const SYNC_STATUS = 0x9114; + +/** + * @constant {number} + */ +export const SYNC_FLAGS = 0x9115; + +/** + * @constant {number} + */ +export const SYNC_FENCE = 0x9116; + +/** + * @constant {number} + */ +export const SYNC_GPU_COMMANDS_COMPLETE = 0x9117; + +/** + * @constant {number} + */ +export const UNSIGNALED = 0x9118; + +/** + * @constant {number} + */ +export const SIGNALED = 0x9119; + +/** + * @constant {number} + */ +export const ALREADY_SIGNALED = 0x911a; + +/** + * @constant {number} + */ +export const TIMEOUT_EXPIRED = 0x911b; + +/** + * @constant {number} + */ +export const CONDITION_SATISFIED = 0x911c; + +/** + * @constant {number} + */ +export const WAIT_FAILED = 0x911d; + +/** + * @constant {number} + */ +export const SYNC_FLUSH_COMMANDS_BIT = 0x00000001; + +// Miscellaneous constants + +/** + * @constant {number} + */ +export const COLOR = 0x1800; + +/** + * @constant {number} + */ +export const DEPTH = 0x1801; + +/** + * @constant {number} + */ +export const STENCIL = 0x1802; + +/** + * @constant {number} + */ +export const MIN = 0x8007; + +/** + * @constant {number} + */ +export const MAX = 0x8008; + +/** + * @constant {number} + */ +export const DEPTH_COMPONENT24 = 0x81a6; + +/** + * @constant {number} + */ +export const STREAM_READ = 0x88e1; + +/** + * @constant {number} + */ +export const STREAM_COPY = 0x88e2; + +/** + * @constant {number} + */ +export const STATIC_READ = 0x88e5; + +/** + * @constant {number} + */ +export const STATIC_COPY = 0x88e6; + +/** + * @constant {number} + */ +export const DYNAMIC_READ = 0x88e9; + +/** + * @constant {number} + */ +export const DYNAMIC_COPY = 0x88ea; + +/** + * @constant {number} + */ +export const DEPTH_COMPONENT32F = 0x8cac; + +/** + * @constant {number} + */ +export const DEPTH32F_STENCIL8 = 0x8cad; + +/** + * @constant {number} + */ +export const INVALID_INDEX = 0xffffffff; + +/** + * @constant {number} + */ +export const TIMEOUT_IGNORED = -1; + +/** + * @constant {number} + */ +export const MAX_CLIENT_WAIT_TIMEOUT_WEBGL = 0x9247; + +// Constants defined in WebGL extensions + +// ANGLE_instanced_arrays +// The ANGLE_instanced_arrays extension is part of the WebGL API and allows to draw the same object, or groups of similar objects multiple times, if they share the same vertex data, primitive count and type +/** + * Describes the frequency divisor used for instanced rendering + * @constant {number} + */ +export const VERTEX_ATTRIB_ARRAY_DIVISOR_ANGLE = 0x88fe; + +// WEBGL_debug_renderer_info +// The WEBGL_debug_renderer_info extension is part of the WebGL API and exposes two constants with information about the graphics driver for debugging purposes +/** + * Passed to getParameter to get the vendor string of the graphics driver + * @constant {number} + */ +export const UNMASKED_VENDOR_WEBGL = 0x9245; + +/** + * Passed to getParameter to get the renderer string of the graphics driver + * @constant {number} + */ +export const UNMASKED_RENDERER_WEBGL = 0x9246; + +// EXT_texture_filter_anisotropic +// The EXT_texture_filter_anisotropic extension is part of the WebGL API and exposes two constants for anisotropic filtering (AF) +/** + * Returns the maximum available anisotropy + * @constant {number} + */ +export const MAX_TEXTURE_MAX_ANISOTROPY_EXT = 0x84ff; + +/** + * Passed to texParameter to set the desired maximum anisotropy for a texture + * @constant {number} + */ +export const TEXTURE_MAX_ANISOTROPY_EXT = 0x84fe; + +// WEBGL_compressed_texture_s3tc +// The WEBGL_compressed_texture_s3tc extension is part of the WebGL API and exposes four S3TC compressed texture formats +/** + * A DXT1-compressed image in an RGB image format + * @constant {number} + */ +export const COMPRESSED_RGB_S3TC_DXT1_EXT = 0x83f0; + +/** + * A DXT1-compressed image in an RGB image format with a simple on/off alpha value + * @constant {number} + */ +export const COMPRESSED_RGBA_S3TC_DXT1_EXT = 0x83f1; + +/** + * A DXT3-compressed image in an RGBA image format. Compared to a 32-bit RGBA texture, it offers 4:1 compression + * @constant {number} + */ +export const COMPRESSED_RGBA_S3TC_DXT3_EXT = 0x83f2; + +/** + * A DXT5-compressed image in an RGBA image format. It also provides a 4:1 compression, but differs to the DXT3 compression in how the alpha compression is done + * @constant {number} + */ +export const COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83f3; + +// WEBGL_compressed_texture_s3tc_srgb +// The WEBGL_compressed_texture_s3tc_srgb extension is part of the WebGL API and exposes four S3TC compressed texture formats for the sRGB colorspace +/** + * A DXT1-compressed image in an sRGB image format + * @constant {number} + */ +export const COMPRESSED_SRGB_S3TC_DXT1_EXT = 0x8c4c; + +/** + * A DXT1-compressed image in an sRGB image format with a simple on/off alpha value + * @constant {number} + */ +export const COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT = 0x8c4d; + +/** + * A DXT3-compressed image in an sRGBA image format + * @constant {number} + */ +export const COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT = 0x8c4e; + +/** + * A DXT5-compressed image in an sRGBA image format + * @constant {number} + */ +export const COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT = 0x8c4f; + +// WEBGL_compressed_texture_etc +// The WEBGL_compressed_texture_etc extension is part of the WebGL API and exposes 10 ETC/EAC compressed texture formats +/** + * One-channel (red) unsigned format compression + * @constant {number} + */ +export const COMPRESSED_R11_EAC = 0x9270; + +/** + * One-channel (red) signed format compression + * @constant {number} + */ +export const COMPRESSED_SIGNED_R11_EAC = 0x9271; + +/** + * Two-channel (red and green) unsigned format compression + * @constant {number} + */ +export const COMPRESSED_RG11_EAC = 0x9272; + +/** + * Two-channel (red and green) signed format compression + * @constant {number} + */ +export const COMPRESSED_SIGNED_RG11_EAC = 0x9273; + +/** + * Compresses RBG8 data with no alpha channel + * @constant {number} + */ +export const COMPRESSED_RGB8_ETC2 = 0x9274; + +/** + * Compresses RGBA8 data. The RGB part is encoded the same as RGB_ETC2, but the alpha part is encoded separately + * @constant {number} + */ +export const COMPRESSED_RGBA8_ETC2_EAC = 0x9275; + +/** + * Compresses sRBG8 data with no alpha channel + * @constant {number} + */ +export const COMPRESSED_SRGB8_ETC2 = 0x9276; + +/** + * Compresses sRGBA8 data. The sRGB part is encoded the same as SRGB_ETC2, but the alpha part is encoded separately + * @constant {number} + */ +export const COMPRESSED_SRGB8_ALPHA8_ETC2_EAC = 0x9277; + +/** + * Similar to RGB8_ETC, but with ability to punch through the alpha channel, which means to make it completely opaque or transparent + * @constant {number} + */ +export const COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9278; + +/** + * Similar to SRGB8_ETC, but with ability to punch through the alpha channel, which means to make it completely opaque or transparent + * @constant {number} + */ +export const COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9279; + +// WEBGL_compressed_texture_pvrtc +// The WEBGL_compressed_texture_pvrtc extension is part of the WebGL API and exposes four PVRTC compressed texture formats +/** + * RGB compression in 4-bit mode. One block for each 4×4 pixels + * @constant {number} + */ +export const COMPRESSED_RGB_PVRTC_4BPPV1_IMG = 0x8c00; + +/** + * RGBA compression in 4-bit mode. One block for each 4×4 pixels + * @constant {number} + */ +export const COMPRESSED_RGBA_PVRTC_4BPPV1_IMG = 0x8c02; + +/** + * RGB compression in 2-bit mode. One block for each 8×4 pixels + * @constant {number} + */ +export const COMPRESSED_RGB_PVRTC_2BPPV1_IMG = 0x8c01; + +/** + * RGBA compression in 2-bit mode. One block for each 8×4 pixels + * @constant {number} + */ +export const COMPRESSED_RGBA_PVRTC_2BPPV1_IMG = 0x8c03; + +// WEBGL_compressed_texture_etc1 +// The WEBGL_compressed_texture_etc1 extension is part of the WebGL API and exposes the ETC1 compressed texture format +/** + * Compresses 24-bit RGB data with no alpha channel + * @constant {number} + */ +export const COMPRESSED_RGB_ETC1_WEBGL = 0x8d64; + +// WEBGL_compressed_texture_atc +// The WEBGL_compressed_texture_atc extension is part of the WebGL API and exposes 3 ATC compressed texture formats. ATC is a proprietary compression algorithm for compressing textures on handheld devices +/** + * Compresses RGB textures with no alpha channel + * @constant {number} + */ +export const COMPRESSED_RGB_ATC_WEBGL = 0x8c92; + +/** + * Compresses RGBA textures using explicit alpha encoding (useful when alpha transitions are sharp) + * @constant {number} + */ +export const COMPRESSED_RGBA_ATC_EXPLICIT_ALPHA_WEBGL = 0x8c92; + +/** + * Compresses RGBA textures using interpolated alpha encoding (useful when alpha transitions are gradient) + * @constant {number} + */ +export const COMPRESSED_RGBA_ATC_INTERPOLATED_ALPHA_WEBGL = 0x87ee; + +// WEBGL_compressed_texture_astc +// The WEBGL_compressed_texture_astc extension is part of the WebGL API and exposes Adaptive Scalable Texture Compression (ASTC) compressed texture formats to WebGL +// https://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_astc/ +// https://developer.nvidia.com/astc-texture-compression-for-game-assets +/** + * Compresses RGBA textures using ASTC compression in a blocksize of 4x4 + * @constant {number} + */ +export const COMPRESSED_RGBA_ASTC_4X4_KHR = 0x93b0; + +/** + * Compresses RGBA textures using ASTC compression in a blocksize of 5x4 + * @constant {number} + */ +export const COMPRESSED_RGBA_ASTC_5X4_KHR = 0x93b1; + +/** + * Compresses RGBA textures using ASTC compression in a blocksize of 5x5 + * @constant {number} + */ +export const COMPRESSED_RGBA_ASTC_5X5_KHR = 0x93b2; + +/** + * Compresses RGBA textures using ASTC compression in a blocksize of 6x5 + * @constant {number} + */ +export const COMPRESSED_RGBA_ASTC_6X5_KHR = 0x93b3; + +/** + * Compresses RGBA textures using ASTC compression in a blocksize of 6x6 + * @constant {number} + */ +export const COMPRESSED_RGBA_ASTC_6X6_KHR = 0x93b4; + +/** + * Compresses RGBA textures using ASTC compression in a blocksize of 8x5 + * @constant {number} + */ +export const COMPRESSED_RGBA_ASTC_8X5_KHR = 0x93b5; + +/** + * Compresses RGBA textures using ASTC compression in a blocksize of 8x6 + * @constant {number} + */ +export const COMPRESSED_RGBA_ASTC_8X6_KHR = 0x93b6; + +/** + * Compresses RGBA textures using ASTC compression in a blocksize of 8x8 + * @constant {number} + */ +export const COMPRESSED_RGBA_ASTC_8X8_KHR = 0x93b7; + +/** + * Compresses RGBA textures using ASTC compression in a blocksize of 10x5 + * @constant {number} + */ +export const COMPRESSED_RGBA_ASTC_10X5_KHR = 0x93b8; + +/** + * Compresses RGBA textures using ASTC compression in a blocksize of 10x6 + * @constant {number} + */ +export const COMPRESSED_RGBA_ASTC_10X6_KHR = 0x93b9; + +/** + * Compresses RGBA textures using ASTC compression in a blocksize of 10x8 + * @constant {number} + */ +export const COMPRESSED_RGBA_ASTC_10X8_KHR = 0x93ba; + +/** + * Compresses RGBA textures using ASTC compression in a blocksize of 10x10 + * @constant {number} + */ +export const COMPRESSED_RGBA_ASTC_10X10_KHR = 0x93bb; + +/** + * Compresses RGBA textures using ASTC compression in a blocksize of 12x10 + * @constant {number} + */ +export const COMPRESSED_RGBA_ASTC_12X10_KHR = 0x93bc; + +/** + * Compresses RGBA textures using ASTC compression in a blocksize of 12x12 + * @constant {number} + */ +export const COMPRESSED_RGBA_ASTC_12X12_KHR = 0x93bd; + +/** + * Compresses SRGB8 textures using ASTC compression in a blocksize of 4x4 + * @constant {number} + */ +export const COMPRESSED_SRGB8_ALPHA8_ASTC_4X4_KHR = 0x93d0; + +/** + * Compresses SRGB8 textures using ASTC compression in a blocksize of 5x4 + * @constant {number} + */ +export const COMPRESSED_SRGB8_ALPHA8_ASTC_5X4_KHR = 0x93d1; + +/** + * Compresses SRGB8 textures using ASTC compression in a blocksize of 5x5 + * @constant {number} + */ +export const COMPRESSED_SRGB8_ALPHA8_ASTC_5X5_KHR = 0x93d2; + +/** + * Compresses SRGB8 textures using ASTC compression in a blocksize of 6x5 + * @constant {number} + */ +export const COMPRESSED_SRGB8_ALPHA8_ASTC_6X5_KHR = 0x93d3; + +/** + * Compresses SRGB8 textures using ASTC compression in a blocksize of 6x6 + * @constant {number} + */ +export const COMPRESSED_SRGB8_ALPHA8_ASTC_6X6_KHR = 0x93d4; + +/** + * Compresses SRGB8 textures using ASTC compression in a blocksize of 8x5 + * @constant {number} + */ +export const COMPRESSED_SRGB8_ALPHA8_ASTC_8X5_KHR = 0x93d5; + +/** + * Compresses SRGB8 textures using ASTC compression in a blocksize of 8x6 + * @constant {number} + */ +export const COMPRESSED_SRGB8_ALPHA8_ASTC_8X6_KHR = 0x93d6; + +/** + * Compresses SRGB8 textures using ASTC compression in a blocksize of 8x8 + * @constant {number} + */ +export const COMPRESSED_SRGB8_ALPHA8_ASTC_8X8_KHR = 0x93d7; + +/** + * Compresses SRGB8 textures using ASTC compression in a blocksize of 10x5 + * @constant {number} + */ +export const COMPRESSED_SRGB8_ALPHA8_ASTC_10X5_KHR = 0x93d8; + +/** + * Compresses SRGB8 textures using ASTC compression in a blocksize of 10x6 + * @constant {number} + */ +export const COMPRESSED_SRGB8_ALPHA8_ASTC_10X6_KHR = 0x93d9; + +/** + * Compresses SRGB8 textures using ASTC compression in a blocksize of 10x8 + * @constant {number} + */ +export const COMPRESSED_SRGB8_ALPHA8_ASTC_10X8_KHR = 0x93da; + +/** + * Compresses SRGB8 textures using ASTC compression in a blocksize of 10x10 + * @constant {number} + */ +export const COMPRESSED_SRGB8_ALPHA8_ASTC_10X10_KHR = 0x93db; + +/** + * Compresses SRGB8 textures using ASTC compression in a blocksize of 12x10 + * @constant {number} + */ +export const COMPRESSED_SRGB8_ALPHA8_ASTC_12X10_KHR = 0x93dc; + +/** + * Compresses SRGB8 textures using ASTC compression in a blocksize of 12x12 + * @constant {number} + */ +export const COMPRESSED_SRGB8_ALPHA8_ASTC_12X12_KHR = 0x93dd; + +// WEBGL_depth_texture +// The WEBGL_depth_texture extension is part of the WebGL API and defines 2D depth and depth-stencil textures +/** + * Unsigned integer type for 24-bit depth texture data + * @constant {number} + */ +export const UNSIGNED_INT_24_8_WEBGL = 0x84fa; + +// OES_texture_half_float +// The OES_texture_half_float extension is part of the WebGL API and adds texture formats with 16- (aka half float) and 32-bit floating-point components +/** + * Half floating-point type (16-bit) + * @constant {number} + */ +export const HALF_FLOAT_OES = 0x8d61; + +// WEBGL_color_buffer_float +// The WEBGL_color_buffer_float extension is part of the WebGL API and adds the ability to render to 32-bit floating-point color buffers +/** + * RGBA 32-bit floating-point color-renderable format + * @constant {number} + */ +export const RGBA32F_EXT = 0x8814; + +/** + * RGB 32-bit floating-point color-renderable format + * @constant {number} + */ +export const RGB32F_EXT = 0x8815; + +/** + * @constant {number} + */ +export const FRAMEBUFFER_ATTACHMENT_COMPONENT_TYPE_EXT = 0x8211; + +/** + * @constant {number} + */ +export const UNSIGNED_NORMALIZED_EXT = 0x8c17; + +// EXT_blend_minmax +// The EXT_blend_minmax extension is part of the WebGL API and extends blending capabilities by adding two new blend equations: the minimum or maximum color components of the source and destination colors +/** + * Produces the minimum color components of the source and destination colors + * @constant {number} + */ +export const MIN_EXT = 0x8007; + +/** + * Produces the maximum color components of the source and destination colors + * @constant {number} + */ +export const MAX_EXT = 0x8008; + +// EXT_sRGB +// The EXT_sRGB extension is part of the WebGL API and adds sRGB support to textures and framebuffer objects +/** + * Unsized sRGB format that leaves the precision up to the driver + * @constant {number} + */ +export const SRGB_EXT = 0x8c40; + +/** + * Unsized sRGB format with unsized alpha component + * @constant {number} + */ +export const SRGB_ALPHA_EXT = 0x8c42; + +/** + * Sized (8-bit) sRGB and alpha formats + * @constant {number} + */ +export const SRGB8_ALPHA8_EXT = 0x8c43; + +/** + * Returns the framebuffer color encoding + * @constant {number} + */ +export const FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING_EXT = 0x8210; + +// OES_standard_derivatives +// The OES_standard_derivatives extension is part of the WebGL API and adds the GLSL derivative functions dFdx, dFdy, and fwidth +/** + * Indicates the accuracy of the derivative calculation for the GLSL built-in functions: dFdx, dFdy, and fwidth + * @constant {number} + */ +export const FRAGMENT_SHADER_DERIVATIVE_HINT_OES = 0x8b8b; + +// WEBGL_draw_buffers +// The WEBGL_draw_buffers extension is part of the WebGL API and enables a fragment shader to write to several textures, which is useful for deferred shading, for example +/** + * Framebuffer color attachment point + * @constant {number} + */ +export const COLOR_ATTACHMENT0_WEBGL = 0x8ce0; + +/** + * Framebuffer color attachment point + * @constant {number} + */ +export const COLOR_ATTACHMENT1_WEBGL = 0x8ce1; + +/** + * Framebuffer color attachment point + * @constant {number} + */ +export const COLOR_ATTACHMENT2_WEBGL = 0x8ce2; + +/** + * Framebuffer color attachment point + * @constant {number} + */ +export const COLOR_ATTACHMENT3_WEBGL = 0x8ce3; + +/** + * Framebuffer color attachment point + * @constant {number} + */ +export const COLOR_ATTACHMENT4_WEBGL = 0x8ce4; + +/** + * Framebuffer color attachment point + * @constant {number} + */ +export const COLOR_ATTACHMENT5_WEBGL = 0x8ce5; + +/** + * Framebuffer color attachment point + * @constant {number} + */ +export const COLOR_ATTACHMENT6_WEBGL = 0x8ce6; + +/** + * Framebuffer color attachment point + * @constant {number} + */ +export const COLOR_ATTACHMENT7_WEBGL = 0x8ce7; + +/** + * Framebuffer color attachment point + * @constant {number} + */ +export const COLOR_ATTACHMENT8_WEBGL = 0x8ce8; + +/** + * Framebuffer color attachment point + * @constant {number} + */ +export const COLOR_ATTACHMENT9_WEBGL = 0x8ce9; + +/** + * Framebuffer color attachment point + * @constant {number} + */ +export const COLOR_ATTACHMENT10_WEBGL = 0x8cea; + +/** + * Framebuffer color attachment point + * @constant {number} + */ +export const COLOR_ATTACHMENT11_WEBGL = 0x8ceb; + +/** + * Framebuffer color attachment point + * @constant {number} + */ +export const COLOR_ATTACHMENT12_WEBGL = 0x8cec; + +/** + * Framebuffer color attachment point + * @constant {number} + */ +export const COLOR_ATTACHMENT13_WEBGL = 0x8ced; + +/** + * Framebuffer color attachment point + * @constant {number} + */ +export const COLOR_ATTACHMENT14_WEBGL = 0x8cee; + +/** + * Framebuffer color attachment point + * @constant {number} + */ +export const COLOR_ATTACHMENT15_WEBGL = 0x8cef; + +/** + * Draw buffer + * @constant {number} + */ +export const DRAW_BUFFER0_WEBGL = 0x8825; + +/** + * Draw buffer + * @constant {number} + */ +export const DRAW_BUFFER1_WEBGL = 0x8826; + +/** + * Draw buffer + * @constant {number} + */ +export const DRAW_BUFFER2_WEBGL = 0x8827; + +/** + * Draw buffer + * @constant {number} + */ +export const DRAW_BUFFER3_WEBGL = 0x8828; + +/** + * Draw buffer + * @constant {number} + */ +export const DRAW_BUFFER4_WEBGL = 0x8829; + +/** + * Draw buffer + * @constant {number} + */ +export const DRAW_BUFFER5_WEBGL = 0x882a; + +/** + * Draw buffer + * @constant {number} + */ +export const DRAW_BUFFER6_WEBGL = 0x882b; + +/** + * Draw buffer + * @constant {number} + */ +export const DRAW_BUFFER7_WEBGL = 0x882c; + +/** + * Draw buffer + * @constant {number} + */ +export const DRAW_BUFFER8_WEBGL = 0x882d; + +/** + * Draw buffer + * @constant {number} + */ +export const DRAW_BUFFER9_WEBGL = 0x882e; + +/** + * Draw buffer + * @constant {number} + */ +export const DRAW_BUFFER10_WEBGL = 0x882f; + +/** + * Draw buffer + * @constant {number} + */ +export const DRAW_BUFFER11_WEBGL = 0x8830; + +/** + * Draw buffer + * @constant {number} + */ +export const DRAW_BUFFER12_WEBGL = 0x8831; + +/** + * Draw buffer + * @constant {number} + */ +export const DRAW_BUFFER13_WEBGL = 0x8832; + +/** + * Draw buffer + * @constant {number} + */ +export const DRAW_BUFFER14_WEBGL = 0x8833; + +/** + * Draw buffer + * @constant {number} + */ +export const DRAW_BUFFER15_WEBGL = 0x8834; + +/** + * Maximum number of framebuffer color attachment points + * @constant {number} + */ +export const MAX_COLOR_ATTACHMENTS_WEBGL = 0x8cdf; + +/** + * Maximum number of draw buffers + * @constant {number} + */ +export const MAX_DRAW_BUFFERS_WEBGL = 0x8824; + +// OES_vertex_array_object +// The OES_vertex_array_object extension is part of the WebGL API and provides vertex array objects (VAOs) which encapsulate vertex array states. These objects keep pointers to vertex data and provide names for different sets of vertex data +/** + * The bound vertex array object (VAO) + * @constant {number} + */ +export const VERTEX_ARRAY_BINDING_OES = 0x85b5; + +// EXT_disjoint_timer_query +// The EXT_disjoint_timer_query extension is part of the WebGL API and provides a way to measure the duration of a set of GL commands, without stalling the rendering pipeline +/** + * The number of bits used to hold the query result for the given target + * @constant {number} + */ +export const QUERY_COUNTER_BITS_EXT = 0x8864; + +/** + * The currently active query + * @constant {number} + */ +export const CURRENT_QUERY_EXT = 0x8865; + +/** + * The query result + * @constant {number} + */ +export const QUERY_RESULT_EXT = 0x8866; + +/** + * A Boolean indicating whether or not a query result is available + * @constant {number} + */ +export const QUERY_RESULT_AVAILABLE_EXT = 0x8867; + +/** + * Elapsed time (in nanoseconds) + * @constant {number} + */ +export const TIME_ELAPSED_EXT = 0x88bf; + +/** + * The current time + * @constant {number} + */ +export const TIMESTAMP_EXT = 0x8e28; + +/** + * A Boolean indicating whether or not the GPU performed any disjoint operation + * @constant {number} + */ +export const GPU_DISJOINT_EXT = 0x8fbb; + +// Constants defined in WebGL draft extensions + +// KHR_parallel_shader_compile +// The KHR_parallel_shader_compile extension is part of the WebGL draft API and provides multithreaded asynchronous shader compilation +/** + * Query to determine if the compilation process is complete + * @constant {number} + */ +export const COMPLETION_STATUS_KHR = 0x91b1; diff --git a/simulator/src/z85.ts b/simulator/src/z85.ts new file mode 100644 index 0000000..c0d49fb --- /dev/null +++ b/simulator/src/z85.ts @@ -0,0 +1,78 @@ +// Encodes binary data into text, like base64 but more efficient. +// +// Implements http://rfc.zeromq.org/spec:32 +// Ported from https://github.com/zeromq/libzmq/blob/8cda54c52b08005b71f828243f22051cdbc482b4/src/zmq_utils.cpp#L77-L168 + +const ENCODER = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#".split(""); + +const DECODER = [ + 0x00, 0x44, 0x00, 0x54, 0x53, 0x52, 0x48, 0x00, + 0x4B, 0x4C, 0x46, 0x41, 0x00, 0x3F, 0x3E, 0x45, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x40, 0x00, 0x49, 0x42, 0x4A, 0x47, + 0x51, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, + 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, + 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, + 0x3B, 0x3C, 0x3D, 0x4D, 0x00, 0x4E, 0x43, 0x00, + 0x00, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, + 0x21, 0x22, 0x23, 0x4F, 0x00, 0x50, 0x00, 0x00 +]; + +export function encode (src: number[] | Uint8Array | Uint8ClampedArray): string { + const size = src.length; + const extra = (size % 4); + const paddedSize = extra ? size + 4-extra : size; + + let str = "", + byte_nbr = 0, + value = 0; + while (byte_nbr < paddedSize) { + const b = (byte_nbr < size) ? src[byte_nbr] : 0; + ++byte_nbr; + value = (value * 256) + b; + if ((byte_nbr % 4) == 0) { + let divisor = 85 * 85 * 85 * 85; + while (divisor >= 1) { + const idx = Math.floor(value / divisor) % 85; + str += ENCODER[idx]; + divisor /= 85; + } + value = 0; + } + } + + return str; +} + +export function decode (string: string, dest: number[] | Uint8Array | Uint8ClampedArray): number { + let byte_nbr = 0, + char_nbr = 0, + value = 0; + const string_len = string.length, + dest_len = dest.length; + + if ((string.length % 5) == 0) { + while (char_nbr < string_len) { + const idx = string.charCodeAt(char_nbr++) - 32; + if ((idx < 0) || (idx >= DECODER.length)) { + return byte_nbr; + } + value = (value * 85) + DECODER[idx]; + if ((char_nbr % 5) == 0) { + let divisor = 256 * 256 * 256; + while (divisor >= 1) { + if (byte_nbr >= dest_len) { + return byte_nbr; + } + dest[byte_nbr++] = (value / divisor) % 256; + divisor /= 256; + } + value = 0; + } + } + } + + return byte_nbr; +} diff --git a/simulator/tsconfig.json b/simulator/tsconfig.json new file mode 100644 index 0000000..407d685 --- /dev/null +++ b/simulator/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "module": "esnext", + "lib": ["es2017", "dom"], + "noEmit": true, + "rootDir": "./src", + "strict": true, + "target": "ES2015", + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "forceConsistentCasingInFileNames": true, + "alwaysStrict": true, + "useDefineForClassFields": false + }, + "include": ["src/**/*.ts", "src/**/*.js"], + "exclude": [] +} diff --git a/src/wasm4.zig b/src/wasm4.zig index 01fb919..71114b7 100644 --- a/src/wasm4.zig +++ b/src/wasm4.zig @@ -1,5 +1,5 @@ -// -// WASM-4: https://wasm4.org/docs +const std = @import("std"); +const builtin = @import("builtin"); // ┌───────────────────────────────────────────────────────────────────────────┐ // │ │ @@ -7,7 +7,8 @@ // │ │ // └───────────────────────────────────────────────────────────────────────────┘ -pub const SCREEN_SIZE: u32 = 160; +pub const screen_width: u32 = 160; +pub const screen_height: u32 = 128; // ┌───────────────────────────────────────────────────────────────────────────┐ // │ │ @@ -15,32 +16,50 @@ pub const SCREEN_SIZE: u32 = 160; // │ │ // └───────────────────────────────────────────────────────────────────────────┘ -pub const PALETTE: *[4]u32 = @ptrFromInt(0x20000004); -pub const DRAW_COLORS: *u16 = @ptrFromInt(0x20000014); -pub const GAMEPAD1: *const u8 = @ptrFromInt(0x20000016); -pub const GAMEPAD2: *const u8 = @ptrFromInt(0x20000017); -pub const GAMEPAD3: *const u8 = @ptrFromInt(0x20000018); -pub const GAMEPAD4: *const u8 = @ptrFromInt(0x20000019); -pub const MOUSE_X: *const i16 = @ptrFromInt(0x2000001a); -pub const MOUSE_Y: *const i16 = @ptrFromInt(0x2000001c); -pub const MOUSE_BUTTONS: *const u8 = @ptrFromInt(0x2000001e); -pub const SYSTEM_FLAGS: *u8 = @ptrFromInt(0x2000001f); -pub const NETPLAY: *const u8 = @ptrFromInt(0x20000020); -pub const FRAMEBUFFER: *[6400]u8 = @ptrFromInt(0x200000A0); - -pub const BUTTON_1: u8 = 1; -pub const BUTTON_2: u8 = 2; -pub const BUTTON_LEFT: u8 = 16; -pub const BUTTON_RIGHT: u8 = 32; -pub const BUTTON_UP: u8 = 64; -pub const BUTTON_DOWN: u8 = 128; - -pub const MOUSE_LEFT: u8 = 1; -pub const MOUSE_RIGHT: u8 = 2; -pub const MOUSE_MIDDLE: u8 = 4; - -pub const SYSTEM_PRESERVE_FRAMEBUFFER: u8 = 1; -pub const SYSTEM_HIDE_GAMEPAD_OVERLAY: u8 = 2; +const base = if (builtin.target.isWasm()) 0 else 0x20000000; + +/// RGB888, true color +pub const NeopixelColor = packed struct(u24) { blue: u8, green: u8, red: u8 }; + +/// RGB565, high color +pub const DisplayColor = packed struct(u16) { blue: u5, green: u6, red: u5 }; +const OptionalDisplayColor = enum(i32) { + none = -1, + _, + + inline fn from(color: ?DisplayColor) OptionalDisplayColor { + return if (color) |c| @enumFromInt(@as(u16, @bitCast(c))) else .none; + } +}; + +pub const Controls = packed struct { + /// START button + start: bool, + /// SELECT button + select: bool, + /// A button + a: bool, + /// B button + b: bool, + + /// Tactile click + click: bool, + /// Tactile up + up: bool, + /// Tactile down + down: bool, + /// Tactile left + left: bool, + /// Tactile right + right: bool, +}; + +pub const controls: *const Controls = @ptrFromInt(base + 0x04); +pub const light_level: *const u12 = @ptrFromInt(base + 0x06); +/// 5 24-bit color LEDs +pub const neopixels: *[5]NeopixelColor = @ptrFromInt(base + 0x08); +pub const red_led: *bool = @ptrFromInt(base + 0x1c); +pub const framebuffer: *[screen_width * screen_height]DisplayColor = @ptrFromInt(base + 0x1e); // ┌───────────────────────────────────────────────────────────────────────────┐ // │ │ @@ -48,130 +67,234 @@ pub const SYSTEM_HIDE_GAMEPAD_OVERLAY: u8 = 2; // │ │ // └───────────────────────────────────────────────────────────────────────────┘ -/// Copies pixels to the framebuffer. -pub inline fn blit(sprite: [*]const u8, x: i32, y: i32, width: u32, height: u32, flags: u32) void { - const rest: extern struct { - width: u32, - height: u32, - flags: u32, - } = .{ - .width = width, - .height = height, - .flags = flags, +const platform_specific = if (builtin.target.isWasm()) + struct { + extern fn blit(sprite: [*]const u8, x: i32, y: i32, width: u32, height: u32, flags: BlitFlags) void; + extern fn blit_sub(sprite: [*]const u8, x: i32, y: i32, width: u32, height: u32, src_x: u32, src_y: u32, stride: u32, flags: BlitFlags) void; + extern fn line(color: DisplayColor, x1: i32, y1: i32, x2: i32, y2: i32) void; + extern fn oval(stroke_color: OptionalDisplayColor, fill_color: OptionalDisplayColor, x: i32, y: i32, width: u32, height: u32) void; + extern fn rect(stroke_color: OptionalDisplayColor, fill_color: OptionalDisplayColor, x: i32, y: i32, width: u32, height: u32) void; + extern fn text(text_color: DisplayColor, background_color: OptionalDisplayColor, str_ptr: [*]const u8, str_len: usize, x: i32, y: i32) void; + extern fn vline(color: DisplayColor, x: i32, y: i32, len: u32) void; + extern fn hline(color: DisplayColor, x: i32, y: i32, len: u32) void; + extern fn tone(frequency: u32, duration: u32, volume: u32, flags: ToneFlags) void; + extern fn read_flash(offset: u32, dst: [*]u8, len: u32) u32; + extern fn write_flash_page(page: u32, src: [*]const u8) void; + extern fn trace(str_ptr: [*]const u8, str_len: usize) void; + } +else + struct { + export fn __return_thunk__() noreturn { + asm volatile (" svc #12"); + unreachable; + } }; - asm volatile (" svc #0" - : - : [sprite] "{r0}" (sprite), - [x] "{r1}" (x), - [y] "{r2}" (y), - [rest] "{r3}" (&rest), - : "memory" - ); + +comptime { + _ = platform_specific; } -/// Copies a subregion within a larger sprite atlas to the framebuffer. -pub inline fn blit_sub(sprite: [*]const u8, x: i32, y: i32, width: u32, height: u32, src_x: u32, src_y: u32, stride: u32, flags: u32) void { - const rest: extern struct { - width: u32, - height: u32, - src_x: u32, - src_y: u32, - stride: u32, - flags: u32, - } = .{ - .width = width, - .height = height, - .src_x = src_x, - .src_y = src_y, - .stride = stride, - .flags = flags, - }; - asm volatile (" svc #1" - : - : [sprite] "{r0}" (sprite), - [x] "{r1}" (x), - [y] "{r2}" (y), - [rest] "{r3}" (&rest), - : "memory" - ); +pub const BitsPerPixel = enum(u1) { one, two }; +pub const BlitFlags = packed struct(u32) { + bits_per_pixel: BitsPerPixel = .one, + flip_x: bool = false, + flip_y: bool = false, + rotate: bool = false, + padding: u28 = undefined, +}; + +/// Copies pixels to the framebuffer. +/// colors.len >= 2 for flags.bits_per_pixel == .one +/// colors.len >= 4 for flags.bits_per_pixel == .two +/// TODO: this is super unsafe also blit is just a basic wrapper over blitSub +pub inline fn blit(colors: [*]const DisplayColor, sprite: [*]const u8, x: i32, y: i32, width: u32, height: u32, flags: BlitFlags) void { + _ = colors; + if (comptime builtin.target.isWasm()) { + platform_specific.blit(sprite, x, y, width, height, flags); + } else { + @compileError("TODO"); + // const rest: extern struct { + // width: u32, + // height: u32, + // flags: u32, + // } = .{ + // .width = width, + // .height = height, + // .flags = flags, + // }; + // asm volatile (" svc #0" + // : + // : [sprite] "{r0}" (sprite), + // [x] "{r1}" (x), + // [y] "{r2}" (y), + // [rest] "{r3}" (&rest), + // : "memory" + // ); + } } -pub const BLIT_2BPP: u32 = 1; -pub const BLIT_1BPP: u32 = 0; -pub const BLIT_FLIP_X: u32 = 2; -pub const BLIT_FLIP_Y: u32 = 4; -pub const BLIT_ROTATE: u32 = 8; +/// Copies a subregion within a larger sprite atlas to the framebuffer. +/// colors.len >= 2 for flags.bits_per_pixel == .one +/// colors.len >= 4 for flags.bits_per_pixel == .two +/// TODO: this is super unsafe also blit is just a basic wrapper over blitSub +pub inline fn blit_sub(sprite: [*]const u8, x: i32, y: i32, width: u32, height: u32, src_x: u32, src_y: u32, stride: u32, flags: BlitFlags) void { + if (comptime builtin.target.isWasm()) { + platform_specific.blit_sub(sprite, x, y, width, height, src_x, src_y, stride, flags); + } else { + @compileError("TODO"); + // const rest: extern struct { + // width: u32, + // height: u32, + // src_x: u32, + // src_y: u32, + // stride: u32, + // flags: u32, + // } = .{ + // .width = width, + // .height = height, + // .src_x = src_x, + // .src_y = src_y, + // .stride = stride, + // .flags = flags, + // }; + // asm volatile (" svc #1" + // : + // : [sprite] "{r0}" (sprite), + // [x] "{r1}" (x), + // [y] "{r2}" (y), + // [rest] "{r3}" (&rest), + // : "memory" + // ); + } +} /// Draws a line between two points. -pub inline fn line(x1: i32, y1: i32, x2: i32, y2: i32) void { - asm volatile (" svc #2" - : - : [x1] "{r0}" (x1), - [y1] "{r1}" (y1), - [x2] "{r2}" (x2), - [y2] "{r3}" (y2), - : "memory" - ); +pub inline fn line(color: DisplayColor, x1: i32, y1: i32, x2: i32, y2: i32) void { + if (comptime builtin.target.isWasm()) { + platform_specific.line(color, x1, y1, x2, y2); + } else { + @compileError("TODO"); + // asm volatile (" svc #2" + // : + // : [x1] "{r0}" (x1), + // [y1] "{r1}" (y1), + // [x2] "{r2}" (x2), + // [y2] "{r3}" (y2), + // : "memory" + // ); + } } /// Draws an oval (or circle). -pub inline fn oval(x: i32, y: i32, width: u32, height: u32) void { - asm volatile (" svc #3" - : - : [x] "{r0}" (x), - [y] "{r1}" (y), - [width] "{r2}" (width), - [height] "{r3}" (height), - : "memory" - ); +pub inline fn oval(stroke_color: ?DisplayColor, fill_color: ?DisplayColor, x: i32, y: i32, width: u32, height: u32) void { + if (comptime builtin.target.isWasm()) { + platform_specific.oval(OptionalDisplayColor.from(stroke_color), OptionalDisplayColor.from(fill_color), x, y, width, height); + } else { + @compileError("TODO"); + // asm volatile (" svc #3" + // : + // : [x] "{r0}" (x), + // [y] "{r1}" (y), + // [width] "{r2}" (width), + // [height] "{r3}" (height), + // : "memory" + // ); + } } /// Draws a rectangle. -pub inline fn rect(x: i32, y: i32, width: u32, height: u32) void { - asm volatile (" svc #4" - : - : [x] "{r0}" (x), - [y] "{r1}" (y), - [width] "{r2}" (width), - [height] "{r3}" (height), - : "memory" - ); +pub inline fn rect(stroke_color: ?DisplayColor, fill_color: ?DisplayColor, x: i32, y: i32, width: u32, height: u32) void { + if (comptime builtin.target.isWasm()) { + platform_specific.rect(OptionalDisplayColor.from(stroke_color), OptionalDisplayColor.from(fill_color), x, y, width, height); + } else { + @compileError("TODO"); + // asm volatile (" svc #4" + // : + // : [x] "{r0}" (x), + // [y] "{r1}" (y), + // [width] "{r2}" (width), + // [height] "{r3}" (height), + // : "memory" + // ); + } } /// Draws text using the built-in system font. -pub inline fn text(str: []const u8, x: i32, y: i32) void { - asm volatile (" svc #5" - : - : [str_ptr] "{r0}" (str.ptr), - [str_len] "{r1}" (str.len), - [x] "{r2}" (x), - [y] "{r3}" (y), - : "memory" - ); +pub inline fn text(text_color: DisplayColor, background_color: ?DisplayColor, str: []const u8, x: i32, y: i32) void { + if (comptime builtin.target.isWasm()) { + platform_specific.text(text_color, OptionalDisplayColor.from(background_color), str.ptr, str.len, x, y); + } else { + @compileError("TODO"); + // asm volatile (" svc #5" + // : + // : [str_ptr] "{r0}" (str.ptr), + // [str_len] "{r1}" (str.len), + // [x] "{r2}" (x), + // [y] "{r3}" (y), + // : "memory" + // ); + } } -/// Draws a vertical line -pub inline fn vline(x: i32, y: i32, len: u32) void { - asm volatile (" svc #6" - : - : [x] "{r0}" (x), - [y] "{r1}" (y), - [len] "{r2}" (len), - : "memory" - ); +/// Draws a horizontal line +pub inline fn hline(color: DisplayColor, x: i32, y: i32, len: u32) void { + if (comptime builtin.target.isWasm()) { + platform_specific.hline(color, x, y, len); + } else { + @compileError("TODO"); + // asm volatile (" svc #7" + // : + // : [x] "{r0}" (x), + // [y] "{r1}" (y), + // [len] "{r2}" (len), + // : "memory" + // ); + } } -/// Draws a horizontal line -pub inline fn hline(x: i32, y: i32, len: u32) void { - asm volatile (" svc #7" - : - : [x] "{r0}" (x), - [y] "{r1}" (y), - [len] "{r2}" (len), - : "memory" - ); +/// Draws a vertical line +pub inline fn vline(color: DisplayColor, x: i32, y: i32, len: u32) void { + if (comptime builtin.target.isWasm()) { + platform_specific.vline(color, x, y, len); + } else { + @compileError("TODO"); + // asm volatile (" svc #6" + // : + // : [x] "{r0}" (x), + // [y] "{r1}" (y), + // [len] "{r2}" (len), + // : "memory" + // ); + } } +pub const ToneFlags = packed struct(u32) { + pub const Channel = enum(u2) { + pulse1, + pulse2, + triangle, + noise, + }; + + pub const DutyCycle = enum(u2) { + @"1/8", + @"1/4", + @"1/2", + @"3/4", + }; + + pub const Panning = enum(u2) { + stereo, + left, + right, + }; + + channel: Channel, + duty_cycle: DutyCycle, + panning: Panning, + padding: u26 = undefined, +}; + // ┌───────────────────────────────────────────────────────────────────────────┐ // │ │ // │ Sound Functions │ @@ -179,49 +302,50 @@ pub inline fn hline(x: i32, y: i32, len: u32) void { // └───────────────────────────────────────────────────────────────────────────┘ /// Plays a sound tone. -pub inline fn tone(frequency: u32, duration: u32, volume: u32, flags: u32) void { - asm volatile (" svc #8" - : - : [frequency] "{r0}" (frequency), - [duration] "{r1}" (duration), - [volume] "{r2}" (volume), - [flags] "{r3}" (flags), - ); +pub inline fn tone(frequency: u32, duration: u32, volume: u32, flags: ToneFlags) void { + if (comptime builtin.target.isWasm()) { + platform_specific.tone(frequency, duration, volume, flags); + } else { + @compileError("TODO"); + // asm volatile (" svc #8" + // : + // : [frequency] "{r0}" (frequency), + // [duration] "{r1}" (duration), + // [volume] "{r2}" (volume), + // [flags] "{r3}" (flags), + // ); + } } -pub const TONE_PULSE1: u32 = 0; -pub const TONE_PULSE2: u32 = 1; -pub const TONE_TRIANGLE: u32 = 2; -pub const TONE_NOISE: u32 = 3; -pub const TONE_MODE1: u32 = 0; -pub const TONE_MODE2: u32 = 4; -pub const TONE_MODE3: u32 = 8; -pub const TONE_MODE4: u32 = 12; -pub const TONE_PAN_LEFT: u32 = 16; -pub const TONE_PAN_RIGHT: u32 = 32; - // ┌───────────────────────────────────────────────────────────────────────────┐ // │ │ // │ Storage Functions │ // │ │ // └───────────────────────────────────────────────────────────────────────────┘ -/// Reads up to `size` bytes from persistent storage into the pointer `dest`. -pub inline fn diskr(dest: [*]u8, size: u32) u32 { - return asm volatile (" svc #9" - : [result] "={r0}" (-> u32), - : [dest] "{r0}" (dest), - [size] "{r1}" (size), - ); +pub const flash_page_size = 256; +pub const flash_page_count = 8000; + +/// Attempts to fill `dst`, returns the amount of bytes actually read +pub inline fn read_flash(offset: u32, dst: []u8) u32 { + if (comptime builtin.target.isWasm()) { + return platform_specific.read_flash(offset, dst.ptr, dst.len); + } else { + @compileError("TODO"); + } } -/// Writes up to `size` bytes from the pointer `src` into persistent storage. -pub inline fn diskw(src: [*]const u8, size: u32) u32 { - return asm volatile (" svc #10" - : [result] "={r0}" (-> u32), - : [src] "{r0}" (src), - [size] "{r1}" (size), - ); +pub inline fn write_flash_page(page: u16, src: [flash_page_size]u8) void { + if (comptime builtin.target.isWasm()) { + return platform_specific.write_flash_page(page, &src); + } else { + @compileError("TODO"); + // return asm volatile (" svc #10" + // : [result] "={r0}" (-> u32), + // : [src] "{r0}" (src), + // [size] "{r1}" (size), + // ); + } } // ┌───────────────────────────────────────────────────────────────────────────┐ @@ -232,20 +356,13 @@ pub inline fn diskw(src: [*]const u8, size: u32) u32 { /// Prints a message to the debug console. pub inline fn trace(x: []const u8) void { - asm volatile (" svc #11" - : - : [x_ptr] "{r0}" (x.ptr), - [x_len] "{r1}" (x.len), - ); -} - -// ┌───────────────────────────────────────────────────────────────────────────┐ -// │ │ -// │ Internal Use │ -// │ │ -// └───────────────────────────────────────────────────────────────────────────┘ - -pub export fn __return_thunk__() noreturn { - asm volatile (" svc #12"); - unreachable; + if (comptime builtin.target.isWasm()) { + platform_specific.trace(x.ptr, x.len); + } else { + asm volatile (" svc #11" + : + : [x_ptr] "{r0}" (x.ptr), + [x_len] "{r1}" (x.len), + ); + } } diff --git a/src/watch/404.html b/src/watch/404.html new file mode 100644 index 0000000..7b98053 --- /dev/null +++ b/src/watch/404.html @@ -0,0 +1,9 @@ + + + +404 + + +

404 not found

+ + diff --git a/src/watch/Reloader.zig b/src/watch/Reloader.zig new file mode 100644 index 0000000..da1edbe --- /dev/null +++ b/src/watch/Reloader.zig @@ -0,0 +1,203 @@ +const Reloader = @This(); +const std = @import("std"); +const builtin = @import("builtin"); +const ws = @import("ws"); + +const log = std.log.scoped(.watcher); +const ListenerFn = fn (self: *Reloader, path: []const u8, name: []const u8) void; +const Watcher = switch (builtin.target.os.tag) { + .linux => @import("watcher/LinuxWatcher.zig"), + .macos => @import("watcher/MacosWatcher.zig"), + .windows => @import("watcher/WindowsWatcher.zig"), + else => @compileError("unsupported platform"), +}; + +gpa: std.mem.Allocator, +ws_server: ws.Server, +zig_exe: []const u8, +out_dir_path: []const u8, +watcher: Watcher, + +clients_lock: std.Thread.Mutex = .{}, +clients: std.AutoArrayHashMapUnmanaged(*ws.Conn, void) = .{}, + +pub fn init( + gpa: std.mem.Allocator, + zig_exe: []const u8, + out_dir_path: []const u8, + in_dir_paths: []const []const u8, +) !Reloader { + const ws_server = try ws.Server.init(gpa, .{}); + + return .{ + .gpa = gpa, + .zig_exe = zig_exe, + .out_dir_path = out_dir_path, + .ws_server = ws_server, + .watcher = try Watcher.init(gpa, out_dir_path, in_dir_paths), + }; +} + +pub fn listen(self: *Reloader) !void { + try self.watcher.listen(self.gpa, self); +} + +pub fn onInputChange(self: *Reloader, path: []const u8, name: []const u8) void { + _ = name; + _ = path; + log.debug("re-building!", .{}); + const result = std.ChildProcess.run(.{ + .allocator = self.gpa, + .argv = &.{ self.zig_exe, "build" }, + }) catch |err| { + log.err("unable to run zig build: {s}", .{@errorName(err)}); + return; + }; + defer { + self.gpa.free(result.stdout); + self.gpa.free(result.stderr); + } + + if (result.stdout.len > 0) { + log.info("zig build stdout: {s}", .{result.stdout}); + } + + if (result.stderr.len > 0) { + std.debug.print("{s}\n\n", .{result.stderr}); + } else { + std.debug.print("File change triggered a successful build.\n", .{}); + } + + self.clients_lock.lock(); + defer self.clients_lock.unlock(); + + var idx: usize = 0; + while (idx < self.clients.entries.len) { + const conn = self.clients.entries.get(idx).key; + + const BuildCommand = struct { + command: []const u8 = "build", + err: []const u8, + }; + + const cmd: BuildCommand = .{ .err = result.stderr }; + + var buf = std.ArrayList(u8).init(self.gpa); + defer buf.deinit(); + + std.json.stringify(cmd, .{}, buf.writer()) catch { + log.err("unable to generate ws message", .{}); + return; + }; + + conn.write(buf.items) catch |err| { + log.debug("error writing to websocket: {s}", .{ + @errorName(err), + }); + self.clients.swapRemoveAt(idx); + continue; + }; + + idx += 1; + } +} +pub fn onOutputChange(self: *Reloader, path: []const u8, name: []const u8) void { + if (std.mem.indexOfScalar(u8, name, '.') == null) { + return; + } + log.debug("reload: {s}/{s}!", .{ path, name }); + + self.clients_lock.lock(); + defer self.clients_lock.unlock(); + + std.log.info("{d}", .{self.clients.entries.len}); + + var idx: usize = 0; + while (idx < self.clients.entries.len) { + const conn = self.clients.entries.get(idx).key; + + const msg_fmt = + \\{{ + \\ "command":"reload", + \\ "path":"{s}/{s}" + \\}} + ; + + var buf: [4096]u8 = undefined; + const msg = std.fmt.bufPrint(&buf, msg_fmt, .{ + path[self.out_dir_path.len..], + name, + }) catch { + log.err("unable to generate ws message", .{}); + return; + }; + + conn.write(msg) catch |err| { + log.debug("error writing to websocket: {s}", .{ + @errorName(err), + }); + self.clients.swapRemoveAt(idx); + continue; + }; + + idx += 1; + } +} + +pub fn handleWs(self: *Reloader, stream: std.net.Stream, h: [20]u8) void { + var buf = + ("HTTP/1.1 101 Switching Protocols\r\n" ++ + "Access-Control-Allow-Origin: *\r\n" ++ + "Upgrade: websocket\r\n" ++ + "Connection: upgrade\r\n" ++ + "Sec-Websocket-Accept: 0000000000000000000000000000\r\n\r\n").*; + + const key_pos = buf.len - 32; + _ = std.base64.standard.Encoder.encode(buf[key_pos .. key_pos + 28], h[0..]); + + stream.writeAll(&buf) catch @panic("bad"); + + // var conn = self.ws_server.newConn(stream); + const conn = self.gpa.create(ws.Conn) catch @panic("bad"); + conn.* = self.ws_server.newConn(stream); + + var context: Handler.Context = .{ .watcher = self }; + var handler = Handler.init(undefined, conn, &context) catch @panic("bad"); + self.ws_server.handle(Handler, &handler, conn); +} + +const Handler = struct { + conn: *ws.Conn, + context: *Context, + + const Context = struct { + watcher: *Reloader, + }; + + pub fn init(h: ws.Handshake, conn: *ws.Conn, context: *Context) !Handler { + _ = h; + + const watcher = context.watcher; + watcher.clients_lock.lock(); + defer watcher.clients_lock.unlock(); + try watcher.clients.put(context.watcher.gpa, conn, {}); + + return Handler{ + .conn = conn, + .context = context, + }; + } + + pub fn handle(self: *Handler, message: ws.Message) !void { + _ = self; + _ = message; + } + + pub fn close(self: *Handler) void { + log.debug("ws connection was closed\n", .{}); + const watcher = self.context.watcher; + watcher.clients_lock.lock(); + defer watcher.clients_lock.unlock(); + _ = watcher.clients.swapRemove(self.conn); + } +}; diff --git a/src/watch/main.zig b/src/watch/main.zig new file mode 100644 index 0000000..5d65e16 --- /dev/null +++ b/src/watch/main.zig @@ -0,0 +1,291 @@ +const std = @import("std"); +const fs = std.fs; +const mime = @import("mime"); +const Allocator = std.mem.Allocator; +const Reloader = @import("Reloader.zig"); +const not_found_html = @embedFile("404.html"); +const assert = std.debug.assert; + +const log = std.log.scoped(.server); +pub const std_options: std.Options = .{ + .log_level = .err, +}; + +const usage = + \\usage: zine serve [options] + \\ + \\options: + \\ -p [port] set the port number to listen on + \\ --root [path] directory of static files to serve + \\ +; + +var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){}; + +const Server = struct { + watcher: *Reloader, + zig_out_bin_dir: std.fs.Dir, + + fn deinit(s: *Server) void { + s.zig_out_bin_dir.close(); + s.* = undefined; + } + + fn handleRequest(s: *Server, req: *std.http.Server.Request) !bool { + var arena_impl = std.heap.ArenaAllocator.init(general_purpose_allocator.allocator()); + defer arena_impl.deinit(); + const arena = arena_impl.allocator(); + + var path = req.head.target; + + if (std.mem.indexOf(u8, path, "..")) |_| { + std.debug.print("'..' not allowed in URLs\n", .{}); + @panic("TODO: check if '..' is fine"); + } + + if (std.mem.endsWith(u8, path, "/")) { + path = try std.fmt.allocPrint(arena, "{s}{s}", .{ + path, + "index.html", + }); + } + + if (std.mem.eql(u8, path, "/ws")) { + var it = req.iterateHeaders(); + const key = while (it.next()) |header| { + if (std.ascii.eqlIgnoreCase(header.name, "sec-websocket-key")) { + break header.value; + } + } else { + log.debug("couldn't find key header!\n", .{}); + return false; + }; + + log.debug("key = '{s}'", .{key}); + + var hasher = std.crypto.hash.Sha1.init(.{}); + hasher.update(key); + hasher.update("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); + + var h: [20]u8 = undefined; + hasher.final(&h); + + const ws = try std.Thread.spawn(.{}, Reloader.handleWs, .{ + s.watcher, + req.server.connection.stream, + h, + }); + ws.detach(); + return true; + } + + path = path[0 .. std.mem.indexOfScalar(u8, path, '?') orelse path.len]; + + const ext = fs.path.extension(path); + const mime_type = mime.extension_map.get(ext) orelse + .@"application/octet-stream"; + + const file = s.zig_out_bin_dir.openFile(path[1..], .{}) catch |err| switch (err) { + error.FileNotFound => { + if (std.mem.endsWith(u8, req.head.target, "/")) { + try req.respond(not_found_html, .{ + .status = .not_found, + .extra_headers = &.{ + .{ .name = "content-type", .value = "text/html" }, + .{ .name = "connection", .value = "close" }, + .{ .name = "access-control-allow-origin", .value = "*" }, + }, + }); + log.debug("not found\n", .{}); + return false; + } else { + try appendSlashRedirect(arena, req); + return false; + } + }, + else => { + const message = try std.fmt.allocPrint( + arena, + "error accessing the resource: {s}", + .{ + @errorName(err), + }, + ); + try req.respond(message, .{ + .status = .internal_server_error, + .extra_headers = &.{ + .{ .name = "content-type", .value = "text/html" }, + .{ .name = "connection", .value = "close" }, + .{ .name = "access-control-allow-origin", .value = "*" }, + }, + }); + log.debug("error: {s}\n", .{@errorName(err)}); + return false; + }, + }; + defer file.close(); + + const contents = file.readToEndAlloc(arena, std.math.maxInt(usize)) catch |err| switch (err) { + error.IsDir => { + try appendSlashRedirect(arena, req); + return false; + }, + else => return err, + }; + + try req.respond(contents, .{ + .status = .ok, + .extra_headers = &.{ + .{ .name = "content-type", .value = @tagName(mime_type) }, + .{ .name = "connection", .value = "close" }, + .{ .name = "access-control-allow-origin", .value = "*" }, + }, + }); + log.debug("sent file\n", .{}); + return false; + } +}; + +fn appendSlashRedirect( + arena: std.mem.Allocator, + req: *std.http.Server.Request, +) !void { + const location = try std.fmt.allocPrint( + arena, + "{s}/", + .{req.head.target}, + ); + try req.respond(not_found_html, .{ + .status = .see_other, + .extra_headers = &.{ + .{ .name = "location", .value = location }, + .{ .name = "content-type", .value = "text/html" }, + .{ .name = "connection", .value = "close" }, + .{ .name = "access-control-allow-origin", .value = "*" }, + }, + }); + log.debug("append final slash redirect\n", .{}); +} + +pub fn main() !void { + const gpa = general_purpose_allocator.allocator(); + + const args = try std.process.argsAlloc(gpa); + + log.debug("log from server!", .{}); + + if (args.len < 2) fatal("missing subcommand argument", .{}); + + const cmd_name = args[1]; + if (std.mem.eql(u8, cmd_name, "serve")) { + return cmdServe(gpa, args[2..]); + } else { + fatal("unrecognized subcommand: '{s}'", .{cmd_name}); + } +} + +fn fatal(comptime format: []const u8, args: anytype) noreturn { + std.debug.print(format, args); + std.process.exit(1); +} + +fn cmdServe(gpa: Allocator, args: []const []const u8) !void { + var zig_out_bin_path: ?[]const u8 = null; + var input_dirs: std.ArrayListUnmanaged([]const u8) = .{}; + const zig_exe = args[0]; + + { + var i: usize = 1; + while (i < args.len) : (i += 1) { + const arg = args[i]; + if (std.mem.eql(u8, arg, "--zig-out-bin-dir")) { + i += 1; + if (i >= args.len) fatal("expected arg after '{s}'", .{arg}); + zig_out_bin_path = args[i]; + } else if (std.mem.eql(u8, arg, "--input-dir")) { + i += 1; + if (i >= args.len) fatal("expected arg after '{s}'", .{arg}); + try input_dirs.append(gpa, args[i]); + } else { + fatal("unrecognized arg: '{s}'", .{arg}); + } + } + } + + // ensure the path exists. without this, an empty website that + // doesn't generate a zig-out/ will cause the server to error out + try fs.cwd().makePath(zig_out_bin_path.?); + + var zig_out_bin_dir: fs.Dir = fs.cwd().openDir(zig_out_bin_path.?, .{ .iterate = true }) catch |e| + fatal("unable to open directory '{s}': {s}", .{ zig_out_bin_path.?, @errorName(e) }); + defer zig_out_bin_dir.close(); + + var watcher = try Reloader.init(gpa, zig_exe, zig_out_bin_path.?, input_dirs.items); + + var server: Server = .{ + .watcher = &watcher, + .zig_out_bin_dir = zig_out_bin_dir, + }; + defer server.deinit(); + + const watch_thread = try std.Thread.spawn(.{}, Reloader.listen, .{&watcher}); + watch_thread.detach(); + + try serve(&server, 2468); +} + +fn serve(s: *Server, listen_port: u16) !void { + const address = try std.net.Address.parseIp("127.0.0.1", listen_port); + var tcp_server = try address.listen(.{ + .reuse_port = true, + .reuse_address = true, + }); + defer tcp_server.deinit(); + + const server_port = tcp_server.listen_address.in.getPort(); + std.debug.assert(server_port == listen_port); + + std.debug.print("\x1b[2K\rSimulator live! Go to [link] to test your cartridge.\n", .{}); + + var buffer: [1024]u8 = undefined; + accept: while (true) { + const conn = try tcp_server.accept(); + + var http_server = std.http.Server.init(conn, &buffer); + + var became_websocket = false; + + defer { + if (!became_websocket) { + conn.stream.close(); + } else { + log.debug("request became websocket\n", .{}); + } + } + + while (http_server.state == .ready) { + var request = http_server.receiveHead() catch |err| { + if (err != error.HttpConnectionClosing) { + log.debug("connection error: {s}\n", .{@errorName(err)}); + } + continue :accept; + }; + + became_websocket = s.handleRequest(&request) catch |err| { + log.debug("failed request: {s}", .{@errorName(err)}); + continue :accept; + }; + if (became_websocket) continue :accept; + } + } +} + +/// like fs.path.dirname but ensures a final `/` +fn dirNameWithSlash(path: []const u8) []const u8 { + const d = fs.path.dirname(path).?; + if (d.len > 1) { + return path[0 .. d.len + 1]; + } else { + return "/"; + } +} diff --git a/src/watch/watcher/LinuxWatcher.zig b/src/watch/watcher/LinuxWatcher.zig new file mode 100644 index 0000000..95d41be --- /dev/null +++ b/src/watch/watcher/LinuxWatcher.zig @@ -0,0 +1,448 @@ +const LinuxWatcher = @This(); + +const std = @import("std"); +const Reloader = @import("../Reloader.zig"); + +const log = std.log.scoped(.watcher); + +notify_fd: std.posix.fd_t, + +/// active watch entries +watch_fds: std.AutoHashMapUnmanaged(std.posix.fd_t, WatchEntry) = .{}, + +/// direct descendant tracker +children_fds: std.AutoHashMapUnmanaged(std.posix.fd_t, std.ArrayListUnmanaged(std.posix.fd_t)) = .{}, + +/// inotify cookie tracker for move events +cookie_fds: std.AutoHashMapUnmanaged(u32, std.posix.fd_t) = .{}, + +const TreeKind = enum { input, output }; + +const WatchEntry = struct { + dir_path: []const u8, + name: []const u8, + kind: TreeKind, +}; + +pub fn init( + gpa: std.mem.Allocator, + out_dir_path: []const u8, + in_dir_paths: []const []const u8, +) !LinuxWatcher { + const notify_fd = try std.posix.inotify_init1(0); + var self: LinuxWatcher = .{ .notify_fd = notify_fd }; + _ = try self.addTree(gpa, .output, out_dir_path); + for (in_dir_paths) |p| { + _ = try self.addTree(gpa, .input, p); + } + return self; +} + +/// Register `child` with the `parent` +fn addChild( + self: *LinuxWatcher, + gpa: std.mem.Allocator, + parent: std.posix.fd_t, + child: std.posix.fd_t, +) !void { + const children = try self.children_fds.getOrPut(gpa, parent); + if (!children.found_existing) { + children.value_ptr.* = .{}; + } + try children.value_ptr.append(gpa, child); +} + +/// Remove `child` from the `parent`, if present +fn removeChild( + self: *LinuxWatcher, + parent: std.posix.fd_t, + child: std.posix.fd_t, +) ?std.posix.fd_t { + if (self.children_fds.getEntry(parent)) |entry| { + for (0.., entry.value_ptr.items) |i, fd| { + if (child == fd) { + return entry.value_ptr.swapRemove(i); + } + } + } + return null; +} + +/// Remove child identified by `name`, if present +fn removeChildByName( + self: *LinuxWatcher, + parent: std.posix.fd_t, + name: []const u8, +) ?std.posix.fd_t { + if (self.children_fds.getEntry(parent)) |entry| { + for (0.., entry.value_ptr.items) |i, fd| { + if (self.watch_fds.get(fd)) |data| { + if (std.mem.eql(u8, data.name, name)) { + return entry.value_ptr.swapRemove(i); + } + } + } + } + return null; +} + +/// Start tracking directory tree and returns the watch descriptor for `root_dir_path` +/// Register children within the tree +/// **NOTE**: caller is expected to register the returned watch fd as a child +fn addTree( + self: *LinuxWatcher, + gpa: std.mem.Allocator, + tree_kind: TreeKind, + root_dir_path: []const u8, +) !std.posix.fd_t { + var root_dir = try std.fs.cwd().openDir(root_dir_path, .{ .iterate = true }); + defer root_dir.close(); + const parent_fd = try self.addDir(gpa, tree_kind, root_dir_path); + + // tracker for fds associated with dir paths + // helps to track children within a recursive walk + var lookup = std.StringHashMap(std.posix.fd_t).init(gpa); + defer lookup.deinit(); + + try lookup.put(root_dir_path, parent_fd); + + var it = try root_dir.walk(gpa); + while (try it.next()) |entry| switch (entry.kind) { + else => continue, + .directory => { + const dir_path = try std.fs.path.join(gpa, &.{ root_dir_path, entry.path }); + const dir_fd = try self.addDir(gpa, tree_kind, dir_path); + const p_dir = std.fs.path.dirname(dir_path).?; + const p_fd = lookup.get(p_dir).?; + + try self.addChild(gpa, p_fd, dir_fd); + try lookup.put(dir_path, dir_fd); + }, + }; + + return parent_fd; +} + +fn addDir( + self: *LinuxWatcher, + gpa: std.mem.Allocator, + tree_kind: TreeKind, + dir_path: []const u8, +) !std.posix.fd_t { + const mask = Mask.all(&.{ + .IN_ONLYDIR, .IN_CLOSE_WRITE, + .IN_MOVE, .IN_MOVE_SELF, + .IN_CREATE, .IN_DELETE, + .IN_EXCL_UNLINK, + }); + const watch_fd = try std.posix.inotify_add_watch( + self.notify_fd, + dir_path, + mask, + ); + const name_copy = try gpa.dupe(u8, std.fs.path.basename(dir_path)); + try self.watch_fds.put(gpa, watch_fd, .{ + .dir_path = dir_path, + .name = name_copy, + .kind = tree_kind, + }); + log.debug("added {s} -> {}", .{ dir_path, watch_fd }); + return watch_fd; +} + +/// Explicitly stop watching a descriptor +/// **NOTE**: should only be called on an active `fd` +fn rmWatch( + self: *LinuxWatcher, + fd: std.posix.fd_t, +) void { + if (self.children_fds.getEntry(fd)) |entry| { + for (entry.value_ptr.items) |child_fd| { + self.rmWatch(child_fd); + } + self.children_fds.removeByPtr(entry.key_ptr); + } + std.posix.inotify_rm_watch(self.notify_fd, fd); +} + +/// Handle the start of the move process +/// Remove `name`-identified fd from children of `from_fd` +/// Register `cookie` for the moved fd for future identification +fn moveDirStart( + self: *LinuxWatcher, + gpa: std.mem.Allocator, + from_fd: std.posix.fd_t, + cookie: u32, + name: []const u8, +) !void { + const moved_fd = self.removeChildByName(from_fd, name).?; + + try self.cookie_fds.put( + gpa, + cookie, + moved_fd, + ); +} + +/// Handle the end of the move process and returns the resulting moved fd +/// Register the moved fd as a child of `to_fd` +fn moveDirEnd( + self: *LinuxWatcher, + gpa: std.mem.Allocator, + to_fd: std.posix.fd_t, + cookie: u32, + name: []const u8, +) !std.posix.fd_t { + const parent = self.watch_fds.get(to_fd).?; + + // known cookie - move within watched directories + if (self.cookie_fds.fetchRemove(cookie)) |entry| { + const moved_fd = entry.value; + + var watch_entry = self.watch_fds.getEntry(moved_fd).?.value_ptr; + gpa.free(watch_entry.name); + const name_copy = try gpa.dupe(u8, name); + watch_entry.name = name_copy; + watch_entry.kind = parent.kind; + + try self.updateDirPath(gpa, moved_fd, parent.dir_path); + try self.addChild(gpa, to_fd, moved_fd); + return moved_fd; + } else { // unknown cookie - move from the outside + const dir_path = try std.fs.path.join(gpa, &.{ parent.dir_path, name }); + const moved_fd = try self.addTree(gpa, parent.kind, dir_path); + try self.addChild(gpa, to_fd, moved_fd); + return moved_fd; + } +} + +/// Cascade path updates for `fd` and its children +fn updateDirPath( + self: *LinuxWatcher, + gpa: std.mem.Allocator, + fd: std.posix.fd_t, + parent_dir: []const u8, +) !void { + var data = self.watch_fds.getEntry(fd).?.value_ptr; + gpa.free(data.dir_path); + const dir_path = try std.fs.path.join(gpa, &.{ parent_dir, data.name }); + data.dir_path = dir_path; + + if (self.children_fds.getEntry(fd)) |entry| { + for (entry.value_ptr.items) |child_fd| { + try self.updateDirPath(gpa, child_fd, dir_path); + } + } +} + +/// Handle the post-move event +/// Remove stale cookie waiting for the `moved_fd`, if present +fn moveDirComplete( + self: *LinuxWatcher, + moved_fd: std.posix.fd_t, +) !void { + var it = self.cookie_fds.iterator(); + while (it.next()) |entry| { + // cookie for fd exists - moved outside the watched directory + if (entry.value_ptr.* == moved_fd) { + self.rmWatch(moved_fd); + self.cookie_fds.removeByPtr(entry.key_ptr); + break; + } + } +} + +/// Clean up `fd`-related bookkeeping +/// **NOTE**: expects `fd` to be a no-longer-watched descriptor +fn dropWatch( + self: *LinuxWatcher, + gpa: std.mem.Allocator, + fd: std.posix.fd_t, +) void { + if (self.watch_fds.fetchRemove(fd)) |entry| { + gpa.free(entry.value.dir_path); + gpa.free(entry.value.name); + } + + var it = self.children_fds.keyIterator(); + while (it.next()) |parent_fd| { + _ = self.removeChild(parent_fd.*, fd); + } + + if (self.children_fds.fetchRemove(fd)) |entry| { + log.warn("Stopping watch for {d} that has known children: {any}", .{ fd, entry.value }); + } +} + +pub fn listen( + self: *LinuxWatcher, + gpa: std.mem.Allocator, + reloader: *Reloader, +) !void { + const Event = std.os.linux.inotify_event; + const event_size = @sizeOf(Event); + while (true) { + var buffer: [event_size * 10]u8 = undefined; + const len = try std.posix.read(self.notify_fd, &buffer); + if (len < 0) @panic("notify fd read error"); + + var event_data = buffer[0..len]; + while (event_data.len > 0) { + const event: *Event = @alignCast(@ptrCast(event_data[0..event_size])); + const parent = self.watch_fds.get(event.wd).?; + event_data = event_data[event_size + event.len ..]; + + // std.debug.print("flags: ", .{}); + // Mask.debugPrint(event.mask); + // std.debug.print("for {s}/{?s}\n", .{ parent.dir_path, event.getName() }); + + if (Mask.is(event.mask, .IN_IGNORED)) { + log.debug("IGNORE {s}", .{parent.dir_path}); + self.dropWatch(gpa, event.wd); + continue; + } else if (Mask.is(event.mask, .IN_MOVE_SELF)) { + if (event.getName() == null) { + try self.moveDirComplete(event.wd); + } + continue; + } + + if (Mask.is(event.mask, .IN_ISDIR)) { + if (Mask.is(event.mask, .IN_CREATE)) { + const dir_name = event.getName().?; + const dir_path = try std.fs.path.join(gpa, &.{ + parent.dir_path, + dir_name, + }); + + log.debug("ISDIR CREATE {s}", .{dir_path}); + + const new_fd = try self.addTree(gpa, parent.kind, dir_path); + try self.addChild(gpa, event.wd, new_fd); + const data = self.watch_fds.get(new_fd).?; + switch (data.kind) { + .input => { + reloader.onInputChange(data.dir_path, ""); + }, + .output => { + reloader.onOutputChange(data.dir_path, ""); + }, + } + continue; + } else if (Mask.is(event.mask, .IN_MOVED_FROM)) { + log.debug("MOVING {s}/{s}", .{ parent.dir_path, event.getName().? }); + try self.moveDirStart(gpa, event.wd, event.cookie, event.getName().?); + continue; + } else if (Mask.is(event.mask, .IN_MOVED_TO)) { + log.debug("MOVED {s}/{s}", .{ parent.dir_path, event.getName().? }); + const moved_fd = try self.moveDirEnd(gpa, event.wd, event.cookie, event.getName().?); + const moved = self.watch_fds.get(moved_fd).?; + switch (moved.kind) { + .input => { + reloader.onInputChange(moved.dir_path, ""); + }, + .output => { + reloader.onOutputChange(moved.dir_path, ""); + }, + } + continue; + } + } else { + if (Mask.is(event.mask, .IN_CLOSE_WRITE) or + Mask.is(event.mask, .IN_MOVED_TO)) + { + switch (parent.kind) { + .input => { + const name = event.getName() orelse continue; + reloader.onInputChange(parent.dir_path, name); + }, + .output => { + const name = event.getName() orelse continue; + reloader.onOutputChange(parent.dir_path, name); + }, + } + } + } + } + } +} + +const Mask = struct { + pub const IN_ACCESS = 0x00000001; + pub const IN_MODIFY = 0x00000002; + pub const IN_ATTRIB = 0x00000004; + pub const IN_CLOSE_WRITE = 0x00000008; + pub const IN_CLOSE_NOWRITE = 0x00000010; + pub const IN_CLOSE = (IN_CLOSE_WRITE | IN_CLOSE_NOWRITE); + pub const IN_OPEN = 0x00000020; + pub const IN_MOVED_FROM = 0x00000040; + pub const IN_MOVED_TO = 0x00000080; + pub const IN_MOVE = (IN_MOVED_FROM | IN_MOVED_TO); + pub const IN_CREATE = 0x00000100; + pub const IN_DELETE = 0x00000200; + pub const IN_DELETE_SELF = 0x00000400; + pub const IN_MOVE_SELF = 0x00000800; + pub const IN_ALL_EVENTS = 0x00000fff; + + pub const IN_UNMOUNT = 0x00002000; + pub const IN_Q_OVERFLOW = 0x00004000; + pub const IN_IGNORED = 0x00008000; + + pub const IN_ONLYDIR = 0x01000000; + pub const IN_DONT_FOLLOW = 0x02000000; + pub const IN_EXCL_UNLINK = 0x04000000; + pub const IN_MASK_CREATE = 0x10000000; + pub const IN_MASK_ADD = 0x20000000; + + pub const IN_ISDIR = 0x40000000; + pub const IN_ONESHOT = 0x80000000; + + pub fn is(m: u32, comptime flag: std.meta.DeclEnum(Mask)) bool { + const f = @field(Mask, @tagName(flag)); + return (m & f) != 0; + } + + pub fn all(comptime flags: []const std.meta.DeclEnum(Mask)) u32 { + var result: u32 = 0; + inline for (flags) |f| result |= @field(Mask, @tagName(f)); + return result; + } + + pub fn debugPrint(m: u32) void { + const flags = .{ + .IN_ACCESS, + .IN_MODIFY, + .IN_ATTRIB, + .IN_CLOSE_WRITE, + .IN_CLOSE_NOWRITE, + .IN_CLOSE, + .IN_OPEN, + .IN_MOVED_FROM, + .IN_MOVED_TO, + .IN_MOVE, + .IN_CREATE, + .IN_DELETE, + .IN_DELETE_SELF, + .IN_MOVE_SELF, + .IN_ALL_EVENTS, + + .IN_UNMOUNT, + .IN_Q_OVERFLOW, + .IN_IGNORED, + + .IN_ONLYDIR, + .IN_DONT_FOLLOW, + .IN_EXCL_UNLINK, + .IN_MASK_CREATE, + .IN_MASK_ADD, + + .IN_ISDIR, + .IN_ONESHOT, + }; + inline for (flags) |f| { + if (is(m, f)) { + std.debug.print("{s} ", .{@tagName(f)}); + } + } + } +}; diff --git a/src/watch/watcher/MacosWatcher.zig b/src/watch/watcher/MacosWatcher.zig new file mode 100644 index 0000000..ac8e7ec --- /dev/null +++ b/src/watch/watcher/MacosWatcher.zig @@ -0,0 +1,124 @@ +const MacosWatcher = @This(); + +const std = @import("std"); +const Reloader = @import("../Reloader.zig"); +const c = @cImport({ + @cInclude("CoreServices/CoreServices.h"); +}); + +const log = std.log.scoped(.watcher); + +out_dir_path: []const u8, +in_dir_paths: []const []const u8, + +pub fn init( + gpa: std.mem.Allocator, + out_dir_path: []const u8, + in_dir_paths: []const []const u8, +) !MacosWatcher { + _ = gpa; + return .{ + .out_dir_path = out_dir_path, + .in_dir_paths = in_dir_paths, + }; +} + +pub fn callback( + streamRef: c.ConstFSEventStreamRef, + clientCallBackInfo: ?*anyopaque, + numEvents: usize, + eventPaths: ?*anyopaque, + eventFlags: ?[*]const c.FSEventStreamEventFlags, + eventIds: ?[*]const c.FSEventStreamEventId, +) callconv(.C) void { + _ = eventIds; + _ = eventFlags; + _ = streamRef; + const ctx: *Context = @alignCast(@ptrCast(clientCallBackInfo)); + + const paths: [*][*:0]u8 = @alignCast(@ptrCast(eventPaths)); + for (paths[0..numEvents]) |p| { + const path = std.mem.span(p); + log.debug("Changed: {s}\n", .{path}); + + const basename = std.fs.path.basename(path); + var base_path = path[0 .. path.len - basename.len]; + if (std.mem.endsWith(u8, base_path, "/")) + base_path = base_path[0 .. base_path.len - 1]; + + const is_out = std.mem.startsWith(u8, path, ctx.out_dir_path); + if (is_out) { + ctx.reloader.onOutputChange(base_path, basename); + } else { + ctx.reloader.onInputChange(base_path, basename); + } + } +} + +const Context = struct { + reloader: *Reloader, + out_dir_path: []const u8, +}; +pub fn listen( + self: *MacosWatcher, + gpa: std.mem.Allocator, + reloader: *Reloader, +) !void { + var macos_paths = try gpa.alloc(c.CFStringRef, self.in_dir_paths.len + 1); + defer gpa.free(macos_paths); + + macos_paths[0] = c.CFStringCreateWithCString( + null, + self.out_dir_path.ptr, + c.kCFStringEncodingUTF8, + ); + + for (self.in_dir_paths, macos_paths[1..]) |str, *ref| { + ref.* = c.CFStringCreateWithCString( + null, + str.ptr, + c.kCFStringEncodingUTF8, + ); + } + + const paths_to_watch: c.CFArrayRef = c.CFArrayCreate( + null, + @ptrCast(macos_paths.ptr), + @intCast(macos_paths.len), + null, + ); + + var ctx: Context = .{ + .reloader = reloader, + .out_dir_path = self.out_dir_path, + }; + + var stream_context: c.FSEventStreamContext = .{ .info = &ctx }; + const stream: c.FSEventStreamRef = c.FSEventStreamCreate( + null, + &callback, + &stream_context, + paths_to_watch, + c.kFSEventStreamEventIdSinceNow, + 0.05, + c.kFSEventStreamCreateFlagFileEvents, + ); + + c.FSEventStreamScheduleWithRunLoop( + stream, + c.CFRunLoopGetCurrent(), + c.kCFRunLoopDefaultMode, + ); + + if (c.FSEventStreamStart(stream) == 0) { + @panic("failed to start the event stream"); + } + + c.CFRunLoopRun(); + + c.FSEventStreamStop(stream); + c.FSEventStreamInvalidate(stream); + c.FSEventStreamRelease(stream); + + c.CFRelease(paths_to_watch); +} diff --git a/src/watch/watcher/WindowsWatcher.zig b/src/watch/watcher/WindowsWatcher.zig new file mode 100644 index 0000000..04e3e30 --- /dev/null +++ b/src/watch/watcher/WindowsWatcher.zig @@ -0,0 +1,226 @@ +const WindowsWatcher = @This(); + +const std = @import("std"); +const windows = std.os.windows; +const Reloader = @import("../Reloader.zig"); + +const log = std.log.scoped(.watcher); + +const Error = error{ InvalidHandle, QueueFailed, WaitFailed }; + +const CompletionKey = usize; +/// Values should be a multiple of `ReadBufferEntrySize` +const ReadBufferIndex = u32; +const ReadBufferEntrySize = 1024; + +const WatchEntry = struct { + kind: Kind, + + dir_path: [:0]const u8, + dir_handle: windows.HANDLE, + + overlap: windows.OVERLAPPED = std.mem.zeroes(windows.OVERLAPPED), + buf_idx: ReadBufferIndex, + + pub const Kind = enum { input, output }; +}; + +iocp_port: windows.HANDLE, +entries: std.AutoHashMap(CompletionKey, WatchEntry), +read_buffer: []u8, + +pub fn init( + gpa: std.mem.Allocator, + out_dir_path: []const u8, + in_dir_paths: []const []const u8, +) !WindowsWatcher { + var watcher = WindowsWatcher{ + .iocp_port = windows.INVALID_HANDLE_VALUE, + .entries = std.AutoHashMap(CompletionKey, WatchEntry).init(gpa), + .read_buffer = undefined, + }; + errdefer { + var iter = watcher.entries.valueIterator(); + while (iter.next()) |entry| { + windows.CloseHandle(entry.dir_handle); + gpa.free(entry.dir_path); + } + watcher.entries.deinit(); + } + + // Doubles as the number of WatchEntries + var comp_key: CompletionKey = 0; + + { + const out_path = try gpa.dupeZ(u8, out_dir_path); + try watcher.entries.putNoClobber( + comp_key, + try addPath(out_path, comp_key, .output, &watcher.iocp_port), + ); + comp_key += 1; + } + + for (in_dir_paths) |path| { + const in_path = try gpa.dupeZ(u8, path); + try watcher.entries.putNoClobber( + comp_key, + try addPath(in_path, comp_key, .input, &watcher.iocp_port), + ); + comp_key += 1; + } + + watcher.read_buffer = try gpa.alloc(u8, ReadBufferEntrySize * comp_key); + + // Here we need pointers to both the read_buffer and entry overlapped structs, + // which we can only do after setting up everything else. + watcher.entries.lockPointers(); + for (0..comp_key) |key| { + const entry = watcher.entries.getPtr(key).?; + if (windows.kernel32.ReadDirectoryChangesW( + entry.dir_handle, + @ptrCast(@alignCast(&watcher.read_buffer[entry.buf_idx])), + ReadBufferEntrySize, + @intFromBool(true), + windows.FILE_NOTIFY_CHANGE_LAST_WRITE | windows.FILE_NOTIFY_CHANGE_FILE_NAME | windows.FILE_NOTIFY_CHANGE_DIR_NAME, + null, + &entry.overlap, + null, + ) == 0) { + log.err("ReadDirectoryChanges error: {s}", .{@tagName(windows.kernel32.GetLastError())}); + return Error.QueueFailed; + } + } + return watcher; +} + +fn addPath( + path: [:0]const u8, + /// Assumed to increment by 1 after each invocation, starting at 0. + key: CompletionKey, + io: WatchEntry.Kind, + port: *windows.HANDLE, +) !WatchEntry { + const dir_handle = CreateFileA( + path, + windows.GENERIC_READ, // FILE_LIST_DIRECTORY, + windows.FILE_SHARE_READ | windows.FILE_SHARE_WRITE | windows.FILE_SHARE_DELETE, + null, + windows.OPEN_EXISTING, + windows.FILE_FLAG_BACKUP_SEMANTICS | windows.FILE_FLAG_OVERLAPPED, + null, + ); + if (dir_handle == windows.INVALID_HANDLE_VALUE) { + log.err( + "Unable to open directory {s}: {s}", + .{ path, @tagName(windows.kernel32.GetLastError()) }, + ); + return Error.InvalidHandle; + } + + if (port.* == windows.INVALID_HANDLE_VALUE) { + port.* = try windows.CreateIoCompletionPort(dir_handle, null, key, 0); + } else { + _ = try windows.CreateIoCompletionPort(dir_handle, port.*, key, 0); + } + + return .{ + .kind = io, + .dir_path = path, + .dir_handle = dir_handle, + .buf_idx = @intCast(ReadBufferEntrySize * key), + }; +} + +pub fn listen( + self: *WindowsWatcher, + gpa: std.mem.Allocator, + reloader: *Reloader, +) !void { + _ = gpa; + + var dont_care: struct { + bytes_transferred: windows.DWORD = undefined, + overlap: ?*windows.OVERLAPPED = undefined, + } = .{}; + + var key: CompletionKey = undefined; + while (true) { + // Waits here until any of the directory handles associated with the iocp port + // have been updated. + const wait_result = windows.GetQueuedCompletionStatus( + self.iocp_port, + &dont_care.bytes_transferred, + &key, + &dont_care.overlap, + windows.INFINITE, + ); + if (wait_result != .Normal) { + log.err("GetQueuedCompletionStatus error: {s}", .{@tagName(wait_result)}); + return Error.WaitFailed; + } + + const entry = self.entries.getPtr(key) orelse @panic("Invalid CompletionKey"); + + var info_iter = windows.FileInformationIterator(FILE_NOTIFY_INFORMATION){ + .buf = self.read_buffer[entry.buf_idx..][0..ReadBufferEntrySize], + }; + var path_buf: [windows.MAX_PATH]u8 = undefined; + while (info_iter.next()) |info| { + const filename: []const u8 = blk: { + const n = try std.unicode.utf16LeToUtf8( + &path_buf, + @as([*]u16, @ptrCast(&info.FileName))[0 .. info.FileNameLength / 2], + ); + break :blk path_buf[0..n]; + }; + + const args = .{ @tagName(entry.kind), entry.dir_path, filename }; + switch (info.Action) { + windows.FILE_ACTION_ADDED => log.debug("added ({s}) {s}/{s}", args), + windows.FILE_ACTION_REMOVED => log.debug("removed ({s}) {s}/{s}", args), + windows.FILE_ACTION_MODIFIED => log.debug("modified ({s}) {s}/{s}", args), + windows.FILE_ACTION_RENAMED_OLD_NAME => log.debug("renamed_old_name ({s}) {s}/{s}", args), + windows.FILE_ACTION_RENAMED_NEW_NAME => log.debug("renamed_new_name ({s}) {s}/{s}", args), + else => log.debug("Unknown Action ({s}) {s}/{s}", args), + } + + switch (entry.kind) { + .input => reloader.onInputChange(entry.dir_path, filename), + .output => reloader.onOutputChange(entry.dir_path, filename), + } + } + + // Re-queue the directory entry + if (windows.kernel32.ReadDirectoryChangesW( + entry.dir_handle, + @ptrCast(@alignCast(&self.read_buffer[entry.buf_idx])), + ReadBufferEntrySize, + @intFromBool(true), + windows.FILE_NOTIFY_CHANGE_LAST_WRITE | windows.FILE_NOTIFY_CHANGE_FILE_NAME | windows.FILE_NOTIFY_CHANGE_DIR_NAME, + null, + &entry.overlap, + null, + ) == 0) { + log.err("ReadDirectoryChanges error: {s}", .{@tagName(windows.kernel32.GetLastError())}); + return Error.QueueFailed; + } + } +} + +const FILE_NOTIFY_INFORMATION = extern struct { + NextEntryOffset: windows.DWORD, + Action: windows.DWORD, + FileNameLength: windows.DWORD, + /// Flexible array member + FileName: windows.WCHAR, +}; + +extern "kernel32" fn CreateFileA( + lpFileName: windows.LPCSTR, + dwDesiredAccess: windows.DWORD, + dwShareMode: windows.DWORD, + lpSecurityAttributes: ?*windows.SECURITY_ATTRIBUTES, + dwCreationDisposition: windows.DWORD, + dwFlagsAndAttributes: windows.DWORD, + hTemplateFile: ?windows.HANDLE, +) callconv(windows.WINAPI) windows.HANDLE;