From 97ab30c42da09659c154524d4ef5bd9f8e7652f3 Mon Sep 17 00:00:00 2001 From: sangrail Date: Mon, 13 Oct 2025 18:33:02 +0100 Subject: [PATCH 1/4] Add WebGPU triangle example with SDL3 and WebAssembly support - Renamed and moved the triangle rendering example using SDL3 and WebGPU. - Added game of life example with platform-specific OS handling for both JavaScript (web) and SDL3 (native). - Created build scripts for compiling to WebAssembly for web deployment. - Added HTML template for web application to load the WebAssembly module and JavaScript bindings. - Included necessary shader code for rendering a simple triangle. - Ensured proper memory management and surface configuration for both environments. --- .github/workflows/check.yml | 7 +- wgpu/sdl3/game-of-life/README.md | 829 +++++ wgpu/sdl3/game-of-life/build_web.bat | 13 + wgpu/sdl3/game-of-life/build_web.sh | 20 + wgpu/sdl3/game-of-life/img/conways-gol.png | Bin 0 -> 61584 bytes wgpu/sdl3/game-of-life/main.odin | 469 +++ wgpu/sdl3/game-of-life/os_desktop.odin | 117 + wgpu/sdl3/game-of-life/os_web.odin | 122 + wgpu/sdl3/game-of-life/shaders/compute.wgsl | 32 + wgpu/sdl3/game-of-life/shaders/render.wgsl | 33 + wgpu/sdl3/game-of-life/web/game_of_life.wasm | Bin 0 -> 161158 bytes wgpu/sdl3/game-of-life/web/index.html | 35 + wgpu/sdl3/game-of-life/web/odin.js | 2157 +++++++++++ wgpu/sdl3/game-of-life/web/wgpu.js | 3311 +++++++++++++++++ .../triangle}/build_web.bat | 0 .../triangle}/build_web.sh | 0 .../triangle}/main.odin | 0 .../triangle}/os_js.odin | 0 .../triangle}/os_sdl3.odin | 0 .../triangle}/web/index.html | 0 20 files changed, 7143 insertions(+), 2 deletions(-) create mode 100644 wgpu/sdl3/game-of-life/README.md create mode 100644 wgpu/sdl3/game-of-life/build_web.bat create mode 100755 wgpu/sdl3/game-of-life/build_web.sh create mode 100644 wgpu/sdl3/game-of-life/img/conways-gol.png create mode 100644 wgpu/sdl3/game-of-life/main.odin create mode 100644 wgpu/sdl3/game-of-life/os_desktop.odin create mode 100644 wgpu/sdl3/game-of-life/os_web.odin create mode 100644 wgpu/sdl3/game-of-life/shaders/compute.wgsl create mode 100644 wgpu/sdl3/game-of-life/shaders/render.wgsl create mode 100755 wgpu/sdl3/game-of-life/web/game_of_life.wasm create mode 100644 wgpu/sdl3/game-of-life/web/index.html create mode 100644 wgpu/sdl3/game-of-life/web/odin.js create mode 100644 wgpu/sdl3/game-of-life/web/wgpu.js rename wgpu/{sdl3-triangle => sdl3/triangle}/build_web.bat (100%) rename wgpu/{sdl3-triangle => sdl3/triangle}/build_web.sh (100%) rename wgpu/{sdl3-triangle => sdl3/triangle}/main.odin (100%) rename wgpu/{sdl3-triangle => sdl3/triangle}/os_js.odin (100%) rename wgpu/{sdl3-triangle => sdl3/triangle}/os_sdl3.odin (100%) rename wgpu/{sdl3-triangle => sdl3/triangle}/web/index.html (100%) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 60478de..e9c67fc 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -142,8 +142,11 @@ jobs: odin check wgpu/glfw-triangle -target:windows_amd64 $FLAGS odin check wgpu/glfw-triangle -target:js_wasm32 $FLAGS - odin check wgpu/sdl3-triangle -target:windows_amd64 $FLAGS - odin check wgpu/sdl3-triangle -target:js_wasm32 $FLAGS + odin check wgpu/sdl3/triangle -target:windows_amd64 $FLAGS + odin check wgpu/sdl3.triangle -target:js_wasm32 $FLAGS + + odin check wgpu/sdl3/game_of_life -target:windows_amd64 $FLAGS + odin check wgpu/sdl3/game_of_life -target:js_wasm32 $FLAGS odin check win32/game_of_life -target:windows_amd64 $FLAGS odin check win32/open_window -target:windows_amd64 $FLAGS diff --git a/wgpu/sdl3/game-of-life/README.md b/wgpu/sdl3/game-of-life/README.md new file mode 100644 index 0000000..454bc3c --- /dev/null +++ b/wgpu/sdl3/game-of-life/README.md @@ -0,0 +1,829 @@ +# Conway's Game of Life - Architecture Documentation + +## Overview + +This is a GPU-accelerated implementation of Conway's Game of Life using WebGPU, written in Odin. The application runs on both desktop (SDL3) and web (WebAssembly) platforms with a unified codebase. + +> **Note:** This is based on the SDL3 trinagle example and a port of the tutorial you can find here: https://codelabs.developers.google.com/your-first-webgpu-app#0. The original tutorial used JavaScript. + +**Key Features:** +- 32×32 cell grid (1,024 cells) +- 5 Hz simulation update rate (200ms intervals) +- Compute shader implementation of Game of Life rules +- Ping-pong buffer architecture for efficient GPU updates +- Simple, direct architecture following Odin idioms + +**Learning Goals:** +- Using Odin to implement something previously done in JavaScript +- Working with Odin WGPU bindings: https://pkg.odin-lang.org/vendor/wgpu/ +- Platform abstraction using Odin's build tags + +--- +![](img/conways-gol.png) + +--- + +## Project Structure + +The codebase follows a simple 3-file structure: + +```mermaid +graph LR + A[main.odin] --> B[os_desktop.odin
#+build !js] + A --> C[os_web.odin
#+build js] + + D[shaders/] --> E[compute.wgsl] + D --> F[render.wgsl] + + style A fill:#4CAF50,color:#fff + style B fill:#FF9800,color:#fff + style C fill:#2196F3,color:#fff + style D fill:#9C27B0,color:#fff +``` + +### File Overview + +| File | LOC | Purpose | +|------|-----|---------| +| `main.odin` | 466 | Core GPU logic, state management, rendering | +| `os_desktop.odin` | 104 | SDL3 platform layer (synchronous) | +| `os_web.odin` | 122 | WASM platform layer (asynchronous) | +| **Total** | **692** | **Complete application** | + +### Build Tags + +Odin's build tag system selects the appropriate platform file at compile time: + +- **Desktop build:** `odin build .` → Uses `os_desktop.odin` (excludes `os_web.odin`) +- **Web build:** `odin build . -target:js_wasm32` → Uses `os_web.odin` (excludes `os_desktop.odin`) + +--- + +## Architecture Overview + +```mermaid +flowchart TD + Start([Program Start]) --> Init[main.odin::main] + Init --> OSInit[os_init
Platform-specific] + OSInit --> GPUInit[init_gpu
Create instance & surface] + GPUInit --> ReqAdapter[os_request_adapter_and_device
Platform callbacks] + + ReqAdapter --> |Desktop: Sync| DesktopAdapter[on_adapter_sync] + ReqAdapter --> |Web: Async| WebAdapter[on_adapter callback] + + DesktopAdapter --> DesktopDevice[on_device_sync] + WebAdapter --> WebDevice[on_device callback] + + DesktopDevice --> Complete[complete_gpu_init] + WebDevice --> Complete + + Complete --> CreatePipelines[Create Pipelines
Render & Compute] + CreatePipelines --> CreateBuffers[Create Buffers
Vertex, Uniform, Storage] + CreateBuffers --> Run[os_run
Start event loop] + + Run --> |Desktop| DesktopLoop[SDL Event Loop
Calculate dt] + Run --> |Web| WebLoop[Browser step
Receives dt] + + DesktopLoop --> Frame[frame] + WebLoop --> Frame + + Frame --> Update[update_simulation
Accumulate dt] + Update --> Compute{Time for
update?} + Compute --> |Yes| ComputePass[run_compute_pass
Execute shader] + Compute --> |No| Skip[Skip compute] + ComputePass --> Render[Render Pass
Draw cells] + Skip --> Render + Render --> Present[Present Frame] + Present --> |Loop| DesktopLoop + Present --> |Loop| WebLoop + + style Init fill:#4CAF50,color:#fff + style Complete fill:#4CAF50,color:#fff + style DesktopLoop fill:#FF9800,color:#fff + style WebLoop fill:#2196F3,color:#fff + style Frame fill:#9C27B0,color:#fff +``` + +--- + +## main.odin - Core Application Logic + +The main file contains all GPU-related code and the application state. + +### Key Components + +#### 1. Configuration Constants +```odin +WIDTH :: 512 // Window width +HEIGHT :: 512 // Window height +GRID_SIZE :: 32 // 32×32 grid +WORKGROUP_SIZE :: 8 // Compute shader workgroup size +UPDATE_INTERVAL_MILLISECONDS :: 200.0 // 5 Hz update rate +``` + +#### 2. Application State +```odin +App_State :: struct { + ctx: runtime.Context + + // WebGPU core + instance, surface, adapter, device: wgpu.* + queue: wgpu.Queue + config: wgpu.SurfaceConfiguration + + // Pipelines & layouts + pipeline_layout, bind_group_layout: wgpu.* + render_module, compute_module: wgpu.ShaderModule + render_pipeline: wgpu.RenderPipeline + compute_pipeline: wgpu.ComputePipeline + + // Buffers + vertex_buffer, uniform_buffer: wgpu.Buffer + cell_state_storage: [2]wgpu.Buffer // Ping-pong + bind_groups: [2]wgpu.BindGroup + + // Simulation + step_index: u64 + did_compute, do_update: bool + last_tick: time.Tick + accumulator: time.Duration +} +``` + +#### 3. Initialization Flow + +```mermaid +sequenceDiagram + participant M as main() + participant OS as os_* + participant GPU as GPU Init + + M->>OS: os_init() + M->>GPU: init_gpu() + GPU->>GPU: Create instance + GPU->>OS: os_get_surface() + OS-->>GPU: Surface + GPU->>OS: os_request_adapter_and_device() + + Note over OS: Platform-specific async/sync + + OS->>GPU: complete_gpu_init(device) + GPU->>GPU: create_bind_group_layout() + GPU->>GPU: create_render_pipeline() + GPU->>GPU: create_compute_pipeline() + GPU->>GPU: create_buffers_and_bind_groups() + GPU->>OS: os_run() +``` + +#### 4. Pipeline Creation + +**Render Pipeline:** +- Vertex shader positions cell instances in grid +- Fragment shader colors cells (green/black) +- Reads from storage buffer to determine cell state + +**Compute Pipeline:** +- Implements Conway's Game of Life rules +- Reads from one storage buffer (current state) +- Writes to another storage buffer (next state) +- Runs at 5 Hz (every 200ms) + +```mermaid +graph LR + A[Vertex Buffer
Cell Quad] --> B[Render Pipeline] + C[Uniform Buffer
Grid Size] --> B + D[Storage Buffer A
Cell States] --> B + D --> E[Compute Pipeline] + E --> F[Storage Buffer B
New States] + F --> B + + style B fill:#4CAF50,color:#fff + style E fill:#9C27B0,color:#fff +``` + +#### 5. Buffer Architecture (Ping-Pong) + +```mermaid +graph TD + subgraph "Frame N" + A[Storage Buffer A
Current State] --> C[Compute Shader] + C --> B[Storage Buffer B
Next State] + B --> R1[Render Pipeline
Read from B] + end + + subgraph "Frame N+1" + B2[Storage Buffer B
Current State] --> C2[Compute Shader] + C2 --> A2[Storage Buffer A
Next State] + A2 --> R2[Render Pipeline
Read from A] + end + + R1 -.-> B2 + R2 -.-> A + + style C fill:#9C27B0,color:#fff + style C2 fill:#9C27B0,color:#fff +``` + +**Key insight:** +- Compute writes to buffer `(step + 1) % 2` +- Render reads from buffer `(step + 1) % 2` (the latest computed state) + +#### 6. Simulation Timing + +```mermaid +flowchart LR + A[Frame Called] --> B[update_simulation dt] + B --> C{accumulator >= 200ms?} + C --> |Yes| D[Set do_update = true
Reset accumulator] + C --> |No| E[Set do_update = false] + D --> F[run_compute_pass] + E --> F + F --> G{do_update?} + G --> |Yes| H[Execute Compute Shader
Increment step_index] + G --> |No| I[Skip compute] + H --> J[Render Pass] + I --> J +``` + +**Desktop timing:** `dt` calculated using `SDL.GetPerformanceCounter()` +**Web timing:** `dt` provided by browser (seconds since last frame) + +#### 7. Frame Rendering + +```odin +frame :: proc "c" (dt: f32) { + update_simulation(dt) + + // Acquire surface texture + // Create command encoder + + run_compute_pass(encoder) // Conditional based on timing + + // Render pass + // - Clear to dark blue + // - Bind render pipeline + // - Bind group: (step + 1) % 2 (read latest) + // - Draw instances: GRID_SIZE * GRID_SIZE + + // Submit and present + + if did_compute { + step_index += 1 + } +} +``` + +--- + +## os_desktop.odin - SDL3 Platform Layer + +Provides synchronous initialization and blocking event loop for desktop platforms. + +### Key Components + +```mermaid +flowchart TD + A[os_init] --> B[SDL.Init] + B --> C[SDL.CreateWindow] + + D[os_get_surface] --> E[sdl3glue.GetSurface] + + F[os_get_framebuffer_size] --> G[SDL.GetWindowSizeInPixels] + + H[os_request_adapter_and_device] --> I[wgpu.InstanceRequestAdapter
callback: on_adapter_sync] + I --> J[on_adapter_sync
fires immediately] + J --> K[wgpu.AdapterRequestDevice
callback: on_device_sync] + K --> L[on_device_sync
fires immediately] + L --> M[complete_gpu_init] + + N[os_run] --> O[SDL Event Loop] + O --> P[Calculate dt
SDL.GetPerformanceCounter] + P --> Q[SDL.PollEvent] + Q --> R{Event Type} + R --> |QUIT| S[Exit] + R --> |KEY_DOWN ESCAPE| S + R --> |WINDOW_RESIZED| T[resize] + R --> |Continue| U[frame dt] + T --> U + U --> P + S --> V[cleanup
SDL.DestroyWindow
SDL.Quit] + + style A fill:#FF9800,color:#fff + style N fill:#FF9800,color:#fff + style J fill:#4CAF50,color:#fff + style L fill:#4CAF50,color:#fff +``` + +### Synchronous Callbacks + +On desktop, WebGPU callbacks fire **immediately** (synchronously): + +```odin +wgpu.InstanceRequestAdapter(...) +// Callback fires before this line executes +// adapter is already available +``` + +### Event Loop + +```odin +os_run :: proc() { + last := SDL.GetPerformanceCounter() + + for running { + now = SDL.GetPerformanceCounter() + dt = f32((now - last) * 1000) / f32(SDL.GetPerformanceFrequency()) + last = now + + for SDL.PollEvent(&event) { + #partial switch event.type { + case .QUIT: running = false + case .KEY_DOWN: + if event.key.scancode == .ESCAPE { + running = false + } + case .WINDOW_RESIZED: resize() + } + } + + frame(dt) // dt in milliseconds + } +} +``` + +--- + +## os_web.odin - WASM Platform Layer + +Provides asynchronous initialization and browser-driven event loop for web platforms. + +### Key Components + +```mermaid +flowchart TD + A[os_init] --> B[js.add_window_event_listener
Resize] + + C[os_get_surface] --> D[wgpu.InstanceCreateSurface
Canvas selector: #wgpu-canvas] + + E[os_get_framebuffer_size] --> F[js.get_bounding_client_rect
body] + F --> G[Apply device_pixel_ratio] + + H[os_request_adapter_and_device] --> I[wgpu.InstanceRequestAdapter
inline callback: on_adapter] + I -.Async.-> J[on_adapter
fires when ready] + J --> K[wgpu.AdapterRequestDevice
inline callback: on_device] + K -.Async.-> L[on_device
fires when ready] + L --> M[complete_gpu_init] + + N[os_run] --> O[Set device_ready = true] + + P[step dt
@export] --> Q{device_ready?} + Q --> |No| R[return true] + Q --> |Yes| S[frame dt] + S --> T[return true] + + U[size_callback] --> V{device_ready?} + V --> |Yes| W[resize] + V --> |No| X[return] + + style A fill:#2196F3,color:#fff + style N fill:#2196F3,color:#fff + style P fill:#2196F3,color:#fff + style J fill:#4CAF50,color:#fff + style L fill:#4CAF50,color:#fff +``` + +### Asynchronous Callbacks + +On web, WebGPU callbacks fire **asynchronously** (when browser completes operation): + +```odin +wgpu.InstanceRequestAdapter(...) +// Function returns immediately +// Callback fires later (100-500ms typical) +``` + +**Critical:** Callbacks must be defined **inline** in the same scope for WASM: + +```odin +os_request_adapter_and_device :: proc() { + wgpu.InstanceRequestAdapter( + state.instance, + &{compatibleSurface = state.surface}, + {callback = on_adapter}, + ) + + // Define callback inline + on_adapter :: proc "c" (...) { + context = state.ctx + // ... adapter handling + wgpu.AdapterRequestDevice(..., {callback = on_device}) + + // Nested inline callback + on_device :: proc "c" (...) { + context = state.ctx + // ... device handling + complete_gpu_init(device) + } + } +} +``` + +### Browser Integration + +```mermaid +sequenceDiagram + participant B as Browser + participant W as WASM Module + participant G as GPU + + B->>W: Load & Initialize + W->>G: Request Adapter + Note over W,G: Async - returns immediately + + G-->>W: on_adapter callback + W->>G: Request Device + Note over W,G: Async - returns immediately + + G-->>W: on_device callback + W->>W: complete_gpu_init + W->>W: os_run (set ready flag) + + loop 60 FPS + B->>W: step(dt) + W->>W: frame(dt) + W-->>B: return true + end +``` + +### Exported Functions + +```odin +@(export) +step :: proc(dt: f32) -> bool { + context = state.ctx + + if !device_ready { + return true // Still initializing + } + + frame(dt) // dt in seconds + return true +} + +@(fini) +cleanup_on_exit :: proc "contextless" () { + cleanup() + js.remove_window_event_listener(.Resize, nil, size_callback) +} +``` + +--- + +## Shader Architecture + +### compute.wgsl - Game of Life Rules + +```mermaid +flowchart LR + A[Invocation ID
x, y] --> B[Read 8 Neighbors
from cellStateIn] + B --> C[Count Active
Neighbors] + C --> D{Current Cell
Active?} + D --> |Yes| E{neighbors == 2
or 3?} + D --> |No| F{neighbors == 3?} + E --> |Yes| G[Write 1
cellStateOut] + E --> |No| H[Write 0
cellStateOut] + F --> |Yes| G + F --> |No| H + + style C fill:#9C27B0,color:#fff + style G fill:#4CAF50,color:#fff + style H fill:#f44336,color:#fff +``` + +**Workgroup Configuration:** +- Size: 8×8 (64 threads per workgroup) +- Dispatch: 4×4 workgroups for 32×32 grid +- Total: 1,024 parallel executions + +**Bindings:** +- `@binding(0)`: Uniform buffer (grid size) +- `@binding(1)`: Read-only storage (current cell states) +- `@binding(2)`: Storage (output cell states) + +### render.wgsl - Cell Visualization + +```mermaid +flowchart TD + A[Instance ID] --> B[Calculate Grid Position
x = id % GRID_SIZE
y = id / GRID_SIZE] + B --> C[Read Cell State
cellStateIn instance] + C --> D[Scale Vertex Position
by 1/GRID_SIZE] + D --> E[Translate to Grid Cell
offset by x, y] + E --> F[Output Position] + + G[Fragment Shader] --> H{Cell Active?} + H --> |Yes| I[Green: 0, 0.6, 0] + H --> |No| J[Black: 0, 0, 0] + + style F fill:#4CAF50,color:#fff + style I fill:#4CAF50,color:#fff + style J fill:#424242,color:#fff +``` + +**Vertex Shader:** +- Input: Cell quad vertices (-0.8 to 0.8) +- Instance rendering: Draw GRID_SIZE² instances +- Each instance represents one cell + +**Fragment Shader:** +- Simple color selection based on cell state +- Active cells: Green `(0, 0.6, 0)` +- Inactive cells: Black `(0, 0, 0)` + +--- + +## Platform Differences + +### Desktop (SDL3) + +| Aspect | Implementation | +|--------|----------------| +| **Windowing** | SDL3 native window | +| **Event Loop** | Blocking `SDL.PollEvent()` | +| **Frame Timing** | Manual via `SDL.GetPerformanceCounter()` | +| **WebGPU Init** | Synchronous callbacks | +| **Delta Time** | Milliseconds (calculated) | +| **Exit Handling** | Window close or Escape key | + +### Web (WASM) + +| Aspect | Implementation | +|--------|----------------| +| **Windowing** | HTML5 Canvas (`#wgpu-canvas`) | +| **Event Loop** | Browser `requestAnimationFrame` calls `step()` | +| **Frame Timing** | Provided by browser | +| **WebGPU Init** | Asynchronous callbacks (inline) | +| **Delta Time** | Seconds (from browser) | +| **Exit Handling** | Browser tab close | + +### Callback Timing Diagram + +```mermaid +sequenceDiagram + participant C as Caller + participant W as WebGPU + participant CB as Callback + + Note over C,CB: DESKTOP (Synchronous) + C->>W: RequestAdapter + W->>CB: Callback fires immediately + CB->>C: Returns to caller + Note over C: Adapter ready here + + Note over C,CB: WEB (Asynchronous) + C->>W: RequestAdapter + W-->>C: Returns immediately + Note over C: Adapter NOT ready yet + Note over W: Browser processes... + W->>CB: Callback fires later + Note over CB: Adapter ready NOW +``` + +--- + +## Build System + +### Desktop Build + +```bash +odin build . -out:game-of-life -vet -strict-style -vet-tabs -disallow-do -warnings-as-errors +./sdl3-Game-of-life +``` + +**What happens:** +1. Compiler includes `main.odin` and `os_desktop.odin` +2. `os_web.odin` excluded via `#+build js` tag +3. Links SDL3 and WebGPU native libraries +4. Creates native executable + +### Web Build + +```bash +odin build . -target:js_wasm32 -out:web/game_of_life.wasm +``` + +**What happens:** +1. Compiler includes `main.odin` and `os_web.odin` +2. `os_desktop.odin` excluded via `#+build !js` tag +3. Generates WebAssembly module +4. Exports `step()` function for browser + +### Build Tags Explanation + +```odin +// os_desktop.odin +#+build !js // Include when NOT building for JavaScript +package main + +// os_web.odin +#+build js // Include ONLY when building for JavaScript +package main +``` + +Build tags are **file-level** in Odin - you cannot use them inline within functions. + +--- + +## Data Flow + +### Initialization Flow + +```mermaid +graph TD + A[main] --> B[os_init] + B --> C[init_gpu] + C --> D[Create instance] + D --> E[os_get_surface] + E --> F[os_request_adapter_and_device] + + F --> G{Platform?} + G --> |Desktop| H[Sync callbacks] + G --> |Web| I[Async callbacks] + + H --> J[complete_gpu_init] + I --> J + + J --> K[Configure surface] + K --> L[create_bind_group_layout] + L --> M[create_render_pipeline] + M --> N[create_compute_pipeline] + N --> O[create_buffers_and_bind_groups] + O --> P[Initialize with random cells] + P --> Q[os_run] + + style A fill:#4CAF50,color:#fff + style J fill:#4CAF50,color:#fff + style Q fill:#FF9800,color:#fff +``` + +### Frame Flow + +```mermaid +graph TD + A[frame dt] --> B[update_simulation] + B --> C[accumulator += dt] + C --> D{accumulator >= 200ms?} + D --> |Yes| E[do_update = true
accumulator = 0] + D --> |No| F[do_update = false] + + E --> G[Get surface texture] + F --> G + G --> H[Create command encoder] + + H --> I[run_compute_pass] + I --> J{do_update?} + J --> |Yes| K[Begin compute pass] + J --> |No| L[Skip] + + K --> M[Set compute pipeline] + M --> N[Bind group: step % 2] + N --> O[Dispatch workgroups] + O --> P[did_compute = true] + + P --> Q[Begin render pass] + L --> Q + Q --> R[Clear to dark blue] + R --> S[Set render pipeline] + S --> T[Bind group: step+1 % 2] + T --> U[Set vertex buffer] + U --> V[Draw GRID_SIZE²] + V --> W[End render pass] + + W --> X[Submit & Present] + X --> Y{did_compute?} + Y --> |Yes| Z[step_index++] + Y --> |No| AA[Keep step_index] + + style E fill:#4CAF50,color:#fff + style K fill:#9C27B0,color:#fff + style P fill:#9C27B0,color:#fff + style Z fill:#4CAF50,color:#fff +``` + +--- + +## Performance Characteristics + +### GPU Workload + +| Operation | Frequency | GPU Load | +|-----------|-----------|----------| +| **Compute Shader** | 5 Hz | 1,024 threads (32×32 grid) | +| **Render Pass** | 60 FPS | 1,024 instances, 6 vertices each | +| **Buffer Updates** | 0 Hz | No CPU→GPU transfers after init | + +### Memory Usage + +| Resource | Size | Count | Total | +|----------|------|-------|-------| +| **Vertex Buffer** | 48 bytes | 1 | 48 B | +| **Uniform Buffer** | 8 bytes | 1 | 8 B | +| **Storage Buffers** | 4 KB | 2 | 8 KB | +| **Bind Groups** | - | 2 | - | +| **Pipelines** | - | 2 | - | + +**Total GPU memory:** ~8 KB (excluding shader bytecode and pipeline state) + +### CPU Load + +| Platform | Per Frame | Notes | +|----------|-----------|-------| +| **Desktop** | Minimal | Event polling, dt calculation | +| **Web** | Minimal | Browser calls `step()` | + +**Key insight:** After initialization, all simulation logic runs on GPU. CPU only submits command buffers. + +--- + +## Troubleshooting + +### Desktop Issues + +**Problem:** Window opens but cells don't update +**Solution:** Check that delta time is being calculated (not passing `0`) + +**Problem:** Window doesn't open +**Solution:** Ensure SDL3 is installed and linked correctly + +**Problem:** WebGPU errors +**Solution:** Check that your GPU supports WebGPU (Metal on macOS, D3D12 on Windows, Vulkan on Linux) + +### Web Issues + +**Problem:** Black screen, no errors +**Solution:** Check browser console - likely async callbacks not firing + +**Problem:** "WebGPU not supported" +**Solution:** Use Chrome/Edge with WebGPU enabled, serve over `http://127.0.0.1` (not `file://` or IPV6 [::]: ) + +**Problem:** Callbacks never fire +**Solution:** Ensure callbacks are defined **inline** in the same scope as registration + +**Problem:** Canvas size wrong +**Solution:** Check `device_pixel_ratio` and canvas CSS + +### Common Issues + +**Problem:** Compilation errors with build tags +**Solution:** Build tags must be at top of file, before `package` declaration + +**Problem:** Linking errors +**Solution:** Ensure `vendor:wgpu`, `vendor:sdl3` are available in your Odin installation + +--- + +## Future Enhancements + +Potential improvements while maintaining simplicity: + +1. **Interactive Controls** + - Mouse click to toggle cells + - Space bar to pause/resume + - R key to randomize grid (matchin the Win32 example) + +2. **Adjustable Parameters** + - Grid size selection (16×16, 32×32, 64×64) + - Update rate slider + - Color themes + +3. **Patterns** + - Load predefined patterns (glider, blinker, etc.) + - Save/load grid states + +4. **Performance** + - Larger grids (128×128, 256×256) + - Multiple compute passes per frame + - Benchmarking mode + +**Constraint:** Keep 3-file structure, avoid over-abstraction + +--- + +## References + +- **Original Tutorial:** https://codelabs.developers.google.com/your-first-webgpu-app +- **Odin Language:** https://odin-lang.org +- **Odin WGPU Bindings:** https://pkg.odin-lang.org/vendor/wgpu/ +- **SDL3:** https://wiki.libsdl.org/SDL3/ +- **WebGPU Spec:** https://gpuweb.github.io/gpuweb/ +- **Conway's Game of Life:** https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life + +--- + +## License + +This code is part of the Odin examples repository. + +--- + +*Last updated: October 13, 2025* + diff --git a/wgpu/sdl3/game-of-life/build_web.bat b/wgpu/sdl3/game-of-life/build_web.bat new file mode 100644 index 0000000..50d77df --- /dev/null +++ b/wgpu/sdl3/game-of-life/build_web.bat @@ -0,0 +1,13 @@ +REM NOTE: changing this requires changing the same values in the `web/index.html`. +set INITIAL_MEMORY_PAGES=2000 +set MAX_MEMORY_PAGES=65536 + +set PAGE_SIZE=65536 +set /a INITIAL_MEMORY_BYTES=%INITIAL_MEMORY_PAGES% * %PAGE_SIZE% +set /a MAX_MEMORY_BYTES=%MAX_MEMORY_PAGES% * %PAGE_SIZE% + +call odin.exe build . -target:js_wasm32 -out:web/game_of_life.wasm -o:size -extra-linker-flags:"--export-table --import-memory --initial-memory=%INITIAL_MEMORY_BYTES% --max-memory=%MAX_MEMORY_BYTES%" + +for /f "delims=" %%i in ('odin.exe root') do set "ODIN_ROOT=%%i" +copy "%ODIN_ROOT%\vendor\wgpu\wgpu.js" "web\wgpu.js" +copy "%ODIN_ROOT%\core\sys\wasm\js\odin.js" "web\odin.js" diff --git a/wgpu/sdl3/game-of-life/build_web.sh b/wgpu/sdl3/game-of-life/build_web.sh new file mode 100755 index 0000000..bd5f419 --- /dev/null +++ b/wgpu/sdl3/game-of-life/build_web.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +set -euxo pipefail + +# NOTE: changing this requires changing the same values in the `web/index.html`. +INITIAL_MEMORY_PAGES=2000 +MAX_MEMORY_PAGES=65536 + +INITIAL_MEMORY_BYTES=$(expr $INITIAL_MEMORY_PAGES \* $MAX_MEMORY_PAGES) +MAX_MEMORY_BYTES=$(expr $MAX_MEMORY_PAGES \* $MAX_MEMORY_PAGES) + +ODIN_ROOT=$(odin root) +ODIN_JS="$ODIN_ROOT/core/sys/wasm/js/odin.js" +WGPU_JS="$ODIN_ROOT/vendor/wgpu/wgpu.js" + +odin build . -target:js_wasm32 -out:web/game_of_life.wasm -o:size \ + -extra-linker-flags:"--export-table --import-memory --initial-memory=$INITIAL_MEMORY_BYTES --max-memory=$MAX_MEMORY_BYTES" + +cp $ODIN_JS web/odin.js +cp $WGPU_JS web/wgpu.js diff --git a/wgpu/sdl3/game-of-life/img/conways-gol.png b/wgpu/sdl3/game-of-life/img/conways-gol.png new file mode 100644 index 0000000000000000000000000000000000000000..235ad15eece864647a17e6f53ad91fd0cf7584db GIT binary patch literal 61584 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⋘=q2M1sUu`8@#!=MqP{-mDP6O%HE$vS3nX@_ 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 literal 0 HcmV?d00001 diff --git a/wgpu/sdl3/game-of-life/main.odin b/wgpu/sdl3/game-of-life/main.odin new file mode 100644 index 0000000..23d95c1 --- /dev/null +++ b/wgpu/sdl3/game-of-life/main.odin @@ -0,0 +1,469 @@ +// Conway's Game of Life - WebGPU/ODIN Implementation +// Ported to SDL3/WASM Odin from : https://codelabs.developers.google.com/your-first-webgpu-app#0 +// by Jason Coleman, 2025 +// ============================================== +// GPU-accelerated cellular automaton running on both desktop (SDL3) and web (WASM). +// +// Architecture: +// - Single file contains all GPU initialisation, pipelines, buffers, simulation, and rendering +// - Platform-specific code separated in os_desktop.odin and os_web.odin (selected by build tags) +// - Compute shader implements Game of Life rules with ping-pong buffers +// - Fixed 5 Hz simulation update rate (200ms intervals) +// +// Build: +// Desktop: odin build . -out:game-of-life -vet -strict-style -vet-tabs -disallow-do -warnings-as-errors +// Web: odin build . -target:js_wasm32 -out:web/game_of_life.wasm +// +package main + +import "base:runtime" +import "core:fmt" +import "core:math/rand" +import "core:time" +import "vendor:wgpu" + +//============================================================================= +// Configuration Constants +//============================================================================= + +WIDTH :: 512 +HEIGHT :: 512 +GRID_SIZE :: 32 +WORKGROUP_SIZE :: 8 +UPDATE_INTERVAL_MILLISECONDS :: 200.0 // 5 Hz update rate + +//============================================================================= +// Application State +//============================================================================= + +App_State :: struct { + // Context + ctx: runtime.Context, + + // WebGPU core + instance: wgpu.Instance, + surface: wgpu.Surface, + adapter: wgpu.Adapter, + device: wgpu.Device, + queue: wgpu.Queue, + config: wgpu.SurfaceConfiguration, + + // Pipelines & layouts + pipeline_layout: wgpu.PipelineLayout, + bind_group_layout: wgpu.BindGroupLayout, + render_module: wgpu.ShaderModule, + compute_module: wgpu.ShaderModule, + render_pipeline: wgpu.RenderPipeline, + compute_pipeline: wgpu.ComputePipeline, + + // Buffers + vertex_buffer: wgpu.Buffer, + vertex_count: u32, + vertex_buffer_size: u64, + uniform_buffer: wgpu.Buffer, + cell_state_storage: [2]wgpu.Buffer, + bind_groups: [2]wgpu.BindGroup, + + // Simulation state + step_index: u64, + did_compute: bool, + do_update: bool, + + // Timing + last_tick: time.Tick, + accumulator: time.Duration, +} + +state: App_State + +//============================================================================= +// Entry Point +//============================================================================= + +main :: proc() { + state.ctx = context + os_init() + init_gpu() +} + +//============================================================================= +// GPU Initialisation +//============================================================================= + +init_gpu :: proc() { + state.instance = wgpu.CreateInstance(nil) + if state.instance == nil { + panic("WebGPU is not supported") + } + + state.surface = os_get_surface(state.instance) + + // Platform-specific initialisation (os_desktop.odin or os_web.odin) + os_request_adapter_and_device() +} + +// Called by platform code after device is acquired +complete_gpu_init :: proc(device: wgpu.Device) { + state.device = device + state.queue = wgpu.DeviceGetQueue(state.device) + + // Configure surface + width, height := os_get_framebuffer_size() + state.config = wgpu.SurfaceConfiguration { + device = state.device, + usage = {.RenderAttachment}, + format = .BGRA8Unorm, + width = width, + height = height, + presentMode = .Fifo, + alphaMode = .Opaque, + } + wgpu.SurfaceConfigure(state.surface, &state.config) + + // Create all GPU resources + create_bind_group_layout() + create_render_pipeline() + create_compute_pipeline() + create_buffers_and_bind_groups() + + // Initialize timing + state.last_tick = time.tick_now() + state.accumulator = 0 + + // Start platform loop + os_run() +} + +on_device_error :: proc "c" ( + device: ^wgpu.Device, + errorType: wgpu.ErrorType, + message: string, + userdata1, userdata2: rawptr, +) { + context = state.ctx + panic(message) +} + +//============================================================================= +// Pipeline Creation +//============================================================================= + +create_bind_group_layout :: proc() { + // Binding 0: uniform vec2f (grid size) + b0 := wgpu.BindGroupLayoutEntry { + binding = 0, + visibility = {.Vertex, .Fragment, .Compute}, + buffer = {type = .Uniform, minBindingSize = size_of(f32) * 2}, + } + + // Binding 1: read-only storage (cell state input) + b1 := wgpu.BindGroupLayoutEntry { + binding = 1, + visibility = {.Vertex, .Compute}, + buffer = {type = .ReadOnlyStorage}, + } + + // Binding 2: storage (cell state output - compute only) + b2 := wgpu.BindGroupLayoutEntry { + binding = 2, + visibility = {.Compute}, + buffer = {type = .Storage}, + } + + entries := [3]wgpu.BindGroupLayoutEntry{b0, b1, b2} + + state.bind_group_layout = wgpu.DeviceCreateBindGroupLayout( + state.device, + &{label = "Cell State Bind Group Layout", entryCount = 3, entries = &entries[0]}, + ) + + layouts := [1]wgpu.BindGroupLayout{state.bind_group_layout} + state.pipeline_layout = wgpu.DeviceCreatePipelineLayout( + state.device, + &{bindGroupLayoutCount = 1, bindGroupLayouts = &layouts[0]}, + ) +} + +create_render_pipeline :: proc() { + // Vertex buffer for cell quad + verts := []f32{-0.8, -0.8, 0.8, -0.8, 0.8, 0.8, -0.8, -0.8, 0.8, 0.8, -0.8, 0.8} + + state.vertex_count = u32(len(verts) / 2) + state.vertex_buffer_size = u64(len(verts) * size_of(f32)) + + state.vertex_buffer = wgpu.DeviceCreateBuffer( + state.device, + &{label = "Cell Quad Vertices", usage = {.Vertex, .CopyDst}, size = state.vertex_buffer_size}, + ) + + wgpu.QueueWriteBuffer(state.queue, state.vertex_buffer, 0, raw_data(verts), len(verts) * size_of(f32)) + + // Load shader + state.render_module = wgpu.DeviceCreateShaderModule( + state.device, + &{ + label = "Render Shader", + nextInChain = &wgpu.ShaderSourceWGSL{sType = .ShaderSourceWGSL, code = #load("shaders/render.wgsl")}, + }, + ) + + // Create pipeline + state.render_pipeline = wgpu.DeviceCreateRenderPipeline( + state.device, + &{ + label = "Cell Render Pipeline", + layout = state.pipeline_layout, + vertex = { + module = state.render_module, + entryPoint = "vertexMain", + bufferCount = 1, + buffers = &wgpu.VertexBufferLayout { + arrayStride = size_of(f32) * 2, + stepMode = .Vertex, + attributes = &wgpu.VertexAttribute{shaderLocation = 0, format = .Float32x2}, + attributeCount = 1, + }, + }, + fragment = &{ + module = state.render_module, + entryPoint = "fragmentMain", + targetCount = 1, + targets = &wgpu.ColorTargetState{format = .BGRA8Unorm, writeMask = wgpu.ColorWriteMaskFlags_All}, + }, + primitive = {topology = .TriangleList}, + multisample = {count = 1, mask = 0xFFFFFFFF}, + }, + ) +} + +create_compute_pipeline :: proc() { + state.compute_module = wgpu.DeviceCreateShaderModule( + state.device, + &{ + label = "Compute Shader", + nextInChain = &wgpu.ShaderSourceWGSL{sType = .ShaderSourceWGSL, code = #load("shaders/compute.wgsl")}, + }, + ) + + state.compute_pipeline = wgpu.DeviceCreateComputePipeline( + state.device, + &{ + label = "Game of Life Compute Pipeline", + layout = state.pipeline_layout, + compute = {module = state.compute_module, entryPoint = "computeMain"}, + }, + ) +} + +//============================================================================= +// Buffer Creation +//============================================================================= + +create_buffers_and_bind_groups :: proc() { + // Uniform buffer (grid size) + grid_data := [2]f32{GRID_SIZE, GRID_SIZE} + state.uniform_buffer = wgpu.DeviceCreateBuffer( + state.device, + &{label = "Grid Uniform", usage = {.Uniform, .CopyDst}, size = size_of(grid_data)}, + ) + wgpu.QueueWriteBuffer(state.queue, state.uniform_buffer, 0, &grid_data[0], size_of(grid_data)) + + // Storage buffers (ping-pong for cell states) + cell_count := GRID_SIZE * GRID_SIZE + cell_bytes := u64(size_of(u32) * cell_count) + + for i in 0 ..< 2 { + state.cell_state_storage[i] = wgpu.DeviceCreateBuffer( + state.device, + &{ + label = i == 0 ? "Cell State A" : "Cell State B", + usage = {.Storage, .CopyDst}, + size = cell_bytes, + }, + ) + } + + // Initialise both buffers with random data + { + cells: [GRID_SIZE * GRID_SIZE]u32 + context = runtime.default_context() + rand.reset(u64(time.now()._nsec)) + + for i in 0 ..< GRID_SIZE * GRID_SIZE { + cells[i] = cast(u32)rand.int31_max(2) + } + + wgpu.QueueWriteBuffer(state.queue, state.cell_state_storage[0], 0, &cells, uint(cell_bytes)) + wgpu.QueueWriteBuffer(state.queue, state.cell_state_storage[1], 0, &cells, uint(cell_bytes)) + } + + // Create bind groups (ping-pong) + for i in 0 ..< 2 { + read_buffer := state.cell_state_storage[i] + write_buffer := state.cell_state_storage[(i + 1) % 2] + + entries := [3]wgpu.BindGroupEntry{ + {binding = 0, buffer = state.uniform_buffer, size = size_of(f32) * 2}, + {binding = 1, buffer = read_buffer, size = cell_bytes}, + {binding = 2, buffer = write_buffer, size = cell_bytes}, + } + + state.bind_groups[i] = wgpu.DeviceCreateBindGroup( + state.device, + &{ + label = i == 0 ? "Bind Group 0" : "Bind Group 1", + layout = state.bind_group_layout, + entryCount = 3, + entries = &entries[0], + }, + ) + } +} + +//============================================================================= +// Simulation Logic +//============================================================================= + +update_simulation :: proc(dt: f32) { + // Convert dt to Duration (web uses seconds, desktop uses milliseconds) + dt_duration: time.Duration + when ODIN_OS == .JS { + dt_duration = time.Duration(f64(dt) * f64(time.Second)) + } else { + dt_duration = time.Duration(f64(dt) * f64(time.Millisecond)) + } + + state.accumulator += dt_duration + accumulator_ms := time.duration_milliseconds(state.accumulator) + state.do_update = accumulator_ms >= UPDATE_INTERVAL_MILLISECONDS + + if state.do_update { + state.accumulator = 0 + } +} + +run_compute_pass :: proc(encoder: wgpu.CommandEncoder) { + state.did_compute = false + + if !state.do_update { + return + } + + cpass := wgpu.CommandEncoderBeginComputePass(encoder) + defer wgpu.ComputePassEncoderRelease(cpass) + + wgpu.ComputePassEncoderSetPipeline(cpass, state.compute_pipeline) + wgpu.ComputePassEncoderSetBindGroup(cpass, 0, state.bind_groups[state.step_index % 2]) + + workgroups := u32((GRID_SIZE + WORKGROUP_SIZE - 1) / WORKGROUP_SIZE) + wgpu.ComputePassEncoderDispatchWorkgroups(cpass, workgroups, workgroups, 1) + wgpu.ComputePassEncoderEnd(cpass) + + state.did_compute = true +} + +//============================================================================= +// Rendering +//============================================================================= + +resize :: proc "c" () { + context = state.ctx + state.config.width, state.config.height = os_get_framebuffer_size() + wgpu.SurfaceConfigure(state.surface, &state.config) +} + +frame :: proc "c" (dt: f32) { + context = state.ctx + + update_simulation(dt) + + // Get surface texture + surface_texture := wgpu.SurfaceGetCurrentTexture(state.surface) + + switch surface_texture.status { + case .SuccessOptimal, .SuccessSuboptimal: + // OK + case .Timeout, .Outdated, .Lost: + if surface_texture.texture != nil { + wgpu.TextureRelease(surface_texture.texture) + } + resize() + return + case .OutOfMemory, .DeviceLost, .Error: + fmt.panicf("[Game of Life] surface error: %v", surface_texture.status) + } + defer wgpu.TextureRelease(surface_texture.texture) + + frame_view := wgpu.TextureCreateView(surface_texture.texture, nil) + defer wgpu.TextureViewRelease(frame_view) + + encoder := wgpu.DeviceCreateCommandEncoder(state.device) + defer wgpu.CommandEncoderRelease(encoder) + + // Compute pass + run_compute_pass(encoder) + + // Render pass + { + render_pass := wgpu.CommandEncoderBeginRenderPass( + encoder, + &{ + label = "Main Render Pass", + colorAttachmentCount = 1, + colorAttachments = &wgpu.RenderPassColorAttachment { + view = frame_view, + loadOp = .Clear, + storeOp = .Store, + depthSlice = wgpu.DEPTH_SLICE_UNDEFINED, + clearValue = {0.1, 0.2, 0.3, 1.0}, + }, + }, + ) + defer wgpu.RenderPassEncoderRelease(render_pass) + + wgpu.RenderPassEncoderSetPipeline(render_pass, state.render_pipeline) + + // Use opposite bind group to read latest computed state + bind_group_index := (state.step_index + 1) % 2 + wgpu.RenderPassEncoderSetBindGroup(render_pass, 0, state.bind_groups[bind_group_index]) + + wgpu.RenderPassEncoderSetVertexBuffer(render_pass, 0, state.vertex_buffer, 0, state.vertex_buffer_size) + wgpu.RenderPassEncoderDraw(render_pass, state.vertex_count, GRID_SIZE * GRID_SIZE, 0, 0) + wgpu.RenderPassEncoderEnd(render_pass) + } + + command_buffer := wgpu.CommandEncoderFinish(encoder, nil) + defer wgpu.CommandBufferRelease(command_buffer) + + wgpu.QueueSubmit(state.queue, {command_buffer}) + wgpu.SurfacePresent(state.surface) + + // Advance simulation + if state.did_compute { + state.step_index += 1 + } +} + +//============================================================================= +// Cleanup +//============================================================================= + +cleanup :: proc() { + wgpu.RenderPipelineRelease(state.render_pipeline) + wgpu.ComputePipelineRelease(state.compute_pipeline) + wgpu.PipelineLayoutRelease(state.pipeline_layout) + wgpu.BindGroupLayoutRelease(state.bind_group_layout) + wgpu.ShaderModuleRelease(state.render_module) + wgpu.ShaderModuleRelease(state.compute_module) + wgpu.BufferRelease(state.vertex_buffer) + wgpu.BufferRelease(state.uniform_buffer) + wgpu.BufferRelease(state.cell_state_storage[0]) + wgpu.BufferRelease(state.cell_state_storage[1]) + wgpu.BindGroupRelease(state.bind_groups[0]) + wgpu.BindGroupRelease(state.bind_groups[1]) + wgpu.QueueRelease(state.queue) + wgpu.DeviceRelease(state.device) + wgpu.AdapterRelease(state.adapter) + wgpu.SurfaceRelease(state.surface) + wgpu.InstanceRelease(state.instance) +} diff --git a/wgpu/sdl3/game-of-life/os_desktop.odin b/wgpu/sdl3/game-of-life/os_desktop.odin new file mode 100644 index 0000000..c7950cf --- /dev/null +++ b/wgpu/sdl3/game-of-life/os_desktop.odin @@ -0,0 +1,117 @@ +#+build !js +// Desktop Platform Layer - SDL3 +// ============================== +// Provides platform-specific initialisation and event loop for desktop builds. +// +package main + +import "core:fmt" +import SDL "vendor:sdl3" +import "vendor:wgpu" +import glue "vendor:wgpu/sdl3glue" + +window: ^SDL.Window + +os_init :: proc() { + if !SDL.Init({.VIDEO}) { + fmt.panicf("Failed to initialise SDL: %s", SDL.GetError()) + } + + window = SDL.CreateWindow("Conway's Game of Life", WIDTH, HEIGHT, {.RESIZABLE}) + if window == nil { + fmt.panicf("Failed to create window: %s", SDL.GetError()) + } +} + +os_get_surface :: proc(instance: wgpu.Instance) -> wgpu.Surface { + return glue.GetSurface(instance, window) +} + +os_get_framebuffer_size :: proc() -> (width: u32, height: u32) { + w, h: i32 + if SDL.GetWindowSizeInPixels(window, &w, &h) { + return u32(w), u32(h) + } + return WIDTH, HEIGHT +} + +os_request_adapter_and_device :: proc() { + // Request adapter (synchronous on desktop) + wgpu.InstanceRequestAdapter( + state.instance, + &{compatibleSurface = state.surface, powerPreference = .HighPerformance}, + {callback = on_adapter_sync}, + ) +} + +on_adapter_sync :: proc "c" ( + status: wgpu.RequestAdapterStatus, + adapter: wgpu.Adapter, + message: string, + userdata1, userdata2: rawptr, +) { + context = state.ctx + + if status != .Success || adapter == nil { + fmt.panicf("Failed to get WebGPU adapter: [%v] %s", status, message) + } + + state.adapter = adapter + + // Request device + desc := wgpu.DeviceDescriptor{} + desc.uncapturedErrorCallbackInfo = {callback = on_device_error} + + wgpu.AdapterRequestDevice(state.adapter, &desc, {callback = on_device_sync}) +} + +on_device_sync :: proc "c" ( + status: wgpu.RequestDeviceStatus, + device: wgpu.Device, + message: string, + userdata1, userdata2: rawptr, +) { + context = state.ctx + + if status != .Success || device == nil { + fmt.panicf("Failed to get WebGPU device: [%v] %s", status, message) + } + + complete_gpu_init(device) +} + +os_run :: proc() { + // Main event loop with delta time tracking + event: SDL.Event + running := true + + last := SDL.GetPerformanceCounter() + now: u64 + dt: f32 + + for running { + // Calculate delta time in milliseconds + now = SDL.GetPerformanceCounter() + dt = f32((now - last) * 1000) / f32(SDL.GetPerformanceFrequency()) + last = now + + for SDL.PollEvent(&event) { + #partial switch event.type { + case .QUIT: + running = false + case .WINDOW_RESIZED, .WINDOW_PIXEL_SIZE_CHANGED: + resize() + case .KEY_DOWN: + if event.key.scancode == .ESCAPE { + running = false + } + } + } + + frame(dt) + } + + cleanup() + SDL.DestroyWindow(window) + SDL.Quit() +} diff --git a/wgpu/sdl3/game-of-life/os_web.odin b/wgpu/sdl3/game-of-life/os_web.odin new file mode 100644 index 0000000..6e15396 --- /dev/null +++ b/wgpu/sdl3/game-of-life/os_web.odin @@ -0,0 +1,122 @@ +#+build js +// Web Platform Layer - WASM +// ========================== +// Provides platform-specific initialisation and browser event loop for web builds. +// +package main + +import "base:runtime" +import "core:fmt" +import "core:sys/wasm/js" +import "vendor:wgpu" + +device_ready: bool = false + +os_init :: proc() { + ok := js.add_window_event_listener(.Resize, nil, size_callback) + assert(ok) +} + +os_get_surface :: proc(instance: wgpu.Instance) -> wgpu.Surface { + return wgpu.InstanceCreateSurface( + instance, + &wgpu.SurfaceDescriptor { + nextInChain = &wgpu.SurfaceSourceCanvasHTMLSelector { + sType = .SurfaceSourceCanvasHTMLSelector, + selector = "#wgpu-canvas", + }, + }, + ) +} + +os_get_framebuffer_size :: proc() -> (width: u32, height: u32) { + rect := js.get_bounding_client_rect("body") + dpi := js.device_pixel_ratio() + return u32(f64(rect.width) * dpi), u32(f64(rect.height) * dpi) +} + +os_request_adapter_and_device :: proc() { + fmt.println("[1/3] Requesting adapter (async - waiting for browser)...") + + // Define callbacks inline - crucial for async to work in WASM + wgpu.InstanceRequestAdapter( + state.instance, + &{compatibleSurface = state.surface}, + {callback = on_adapter}, + ) + + // Inline callback for adapter acquisition + on_adapter :: proc "c" ( + status: wgpu.RequestAdapterStatus, + adapter: wgpu.Adapter, + message: string, + userdata1, userdata2: rawptr, + ) { + context = state.ctx + + if status != .Success || adapter == nil { + fmt.panicf("Failed to get WebGPU adapter: [%v] %s", status, message) + } + + fmt.println("[OK] [1/3] Adapter acquired") + state.adapter = adapter + + desc := wgpu.DeviceDescriptor{} + desc.uncapturedErrorCallbackInfo = {callback = on_device_error} + + fmt.println("[2/3] Requesting device (async)...") + wgpu.AdapterRequestDevice(adapter, &desc, {callback = on_device}) + } + + // Inline callback for device acquisition + on_device :: proc "c" ( + status: wgpu.RequestDeviceStatus, + device: wgpu.Device, + message: string, + userdata1, userdata2: rawptr, + ) { + context = state.ctx + + if status != .Success || device == nil { + fmt.panicf("Failed to get WebGPU device: [%v] %s", status, message) + } + + fmt.println("[OK] [2/3] Device acquired") + complete_gpu_init(device) + } + + fmt.println("Waiting for browser callbacks...") +} + +os_run :: proc() { + device_ready = true + fmt.println("[OK] [3/3] WebGPU initialised - Game of Life ready!") +} + +// step is called by the browser runtime at 60 FPS +@(export) +step :: proc(dt: f32) -> bool { + context = state.ctx + + // Wait for async GPU initialization + if !device_ready { + return true + } + + frame(dt) + return true +} + +@(fini) +cleanup_on_exit :: proc "contextless" () { + context = runtime.default_context() + cleanup() + js.remove_window_event_listener(.Resize, nil, size_callback) +} + +size_callback :: proc(e: js.Event) { + if !device_ready { + return + } + resize() +} diff --git a/wgpu/sdl3/game-of-life/shaders/compute.wgsl b/wgpu/sdl3/game-of-life/shaders/compute.wgsl new file mode 100644 index 0000000..0942f5a --- /dev/null +++ b/wgpu/sdl3/game-of-life/shaders/compute.wgsl @@ -0,0 +1,32 @@ +@group(0) @binding(0) var grid : vec2f; +@group(0) @binding(1) var cellStateIn : array; +@group(0) @binding(2) var cellStateOut : array; + +fn cellIndex(cell : vec2u) -> u32 { + return (cell.y % u32(grid.y)) * u32(grid.x) + (cell.x % u32(grid.x)); +} + +fn cellActive(x : u32, y : u32) -> u32 { + return cellStateIn[cellIndex(vec2(x, y))]; +} + +@compute @workgroup_size(8, 8) +fn computeMain(@builtin(global_invocation_id) cell : vec3u) { + let activeNeighbors = + cellActive(cell.x + 1u, cell.y + 1u) + + cellActive(cell.x + 1u, cell.y + 0u) + + cellActive(cell.x + 1u, cell.y - 1u) + + cellActive(cell.x + 0u, cell.y - 1u) + + cellActive(cell.x - 1u, cell.y - 1u) + + cellActive(cell.x - 1u, cell.y + 0u) + + cellActive(cell.x - 1u, cell.y + 1u) + + cellActive(cell.x + 0u, cell.y + 1u); + + let i = cellIndex(cell.xy); + + switch activeNeighbors { + case 2u: { cellStateOut[i] = cellStateIn[i]; } + case 3u: { cellStateOut[i] = 1u; } + default: { cellStateOut[i] = 0u; } + } +} diff --git a/wgpu/sdl3/game-of-life/shaders/render.wgsl b/wgpu/sdl3/game-of-life/shaders/render.wgsl new file mode 100644 index 0000000..9051b2b --- /dev/null +++ b/wgpu/sdl3/game-of-life/shaders/render.wgsl @@ -0,0 +1,33 @@ +struct VertexInput { + @location(0) pos : vec2f, + @builtin(instance_index) instance : u32, +}; + +struct VertexOutput { + @builtin(position) pos : vec4f, + @location(0) cell : vec2f, +}; + +@group(0) @binding(0) var grid : vec2f; +@group(0) @binding(1) var cellState : array; + +@vertex +fn vertexMain(input : VertexInput) -> VertexOutput { + let i = f32(input.instance); + let cell = vec2f(i % grid.x, floor(i / grid.x)); + let state = f32(cellState[input.instance]); + + let cellOffset = cell / grid * 2.0; + let gridPos = (input.pos * state + 1.0) / grid - 1.0 + cellOffset; + + var out : VertexOutput; + out.pos = vec4f(gridPos, 0.0, 1.0); + out.cell = cell; + return out; +} + +@fragment +fn fragmentMain(input : VertexOutput) -> @location(0) vec4f { + let c = input.cell / grid; + return vec4f(c, 1.0 - c.x, 1.0); +} diff --git a/wgpu/sdl3/game-of-life/web/game_of_life.wasm b/wgpu/sdl3/game-of-life/web/game_of_life.wasm new file mode 100755 index 0000000000000000000000000000000000000000..06b5a7182902e06c746b7e3eb41da445ad611e0b GIT binary patch literal 161158 zcmeFa3%p%bb??6(`@PRT*$H_Z5-4kLu{}|#=`ALd>#bR*yu@0;dcFPM|Gj^p5|tC= z5dzv4k^>1eR;;OFjqmtOtVGjlHI|15L5)^4t&c{*8bxiaR#J;L6?*x9e`C(I_FDU# zLr!e_`QOjy{t3xmYt1>w9COSu#vF6ZG3N{}x#neI5Cq|e;%8i3+id>@7dPDl|Aw0{ zZrIDF&6|UZBmGnLnPPr}i^Kd2Sv>%rLX{*t|0-X&>Ea-~{^E*?ItZIK*{^{OlpI`K zS1SJnz}duqJOq@_KJzH}RstOOkS7>QOm;{3{wM8%Q98%Ay8X434@BqRB(Jrkv9Xc*6BI8ctQS^ z2mTWXQVjh~R`K=1L%3ege|n5QJ9=ik?uzSbFI)GrD>wX36dqVGHC1U`dFf?WT*T+< z%QsxMaa~ZYy!<6sU3;SbdH%X9E?u|bg3GR2cll*ktb6{t%hz3U&AOmw8I~)30OS$%1f`kd4Gqopb!hUb1fEMZbOJwO7DmFS+PN zmtUrKZdmuCjX^cfv*=Qz`9)V<_KJ0vU$o(pjh9^+j2LX|MPL7~U3tZeFMG+L-X+4# z*XvsM!pqjZ+&As#l<2zBrNO5!yX1;X&%WYCSHem@S6{ixDHH2na@iFbQ(tn;H9lw0 z=QLjDvxoDW>o#WW?WvK0oS89a^PpmF8=oEIS(jaN)g>EW^wR%!<%abykyKn0bS%FK zQvoxN!r50`>gDm|*#YK#Vo_JTfWaUy7guX8ro0M3?gxV@*&1@MLIHf&t? z3Ma(D=%B4LfS$GCl9zkpjm-*dKnr8s834+pFc{_DKj*S5F1zNXzRwq^4pS4qdF^k% z>@qmka4U-Z1sm2~14Da(D*~KNCuKcYpnVymrK-`hw3siAaz87Od*?mEZtw|o`Q&eM7PV!!J%6Kku%DBFi%g`?YGsWy-m@He}|4pNe@9zah zQ|qo`$)d)e3kTo5er)o&W;7_1m^_=KC8J$q<7F>fchMDBzC7p-n68LKPK%&bEcBxF zR%j%C#mg5)wS@~?t;(Vx40(#{CoFC*sV!VuSvIzC*`iu}VzUt}UmjFf1hrbN5u8*x zF%Bvx*BhOvv*g6u&jn#+X&l6HbwRyaJ(+}4s+~&G>Ghv<>XSP^{|n z$}144jTc?DaYOK&mU{T2OD@0s$`=K{e&R(JG2UDz{d>`iuf5_$Xxb|-+IY!tU%oC3 znwIg!8`iB0exr5~yM~(Jq(7jU` zx_9Z{_6-7jqaIgBZ@KjoQ+mD0z253yx474B{&lZ@S4Ug@>sFUK!3d9Q+#6&x>wcrZJz>$`v#9+(qp^*H;u5d+2|c4;klp~8m*x}Xjl4i67s`$KE!8t zAoF+>^eg%r^sDp_eNaa6faN`EO#(IsK^V*K0j1 zSJPX~l}jrQ@`%2TSlNyqJ9*g!dR>r}9o5^YFFRsrH1w%qMVoqT=0)3jZD&PWdTaTj z4RB5$e>}b#L9RiIezQk&k~&mtCk^-*%0>MM4v*HS*F!)OlBib`-$%Ws5v|#eCdZ9r z_IqxJ%xMKW_L|C?L=IdVAhmw&xd5Q9N$L@e$;0RMy&9lu%IwJBbhPh8fC`njsh=h| zZl3H{$X5YDgls%&sM8y;f?m3gGrDEY7;&62^8a&2@P7(tNMDE{7K=L;pVJB3X|Mp6 zZNIizP%J14_?v7MSX>&#G{*vC`dq7+u0gpttx}ah475sw3hCEOtJEbE;?IIssmG>z zl6t>7*{@5lR1%&LK=calMZLmnL9g&u(kr}YdWFZLUg5Q%S9qJDSNN9c6&{Ovh1Y^! z;cZy2@F~+PJQlf!*MeT*ZFao^8IAH)r(D1^sd?p67oF?wZpP7y6Ww}W-yS*hD+ER&A% z15~Ky4Ol4_E1}i|LKLVBSSivsBM0>9hn|(H2~YnAvQju;rHZjq)XP~Z_N=4?I3p~w zQjoI}93YmOg_U51#DKQZAa%GX>{p&kA0oIKeyaL0dvAR&Fm?)h0;^siYbd$ygjUq^ z5-%zdh^4CJt)AV9GnP_5IKBPjIEpq$o1TObhXvM!Q^|ishENi$kR6l#Ny5=M2%`#m zW@Q|}{f!6-nh2in3{rh<*jrMY_`v`A^0&Wq^S|ys7|0B*O@wPm8+`0FG`=^)fafsE z#%Xw-#%(Z+dm-!=%fgMxK*!lINAK1Jpss8~+SECo3ncQzY6Y62&O;S%NLPb;>^U#X{ z(AWl=Q3n?b1AZ0DF{ahRLSkij2fWb^{eAX|exUEwv@wa)2amM^)KnF^h+2+TVOwp3 zh6x`h!doW7t$l3uhCU?q>x->O8fpgvN1S49hCqz4Fw@qC)semq2r#ql(V*=~W4QHe zz^WYu?GtEsBCwi4+=7kF_b=AN%4W|C)4}?F^Hj$dLBG{&C#_zK-Wka_V(<&)=#|k1 z{1I2eAf|y$BNjCXKBIzY(&#=QE`!od%q9TxGtz4%z#OTg=z99wN-Mn9GM(3Ni=@qT z)g&34X)@UxN!mS9Ny~j` z@xeVcv&qj%fcF!QF&_P7V=<3gg|(nHvw73g7)q$4(Wg>|H$5|q3jqxSp>J^aGb*^; zbay|by&;?wL3~h`E^eu5JdLE_rHP*96rNok^-D@0vOpoD{74nmOlw7X>X9yoJ0&th zl#9~MzfM|(qLD9JYp3`B>GV{PemY1W2-6d&GwMDl1Op)?mQN~3mV^8}_b?L-{gi`w zC`=a@!MKK6hgGay{cwH1m9%7bi~upA>u9&^L5EbbA!!TVYnq=KHp!~mt=}Az&nx!yu zv{px0DM?#Q-b|;1Nhi9=NPD`5P!EKaCYXO=3RzjvhX~BVDbb)H1P)>dlOXSt{Qzkr z>)2%9fClM^UK{kA>R_;#RIll2l@iADgp(#68iCLt<%}G{)AAzE^m$s!gLZ-N>6E5Y zkStw=s`#N;Q{4%_i@SxKbVIw?655if9{Q%*4r5asn%6~AsojSKrB4fU2*}}xiJ<9l zz9*4H4@h2UsLBhCGNvE8a~eE;CH*OsQ09!}6gXBjtvR5!22gJdXMtMD(LXXP`c0%- zOl5pv1)0|fqi3u{Tqg`haya`A^SgC z=V{cSPE$Z_r_!D9ls_Xp4}?y5iV0PECH>6bi2Bv^#=G?UjC5MR8r!8m1E;TH7GDTkZAnsc9Ng!)50y z?!H>zm$yF>g|(su5jZsztyQb{$s4Hg+9?f9lhH9)T467Y$&rNp5PgJGE^`}>N7H(= znKT2sdx_o19p{63eL(wOuE`=CjcUg*3+&hcnyx<}sX@YR)Hvu>A#uz@_x1YL zuf6lJ`maWJs#{6GBWCaW-1~TB-JkBXUjJhiklINmJ;E;zqpY91>-*IV+h}d#w5{B9 zDBW6O-R*2=%44ihm;N$KhLk9baG}W%HB6CT%B$-uNowt>Mm1)IKICt0QxXnlBx)#g z59)&i_P`@E8~|T{L(x~rJT-}Lf?Ar4k-&(|hp8TAa3tDzUbP8K_F+)4Mk#W+1j;ef z)6kPZeX3Kf7j`1vL-!sfl_{H{7!FpwXlmy5rW-Kvsow*uO0!mgu=)CaV@uWyS1wVt zlw;bl#R>uUcej-a{?4u>`kh@E=jpSUX{yzQ7KnZsj5+}F7F0F!pf}QICC&AF`i)zd zVlq#-1$t<@*l+IXx3+C%p3*d*MQdB;^roSX&M{4&1!M7B!!QoP7$&LpFhr;-o(M1O zKnQwjnqK+cetg3%^uY>hjHwBaH^20Pkt6o4O7FoOko|%w;1Cv`%-E)b9<~%nnO~T; zV>kL10ewc3W@!xzBP;>YT$*3M1qAW^xaO~&gdm_~6w#Cq8dzPS_0)ji)4@!NW1h}V zapNwdZTpGZV^hM}JEonrqMi4N$cv+(R|CJg&5W(TdQl7~_FGITnP@)Fe+#y3?T?VP zX7fzuJ-H&fpI)#B_T)o<)DdRlS-;<3ki=X1 z-5dG~MI=p^g;^_EkW{wZ06n`e*mDDeew1K&W7AVO=}_6$Ud#)Xj&3ClMwdce=oq~P z_aK~7F0e!_1%PlVjY+;Fpb@0Dw$$Dpg>j~GG&v1VHI+f9GPE3)hFl#zQ*SGyLwZ{i zo$lUj;b%?sG?&v>c~(c`F70zJXX4)Ob8lnr%~qDyFfXwhY@tY#Je+Q-(H5Hev5gO= zbECBhMzKhlPJLi{iUEX-3>NoGu{ldNNVolnlyqUbLpk`}>vBpHGW3o0LvPcBWt&Ec zK@X5{Ql=b(_SeI~bR8vJcLV_sJU^PtgqNie^Dfk|b6d#LZ_r^lRAfy{Cl+3vR|A)h zvW z;>3_4SNDysmmB?9_`hj~Lt%|yQF`cuw8ZtY&Gm3d!8tR6^g!U<80f+^(HTi@_?*t8_hzi((HlOPIGN!y&LaYIDnDk1u8xw# zfSkt{zhZh8kI7{B4r5^WVLW;P%fQ}VK+Jx9P4t+v)Dqs#S`$4oczfoW=m&$h(^+%P z-o~R<@|zsW-d9HBYvO~0w`5IxKyT67Xl3Gc(N4WWv#tD+X=gaOI+}vt(P9Ck#AKIjDrQy?pMHha zaezVD}FzD?MoDV0Zad#@K8Q~bSdImoK?g*KMO&mP>@h`vT%}3vJ zBuIavx*_oaRVD)88TQDIsT`Vh+&VM(t!S;2Oq+3sX@wF8gUJ-TLo!NfD(Vx?|ZjW$cymnmSY(U&WeYu`)lOMEe3sMcD$I=T%)jC*PtHAp=aOm_cBWpHSv z$?E8T@blph88JZd_-Uaw94{>%4s&84b)v+>wC@QhaU}HGvY>wUo9d0yy(YXyz&W&T zKK=ZwkI~L_S$aU4i&~-i97X>0SK@ecylG)RP}SD=YnfgaM>~5szCdmbb@W)jE_SWy z`J}Xxe&iulh1o>svQ|*&*@T+MqqC$k1DN$JsqYZQOU^Tc`o!n8<{e`Kd?C6?UXmCG zMXyPPxJh>rAdv3-5s)V-tQxgKF_P3*#tc~4vE)yh;6!J?{+=m0WbIRXBD}tj;h406 z`hpen0ffhE)Ae97R1~j^7R^WcBP-*zG9#N4%U=i^?!UBIf;!lGRvrs+ z&V6{qs@*mU(oG*kv9Ux=mE zrjMHwD_-czc1dVBnoeNt-a^vU)T<*m(V)oG)S>CX-a;^-k_I(ooOmMbhG56Tj9p_! z5lEUHm-YFbWe{tGS;gvz!3Pq^H4A&A(rW3?pBe0|QR|49ux9EDNv@?kYndchlaT`3 zi|}=X0VgYz^NP~y!v3U!nQa&Ynp~2{AyBKV4fMyL4NK?WhG{yLu#+cf1h9a7mSlIW zsyb$)mSwE9Ix?;__TeAV;4Q|HOS2pr_>B3Kr(f*MrC@}Ga9otQz$gK5$C4+ACHW`g zIGY6z*m4B691&Z3ZQ|In4v*%nsTXDm^f(lFW~zuPJ)I$*&{%}g!fY2^BmuEi^X_|n z16AgM^k@5yMo z{f|!X{8j@HPT*n`SmGA=L79f5Md95L&$y%(NyY4D*;Nf@^0wYM`c`WJ{cWrVr=JgW zv3x;%>ehuN@i84<)2KWP>&-E2T%56FhJ#m`J-~=cu=`&UZ^a|ba*0t zo#gu3^!k{-jg`M0Th~j|Gdvj&o^CWg9VOnB@=qUSX^iPk`p>~RD+VeHVvKp;58TN5 zn^vDkycYA_Fclw1H;@?>AR+_T_3Q|eimkVk;l!!MBN0m_GgQhP{Yqx9Q-Ha+-?3&b zj@C-uY{dcKW`p%TETNj7?Tw~kZ-G*eR(oCjPStt~wUWNAhVbzIShcr^y!!}}JAvQ5 z{4VBqH@{2pJTwt|XIxl?Sp_}#%8gVqUZwCGSu1O4)0#IT!OR+Yh(!}}j~K5T%ytB{t!1~OABLba;`&;8p56&(J)D#(zWLjcHc6)@;vugdyNTG&mnM38h=6Nx7}yh9A& zNNeVZgs+zVYrxX2m-$QpF~`%ROxWfvfMoVPH$yWV2F^^|V2*Z>S?z!t^vtZ13v-&( zj_Mbw1=W(&BF!eXNViF?q*qeASVN+u(@U>LWhaY75M7`FR^(@_SN|T=WYt+uZ~knN z{KbhTG$oJmJ^79N#*>FL`I9S56h1tw@OjN(JotmEv=2@^r*)k0*(|=6?gtm?h^4uX zm;(tif<)d93E#fF9Vml^o=4{>PF~-IYI&x(B8j}jtt^8B;oD0UOVeWspQo#w0BQ?O z-Al*eL(KHv97s{X2wUtNX7pL-l!b2&802>_VCNtiEr}gwSEz^@85=M}E17_GJeuQ7?CtW zf6rUtA{Ybg#!d$o&&@5!{TPmVNss9rXJnpWRx}m#%>;l1$+lgzxVeiV%qC}}`l)0F zK)H69*wv0TCyQ>Vc7#kCXw*<+AqOSoYF+gINv#WKl31Rh0f9|;-~*EYnIzAYMnoHV zwY(QAXWqR`X7w_qq4`veiJ|$Vts5j>cu=TY-O(Rr$~07D6E0;j

PYvw4OXRsE35 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 P>N1+sgOh+ + + + + + WGPU WASM Game of Life + + + + + + + + + diff --git a/wgpu/sdl3/game-of-life/web/odin.js b/wgpu/sdl3/game-of-life/web/odin.js new file mode 100644 index 0000000..2a8ccdd --- /dev/null +++ b/wgpu/sdl3/game-of-life/web/odin.js @@ -0,0 +1,2157 @@ +"use strict"; + +(function() { + +function getElement(name) { + if (name) { + return document.getElementById(name); + } + return undefined; +} + +function stripNewline(str) { + return str.replace(/\n/, ' ') +} + +class WasmMemoryInterface { + constructor() { + this.memory = null; + this.exports = null; + this.listenerMap = new Map(); + + // Size (in bytes) of the integer type, should be 4 on `js_wasm32` and 8 on `js_wasm64p32` + this.intSize = 4; + } + + setIntSize(size) { + this.intSize = size; + } + + setMemory(memory) { + this.memory = memory; + } + + setExports(exports) { + this.exports = exports; + } + + get mem() { + return new DataView(this.memory.buffer); + } + + + loadF32Array(addr, len) { + let array = new Float32Array(this.memory.buffer, addr, len); + return array; + } + loadF64Array(addr, len) { + let array = new Float64Array(this.memory.buffer, addr, len); + return array; + } + loadU32Array(addr, len) { + let array = new Uint32Array(this.memory.buffer, addr, len); + return array; + } + loadI32Array(addr, len) { + let array = new Int32Array(this.memory.buffer, addr, len); + return array; + } + + + loadU8(addr) { return this.mem.getUint8 (addr); } + loadI8(addr) { return this.mem.getInt8 (addr); } + loadU16(addr) { return this.mem.getUint16 (addr, true); } + loadI16(addr) { return this.mem.getInt16 (addr, true); } + loadU32(addr) { return this.mem.getUint32 (addr, true); } + loadI32(addr) { return this.mem.getInt32 (addr, true); } + loadU64(addr) { + const lo = this.mem.getUint32(addr + 0, true); + const hi = this.mem.getUint32(addr + 4, true); + return lo + hi*4294967296; + }; + loadI64(addr) { + const lo = this.mem.getUint32(addr + 0, true); + const hi = this.mem.getInt32 (addr + 4, true); + return lo + hi*4294967296; + }; + loadF32(addr) { return this.mem.getFloat32(addr, true); } + loadF64(addr) { return this.mem.getFloat64(addr, true); } + loadInt(addr) { + if (this.intSize == 8) { + return this.loadI64(addr); + } else if (this.intSize == 4) { + return this.loadI32(addr); + } else { + throw new Error('Unhandled `intSize`, expected `4` or `8`'); + } + }; + loadUint(addr) { + if (this.intSize == 8) { + return this.loadU64(addr); + } else if (this.intSize == 4) { + return this.loadU32(addr); + } else { + throw new Error('Unhandled `intSize`, expected `4` or `8`'); + } + }; + loadPtr(addr) { return this.loadU32(addr); } + + loadB32(addr) { + return this.loadU32(addr) != 0; + } + + loadBytes(ptr, len) { + return new Uint8Array(this.memory.buffer, ptr, Number(len)); + } + + loadString(ptr, len) { + const bytes = this.loadBytes(ptr, Number(len)); + return new TextDecoder().decode(bytes); + } + + loadCstring(ptr) { + if (ptr == 0) { + return null; + } + let len = 0; + for (; this.mem.getUint8(ptr+len) != 0; len += 1) {} + return this.loadString(ptr, len); + } + + storeU8(addr, value) { this.mem.setUint8 (addr, value); } + storeI8(addr, value) { this.mem.setInt8 (addr, value); } + storeU16(addr, value) { this.mem.setUint16 (addr, value, true); } + storeI16(addr, value) { this.mem.setInt16 (addr, value, true); } + storeU32(addr, value) { this.mem.setUint32 (addr, value, true); } + storeI32(addr, value) { this.mem.setInt32 (addr, value, true); } + storeU64(addr, value) { + this.mem.setUint32(addr + 0, Number(value), true); + + let div = 4294967296; + if (typeof value == 'bigint') { + div = BigInt(div); + } + + this.mem.setUint32(addr + 4, Math.floor(Number(value / div)), true); + } + storeI64(addr, value) { + this.mem.setUint32(addr + 0, Number(value), true); + + let div = 4294967296; + if (typeof value == 'bigint') { + div = BigInt(div); + } + + this.mem.setInt32(addr + 4, Math.floor(Number(value / div)), true); + } + storeF32(addr, value) { this.mem.setFloat32(addr, value, true); } + storeF64(addr, value) { this.mem.setFloat64(addr, value, true); } + storeInt(addr, value) { + if (this.intSize == 8) { + this.storeI64(addr, value); + } else if (this.intSize == 4) { + this.storeI32(addr, value); + } else { + throw new Error('Unhandled `intSize`, expected `4` or `8`'); + } + } + storeUint(addr, value) { + if (this.intSize == 8) { + this.storeU64(addr, value); + } else if (this.intSize == 4) { + this.storeU32(addr, value); + } else { + throw new Error('Unhandled `intSize`, expected `4` or `8`'); + } + } + + // Returned length might not be the same as `value.length` if non-ascii strings are given. + storeString(addr, value) { + const src = new TextEncoder().encode(value); + const dst = new Uint8Array(this.memory.buffer, addr, src.length); + dst.set(src); + return src.length; + } +}; + +class WebGLInterface { + constructor(wasmMemoryInterface) { + this.wasmMemoryInterface = wasmMemoryInterface; + this.ctxElement = null; + this.ctx = null; + this.ctxVersion = 1.0; + this.counter = 1; + this.lastError = 0; + this.buffers = []; + this.mappedBuffers = {}; + this.programs = []; + this.framebuffers = []; + this.renderbuffers = []; + this.textures = []; + this.uniforms = []; + this.shaders = []; + this.vaos = []; + this.contexts = []; + this.currentContext = null; + this.offscreenCanvases = {}; + this.timerQueriesEXT = []; + this.queries = []; + this.samplers = []; + this.transformFeedbacks = []; + this.syncs = []; + this.programInfos = {}; + } + + get mem() { + return this.wasmMemoryInterface + } + + setCurrentContext(element, contextSettings) { + if (!element) { + return false; + } + if (this.ctxElement == element) { + return true; + } + + contextSettings = contextSettings ?? {}; + this.ctx = element.getContext("webgl2", contextSettings) || element.getContext("webgl", contextSettings); + if (!this.ctx) { + return false; + } + this.ctxElement = element; + if (this.ctx.getParameter(0x1F02).indexOf("WebGL 2.0") !== -1) { + this.ctxVersion = 2.0; + } else { + this.ctxVersion = 1.0; + } + return true; + } + + assertWebGL2() { + if (this.ctxVersion < 2) { + throw new Error("WebGL2 procedure called in a canvas without a WebGL2 context"); + } + } + getNewId(table) { + for (var ret = this.counter++, i = table.length; i < ret; i++) { + table[i] = null; + } + return ret; + } + recordError(errorCode) { + this.lastError || (this.lastError = errorCode); + } + populateUniformTable(program) { + let p = this.programs[program]; + this.programInfos[program] = { + uniforms: {}, + maxUniformLength: 0, + maxAttributeLength: -1, + maxUniformBlockNameLength: -1, + }; + for (let ptable = this.programInfos[program], utable = ptable.uniforms, numUniforms = this.ctx.getProgramParameter(p, this.ctx.ACTIVE_UNIFORMS), i = 0; i < numUniforms; ++i) { + let u = this.ctx.getActiveUniform(p, i); + let name = u.name; + if (ptable.maxUniformLength = Math.max(ptable.maxUniformLength, name.length + 1), name.indexOf("]", name.length - 1) !== -1) { + name = name.slice(0, name.lastIndexOf("[")); + } + let loc = this.ctx.getUniformLocation(p, name); + if (loc !== null) { + let id = this.getNewId(this.uniforms); + utable[name] = [u.size, id], this.uniforms[id] = loc; + for (let j = 1; j < u.size; ++j) { + let n = name + "[" + j + "]"; + let loc = this.ctx.getUniformLocation(p, n); + let id = this.getNewId(this.uniforms); + this.uniforms[id] = loc; + } + } + } + } + getSource(shader, strings_ptr, strings_length) { + const stringSize = this.mem.intSize*2; + let source = ""; + for (let i = 0; i < strings_length; i++) { + let ptr = this.mem.loadPtr(strings_ptr + i*stringSize); + let len = this.mem.loadPtr(strings_ptr + i*stringSize + 4); + let str = this.mem.loadString(ptr, len); + source += str; + } + return source; + } + + getWebGL1Interface() { + return { + SetCurrentContextById: (name_ptr, name_len) => { + let name = this.mem.loadString(name_ptr, name_len); + let element = getElement(name); + return this.setCurrentContext(element, {alpha: true, antialias: true, depth: true, premultipliedAlpha: true}); + }, + CreateCurrentContextById: (name_ptr, name_len, attributes) => { + let name = this.mem.loadString(name_ptr, name_len); + let element = getElement(name); + + let contextSettings = { + alpha: !(attributes & (1<<0)), + antialias: !(attributes & (1<<1)), + depth: !(attributes & (1<<2)), + failIfMajorPerformanceCaveat: !!(attributes & (1<<3)), + premultipliedAlpha: !(attributes & (1<<4)), + preserveDrawingBuffer: !!(attributes & (1<<5)), + stencil: !!(attributes & (1<<6)), + desynchronized: !!(attributes & (1<<7)), + }; + + return this.setCurrentContext(element, contextSettings); + }, + GetCurrentContextAttributes: () => { + if (!this.ctx) { + return 0; + } + let attrs = this.ctx.getContextAttributes(); + let res = 0; + if (!attrs.alpha) res |= 1<<0; + if (!attrs.antialias) res |= 1<<1; + if (!attrs.depth) res |= 1<<2; + if (attrs.failIfMajorPerformanceCaveat) res |= 1<<3; + if (!attrs.premultipliedAlpha) res |= 1<<4; + if (attrs.preserveDrawingBuffer) res |= 1<<5; + if (attrs.stencil) res |= 1<<6; + if (attrs.desynchronized) res |= 1<<7; + return res; + }, + + DrawingBufferWidth: () => this.ctx.drawingBufferWidth, + DrawingBufferHeight: () => this.ctx.drawingBufferHeight, + + IsExtensionSupported: (name_ptr, name_len) => { + let name = this.mem.loadString(name_ptr, name_len); + let extensions = this.ctx.getSupportedExtensions(); + return extensions.indexOf(name) !== -1 + }, + + + GetError: () => { + let err = this.lastError; + this.recordError(0); + if (err) { + return err; + } + return this.ctx.getError(); + }, + + GetWebGLVersion: (major_ptr, minor_ptr) => { + let version = this.ctx.getParameter(0x1F02); + if (version.indexOf("WebGL 2.0") !== -1) { + this.mem.storeI32(major_ptr, 2); + this.mem.storeI32(minor_ptr, 0); + return; + } + + this.mem.storeI32(major_ptr, 1); + this.mem.storeI32(minor_ptr, 0); + }, + GetESVersion: (major_ptr, minor_ptr) => { + let version = this.ctx.getParameter(0x1F02); + if (version.indexOf("OpenGL ES 3.0") !== -1) { + this.mem.storeI32(major_ptr, 3); + this.mem.storeI32(minor_ptr, 0); + return; + } + + this.mem.storeI32(major_ptr, 2); + this.mem.storeI32(minor_ptr, 0); + }, + + + ActiveTexture: (x) => { + this.ctx.activeTexture(x); + }, + AttachShader: (program, shader) => { + this.ctx.attachShader(this.programs[program], this.shaders[shader]); + }, + BindAttribLocation: (program, index, name_ptr, name_len) => { + let name = this.mem.loadString(name_ptr, name_len); + this.ctx.bindAttribLocation(this.programs[program], index, name) + }, + BindBuffer: (target, buffer) => { + let bufferObj = buffer ? this.buffers[buffer] : null; + if (target == 35051) { + this.ctx.currentPixelPackBufferBinding = buffer; + } else { + if (target == 35052) { + this.ctx.currentPixelUnpackBufferBinding = buffer; + } + this.ctx.bindBuffer(target, bufferObj) + } + }, + BindFramebuffer: (target, framebuffer) => { + this.ctx.bindFramebuffer(target, framebuffer ? this.framebuffers[framebuffer] : null) + }, + BindTexture: (target, texture) => { + this.ctx.bindTexture(target, texture ? this.textures[texture] : null) + }, + BindRenderbuffer: (target, renderbuffer) => { + this.ctx.bindRenderbuffer(target, renderbuffer ? this.renderbuffers[renderbuffer] : null) + }, + BlendColor: (red, green, blue, alpha) => { + this.ctx.blendColor(red, green, blue, alpha); + }, + BlendEquation: (mode) => { + this.ctx.blendEquation(mode); + }, + BlendEquationSeparate: (modeRGB, modeAlpha) => { + this.ctx.blendEquationSeparate(modeRGB, modeAlpha); + }, + BlendFunc: (sfactor, dfactor) => { + this.ctx.blendFunc(sfactor, dfactor); + }, + BlendFuncSeparate: (srcRGB, dstRGB, srcAlpha, dstAlpha) => { + this.ctx.blendFuncSeparate(srcRGB, dstRGB, srcAlpha, dstAlpha); + }, + + + BufferData: (target, size, data, usage) => { + if (data) { + this.ctx.bufferData(target, this.mem.loadBytes(data, size), usage); + } else { + this.ctx.bufferData(target, size, usage); + } + }, + BufferSubData: (target, offset, size, data) => { + if (data) { + this.ctx.bufferSubData(target, offset, this.mem.loadBytes(data, size)); + } else { + this.ctx.bufferSubData(target, offset, null); + } + }, + + + Clear: (x) => { + this.ctx.clear(x); + }, + ClearColor: (r, g, b, a) => { + this.ctx.clearColor(r, g, b, a); + }, + ClearDepth: (x) => { + this.ctx.clearDepth(x); + }, + ClearStencil: (x) => { + this.ctx.clearStencil(x); + }, + ColorMask: (r, g, b, a) => { + this.ctx.colorMask(!!r, !!g, !!b, !!a); + }, + CompileShader: (shader) => { + this.ctx.compileShader(this.shaders[shader]); + }, + + + CompressedTexImage2D: (target, level, internalformat, width, height, border, imageSize, data) => { + if (data) { + this.ctx.compressedTexImage2D(target, level, internalformat, width, height, border, this.mem.loadBytes(data, imageSize)); + } else { + this.ctx.compressedTexImage2D(target, level, internalformat, width, height, border, null); + } + }, + CompressedTexSubImage2D: (target, level, xoffset, yoffset, width, height, format, imageSize, data) => { + if (data) { + this.ctx.compressedTexSubImage2D(target, level, xoffset, yoffset, width, height, format, this.mem.loadBytes(data, imageSize)); + } else { + this.ctx.compressedTexSubImage2D(target, level, xoffset, yoffset, width, height, format, null); + } + }, + + CopyTexImage2D: (target, level, internalformat, x, y, width, height, border) => { + this.ctx.copyTexImage2D(target, level, internalformat, x, y, width, height, border); + }, + CopyTexSubImage2D: (target, level, xoffset, yoffset, x, y, width, height) => { + this.ctx.copyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height); + }, + + + CreateBuffer: () => { + let buffer = this.ctx.createBuffer(); + if (!buffer) { + this.recordError(1282); + return 0; + } + let id = this.getNewId(this.buffers); + buffer.name = id + this.buffers[id] = buffer; + return id; + }, + CreateFramebuffer: () => { + let buffer = this.ctx.createFramebuffer(); + let id = this.getNewId(this.framebuffers); + buffer.name = id + this.framebuffers[id] = buffer; + return id; + }, + CreateProgram: () => { + let program = this.ctx.createProgram(); + let id = this.getNewId(this.programs); + program.name = id; + this.programs[id] = program; + return id; + }, + CreateRenderbuffer: () => { + let buffer = this.ctx.createRenderbuffer(); + let id = this.getNewId(this.renderbuffers); + buffer.name = id; + this.renderbuffers[id] = buffer; + return id; + }, + CreateShader: (shaderType) => { + let shader = this.ctx.createShader(shaderType); + let id = this.getNewId(this.shaders); + shader.name = id; + this.shaders[id] = shader; + return id; + }, + CreateTexture: () => { + let texture = this.ctx.createTexture(); + if (!texture) { + this.recordError(1282) + return 0; + } + let id = this.getNewId(this.textures); + texture.name = id; + this.textures[id] = texture; + return id; + }, + + + CullFace: (mode) => { + this.ctx.cullFace(mode); + }, + + + DeleteBuffer: (id) => { + let obj = this.buffers[id]; + if (obj && id != 0) { + this.ctx.deleteBuffer(obj); + this.buffers[id] = null; + } + }, + DeleteFramebuffer: (id) => { + let obj = this.framebuffers[id]; + if (obj && id != 0) { + this.ctx.deleteFramebuffer(obj); + this.framebuffers[id] = null; + } + }, + DeleteProgram: (id) => { + let obj = this.programs[id]; + if (obj && id != 0) { + this.ctx.deleteProgram(obj); + this.programs[id] = null; + } + }, + DeleteRenderbuffer: (id) => { + let obj = this.renderbuffers[id]; + if (obj && id != 0) { + this.ctx.deleteRenderbuffer(obj); + this.renderbuffers[id] = null; + } + }, + DeleteShader: (id) => { + let obj = this.shaders[id]; + if (obj && id != 0) { + this.ctx.deleteShader(obj); + this.shaders[id] = null; + } + }, + DeleteTexture: (id) => { + let obj = this.textures[id]; + if (obj && id != 0) { + this.ctx.deleteTexture(obj); + this.textures[id] = null; + } + }, + + + DepthFunc: (func) => { + this.ctx.depthFunc(func); + }, + DepthMask: (flag) => { + this.ctx.depthMask(!!flag); + }, + DepthRange: (zNear, zFar) => { + this.ctx.depthRange(zNear, zFar); + }, + DetachShader: (program, shader) => { + this.ctx.detachShader(this.programs[program], this.shaders[shader]); + }, + Disable: (cap) => { + this.ctx.disable(cap); + }, + DisableVertexAttribArray: (index) => { + this.ctx.disableVertexAttribArray(index); + }, + DrawArrays: (mode, first, count) => { + this.ctx.drawArrays(mode, first, count); + }, + DrawElements: (mode, count, type, indices) => { + this.ctx.drawElements(mode, count, type, indices); + }, + + + Enable: (cap) => { + this.ctx.enable(cap); + }, + EnableVertexAttribArray: (index) => { + this.ctx.enableVertexAttribArray(index); + }, + Finish: () => { + this.ctx.finish(); + }, + Flush: () => { + this.ctx.flush(); + }, + FramebufferRenderbuffer: (target, attachment, renderbuffertarget, renderbuffer) => { + this.ctx.framebufferRenderbuffer(target, attachment, renderbuffertarget, this.renderbuffers[renderbuffer]); + }, + FramebufferTexture2D: (target, attachment, textarget, texture, level) => { + this.ctx.framebufferTexture2D(target, attachment, textarget, this.textures[texture], level); + }, + FrontFace: (mode) => { + this.ctx.frontFace(mode); + }, + + + GenerateMipmap: (target) => { + this.ctx.generateMipmap(target); + }, + + + GetAttribLocation: (program, name_ptr, name_len) => { + let name = this.mem.loadString(name_ptr, name_len); + return this.ctx.getAttribLocation(this.programs[program], name); + }, + + + GetParameter: (pname) => { + return this.ctx.getParameter(pname); + }, + GetParameter4i: (pname, v0, v1, v2, v3) => { + const i4 = this.ctx.getParameter(pname); + this.mem.storeI32(v0, i4[0]); + this.mem.storeI32(v1, i4[1]); + this.mem.storeI32(v2, i4[2]); + this.mem.storeI32(v3, i4[3]); + }, + GetProgramParameter: (program, pname) => { + return this.ctx.getProgramParameter(this.programs[program], pname) + }, + GetProgramInfoLog: (program, buf_ptr, buf_len, length_ptr) => { + let log = this.ctx.getProgramInfoLog(this.programs[program]); + if (log === null) { + log = "(unknown error)"; + } + if (buf_len > 0 && buf_ptr) { + let n = Math.min(buf_len, log.length); + log = log.substring(0, n); + this.mem.loadBytes(buf_ptr, buf_len).set(new TextEncoder().encode(log)) + + this.mem.storeInt(length_ptr, n); + } + }, + GetShaderInfoLog: (shader, buf_ptr, buf_len, length_ptr) => { + let log = this.ctx.getShaderInfoLog(this.shaders[shader]); + if (log === null) { + log = "(unknown error)"; + } + if (buf_len > 0 && buf_ptr) { + let n = Math.min(buf_len, log.length); + log = log.substring(0, n); + this.mem.loadBytes(buf_ptr, buf_len).set(new TextEncoder().encode(log)) + + this.mem.storeInt(length_ptr, n); + } + }, + GetShaderiv: (shader, pname, p) => { + if (p) { + if (pname == 35716) { + let log = this.ctx.getShaderInfoLog(this.shaders[shader]); + if (log === null) { + log = "(unknown error)"; + } + this.mem.storeInt(p, log.length+1); + } else if (pname == 35720) { + let source = this.ctx.getShaderSource(this.shaders[shader]); + let sourceLength = (source === null || source.length == 0) ? 0 : source.length+1; + this.mem.storeInt(p, sourceLength); + } else { + let param = this.ctx.getShaderParameter(this.shaders[shader], pname); + this.mem.storeI32(p, param); + } + } else { + this.recordError(1281); + } + }, + + + GetUniformLocation: (program, name_ptr, name_len) => { + let name = this.mem.loadString(name_ptr, name_len); + let arrayOffset = 0; + if (name.indexOf("]", name.length - 1) !== -1) { + let ls = name.lastIndexOf("["), + arrayIndex = name.slice(ls + 1, -1); + if (arrayIndex.length > 0 && (arrayOffset = parseInt(arrayIndex)) < 0) { + return -1; + } + name = name.slice(0, ls) + } + var ptable = this.programInfos[program]; + if (!ptable) { + return -1; + } + var uniformInfo = ptable.uniforms[name]; + return (uniformInfo && arrayOffset < uniformInfo[0]) ? uniformInfo[1] + arrayOffset : -1 + }, + + + GetVertexAttribOffset: (index, pname) => { + return this.ctx.getVertexAttribOffset(index, pname); + }, + + + Hint: (target, mode) => { + this.ctx.hint(target, mode); + }, + + + IsBuffer: (buffer) => this.ctx.isBuffer(this.buffers[buffer]), + IsEnabled: (cap) => this.ctx.isEnabled(cap), + IsFramebuffer: (framebuffer) => this.ctx.isFramebuffer(this.framebuffers[framebuffer]), + IsProgram: (program) => this.ctx.isProgram(this.programs[program]), + IsRenderbuffer: (renderbuffer) => this.ctx.isRenderbuffer(this.renderbuffers[renderbuffer]), + IsShader: (shader) => this.ctx.isShader(this.shaders[shader]), + IsTexture: (texture) => this.ctx.isTexture(this.textures[texture]), + + LineWidth: (width) => { + this.ctx.lineWidth(width); + }, + LinkProgram: (program) => { + this.ctx.linkProgram(this.programs[program]); + this.programInfos[program] = null; + this.populateUniformTable(program); + }, + PixelStorei: (pname, param) => { + this.ctx.pixelStorei(pname, param); + }, + PolygonOffset: (factor, units) => { + this.ctx.polygonOffset(factor, units); + }, + + + ReadnPixels: (x, y, width, height, format, type, bufSize, data) => { + this.ctx.readPixels(x, y, width, height, format, type, this.mem.loadBytes(data, bufSize)); + }, + RenderbufferStorage: (target, internalformat, width, height) => { + this.ctx.renderbufferStorage(target, internalformat, width, height); + }, + SampleCoverage: (value, invert) => { + this.ctx.sampleCoverage(value, !!invert); + }, + Scissor: (x, y, width, height) => { + this.ctx.scissor(x, y, width, height); + }, + ShaderSource: (shader, strings_ptr, strings_length) => { + let source = this.getSource(shader, strings_ptr, strings_length); + this.ctx.shaderSource(this.shaders[shader], source); + }, + + StencilFunc: (func, ref, mask) => { + this.ctx.stencilFunc(func, ref, mask); + }, + StencilFuncSeparate: (face, func, ref, mask) => { + this.ctx.stencilFuncSeparate(face, func, ref, mask); + }, + StencilMask: (mask) => { + this.ctx.stencilMask(mask); + }, + StencilMaskSeparate: (face, mask) => { + this.ctx.stencilMaskSeparate(face, mask); + }, + StencilOp: (fail, zfail, zpass) => { + this.ctx.stencilOp(fail, zfail, zpass); + }, + StencilOpSeparate: (face, fail, zfail, zpass) => { + this.ctx.stencilOpSeparate(face, fail, zfail, zpass); + }, + + + TexImage2D: (target, level, internalformat, width, height, border, format, type, size, data) => { + if (data) { + this.ctx.texImage2D(target, level, internalformat, width, height, border, format, type, this.mem.loadBytes(data, size)); + } else { + this.ctx.texImage2D(target, level, internalformat, width, height, border, format, type, null); + } + }, + TexParameterf: (target, pname, param) => { + this.ctx.texParameterf(target, pname, param); + }, + TexParameteri: (target, pname, param) => { + this.ctx.texParameteri(target, pname, param); + }, + TexSubImage2D: (target, level, xoffset, yoffset, width, height, format, type, size, data) => { + this.ctx.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, this.mem.loadBytes(data, size)); + }, + + + Uniform1f: (location, v0) => { this.ctx.uniform1f(this.uniforms[location], v0); }, + Uniform2f: (location, v0, v1) => { this.ctx.uniform2f(this.uniforms[location], v0, v1); }, + Uniform3f: (location, v0, v1, v2) => { this.ctx.uniform3f(this.uniforms[location], v0, v1, v2); }, + Uniform4f: (location, v0, v1, v2, v3) => { this.ctx.uniform4f(this.uniforms[location], v0, v1, v2, v3); }, + + Uniform1i: (location, v0) => { this.ctx.uniform1i(this.uniforms[location], v0); }, + Uniform2i: (location, v0, v1) => { this.ctx.uniform2i(this.uniforms[location], v0, v1); }, + Uniform3i: (location, v0, v1, v2) => { this.ctx.uniform3i(this.uniforms[location], v0, v1, v2); }, + Uniform4i: (location, v0, v1, v2, v3) => { this.ctx.uniform4i(this.uniforms[location], v0, v1, v2, v3); }, + + Uniform1fv: (location, count, addr) => { + let array = this.mem.loadF32Array(addr, 1*count); + this.ctx.uniform1fv(this.uniforms[location], array); + }, + Uniform2fv: (location, count, addr) => { + let array = this.mem.loadF32Array(addr, 2*count); + this.ctx.uniform2fv(this.uniforms[location], array); + }, + Uniform3fv: (location, count, addr) => { + let array = this.mem.loadF32Array(addr, 3*count); + this.ctx.uniform3fv(this.uniforms[location], array); + }, + Uniform4fv: (location, count, addr) => { + let array = this.mem.loadF32Array(addr, 4*count); + this.ctx.uniform4fv(this.uniforms[location], array); + }, + + Uniform1iv: (location, count, addr) => { + let array = this.mem.loadI32Array(addr, 1*count); + this.ctx.uniform1iv(this.uniforms[location], array); + }, + Uniform2iv: (location, count, addr) => { + let array = this.mem.loadI32Array(addr, 2*count); + this.ctx.uniform2iv(this.uniforms[location], array); + }, + Uniform3iv: (location, count, addr) => { + let array = this.mem.loadI32Array(addr, 3*count); + this.ctx.uniform3iv(this.uniforms[location], array); + }, + Uniform4iv: (location, count, addr) => { + let array = this.mem.loadI32Array(addr, 4*count); + this.ctx.uniform4iv(this.uniforms[location], array); + }, + + UniformMatrix2fv: (location, addr) => { + let array = this.mem.loadF32Array(addr, 2*2); + this.ctx.uniformMatrix2fv(this.uniforms[location], false, array); + }, + UniformMatrix3fv: (location, addr) => { + let array = this.mem.loadF32Array(addr, 3*3); + this.ctx.uniformMatrix3fv(this.uniforms[location], false, array); + }, + UniformMatrix4fv: (location, addr) => { + let array = this.mem.loadF32Array(addr, 4*4); + this.ctx.uniformMatrix4fv(this.uniforms[location], false, array); + }, + + UseProgram: (program) => { + if (program) this.ctx.useProgram(this.programs[program]); + }, + ValidateProgram: (program) => { + if (program) this.ctx.validateProgram(this.programs[program]); + }, + + + VertexAttrib1f: (index, x) => { + this.ctx.vertexAttrib1f(index, x); + }, + VertexAttrib2f: (index, x, y) => { + this.ctx.vertexAttrib2f(index, x, y); + }, + VertexAttrib3f: (index, x, y, z) => { + this.ctx.vertexAttrib3f(index, x, y, z); + }, + VertexAttrib4f: (index, x, y, z, w) => { + this.ctx.vertexAttrib4f(index, x, y, z, w); + }, + VertexAttribPointer: (index, size, type, normalized, stride, ptr) => { + this.ctx.vertexAttribPointer(index, size, type, !!normalized, stride, ptr); + }, + + Viewport: (x, y, w, h) => { + this.ctx.viewport(x, y, w, h); + }, + }; + } + + getWebGL2Interface() { + return { + /* Buffer objects */ + CopyBufferSubData: (readTarget, writeTarget, readOffset, writeOffset, size) => { + this.assertWebGL2(); + this.ctx.copyBufferSubData(readTarget, writeTarget, readOffset, writeOffset, size); + }, + GetBufferSubData: (target, srcByteOffset, dst_buffer_ptr, dst_buffer_len, dstOffset, length) => { + this.assertWebGL2(); + this.ctx.getBufferSubData(target, srcByteOffset, this.mem.loadBytes(dst_buffer_ptr, dst_buffer_len), dstOffset, length); + }, + + /* Framebuffer objects */ + BlitFramebuffer: (srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter) => { + this.assertWebGL2(); + this.ctx.blitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); + }, + FramebufferTextureLayer: (target, attachment, texture, level, layer) => { + this.assertWebGL2(); + this.ctx.framebufferTextureLayer(target, attachment, this.textures[texture], level, layer); + }, + InvalidateFramebuffer: (target, attachments_ptr, attachments_len) => { + this.assertWebGL2(); + let attachments = this.mem.loadU32Array(attachments_ptr, attachments_len); + this.ctx.invalidateFramebuffer(target, attachments); + }, + InvalidateSubFramebuffer: (target, attachments_ptr, attachments_len, x, y, width, height) => { + this.assertWebGL2(); + let attachments = this.mem.loadU32Array(attachments_ptr, attachments_len); + this.ctx.invalidateSubFramebuffer(target, attachments, x, y, width, height); + }, + ReadBuffer: (src) => { + this.assertWebGL2(); + this.ctx.readBuffer(src); + }, + + /* Renderbuffer objects */ + RenderbufferStorageMultisample: (target, samples, internalformat, width, height) => { + this.assertWebGL2(); + this.ctx.renderbufferStorageMultisample(target, samples, internalformat, width, height); + }, + + /* Texture objects */ + + TexStorage3D: (target, levels, internalformat, width, height, depth) => { + this.assertWebGL2(); + this.ctx.texStorage3D(target, levels, internalformat, width, height, depth); + }, + TexImage3D: (target, level, internalformat, width, height, depth, border, format, type, size, data) => { + this.assertWebGL2(); + if (data) { + this.ctx.texImage3D(target, level, internalformat, width, height, depth, border, format, type, this.mem.loadBytes(data, size)); + } else { + this.ctx.texImage3D(target, level, internalformat, width, height, depth, border, format, type, null); + } + }, + TexSubImage3D: (target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, size, data) => { + this.assertWebGL2(); + this.ctx.texSubImage3D(target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, this.mem.loadBytes(data, size)); + }, + CompressedTexImage3D: (target, level, internalformat, width, height, depth, border, imageSize, data) => { + this.assertWebGL2(); + if (data) { + this.ctx.compressedTexImage3D(target, level, internalformat, width, height, depth, border, this.mem.loadBytes(data, imageSize)); + } else { + this.ctx.compressedTexImage3D(target, level, internalformat, width, height, depth, border, null); + } + }, + CompressedTexSubImage3D: (target, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data) => { + this.assertWebGL2(); + if (data) { + this.ctx.compressedTexSubImage3D(target, level, xoffset, yoffset, zoffset, width, height, depth, format, this.mem.loadBytes(data, imageSize)); + } else { + this.ctx.compressedTexSubImage3D(target, level, xoffset, yoffset, zoffset, width, height, depth, format, null); + } + }, + + CopyTexSubImage3D: (target, level, xoffset, yoffset, zoffset, x, y, width, height) => { + this.assertWebGL2(); + this.ctx.copyTexSubImage3D(target, level, xoffset, yoffset, zoffset, x, y, width, height); + }, + + /* Programs and shaders */ + GetFragDataLocation: (program, name_ptr, name_len) => { + this.assertWebGL2(); + return this.ctx.getFragDataLocation(this.programs[program], this.mem.loadString(name_ptr, name_len)); + }, + + /* Uniforms */ + Uniform1ui: (location, v0) => { + this.assertWebGL2(); + this.ctx.uniform1ui(this.uniforms[location], v0); + }, + Uniform2ui: (location, v0, v1) => { + this.assertWebGL2(); + this.ctx.uniform2ui(this.uniforms[location], v0, v1); + }, + Uniform3ui: (location, v0, v1, v2) => { + this.assertWebGL2(); + this.ctx.uniform3ui(this.uniforms[location], v0, v1, v2); + }, + Uniform4ui: (location, v0, v1, v2, v3) => { + this.assertWebGL2(); + this.ctx.uniform4ui(this.uniforms[location], v0, v1, v2, v3); + }, + + UniformMatrix3x2fv: (location, addr) => { + this.assertWebGL2(); + let array = this.mem.loadF32Array(addr, 3*2); + this.ctx.uniformMatrix3x2fv(this.uniforms[location], false, array); + }, + UniformMatrix4x2fv: (location, addr) => { + this.assertWebGL2(); + let array = this.mem.loadF32Array(addr, 4*2); + this.ctx.uniformMatrix4x2fv(this.uniforms[location], false, array); + }, + UniformMatrix2x3fv: (location, addr) => { + this.assertWebGL2(); + let array = this.mem.loadF32Array(addr, 2*3); + this.ctx.uniformMatrix2x3fv(this.uniforms[location], false, array); + }, + UniformMatrix4x3fv: (location, addr) => { + this.assertWebGL2(); + let array = this.mem.loadF32Array(addr, 4*3); + this.ctx.uniformMatrix4x3fv(this.uniforms[location], false, array); + }, + UniformMatrix2x4fv: (location, addr) => { + this.assertWebGL2(); + let array = this.mem.loadF32Array(addr, 2*4); + this.ctx.uniformMatrix2x4fv(this.uniforms[location], false, array); + }, + UniformMatrix3x4fv: (location, addr) => { + this.assertWebGL2(); + let array = this.mem.loadF32Array(addr, 3*4); + this.ctx.uniformMatrix3x4fv(this.uniforms[location], false, array); + }, + + /* Vertex attribs */ + VertexAttribI4i: (index, x, y, z, w) => { + this.assertWebGL2(); + this.ctx.vertexAttribI4i(index, x, y, z, w); + }, + VertexAttribI4ui: (index, x, y, z, w) => { + this.assertWebGL2(); + this.ctx.vertexAttribI4ui(index, x, y, z, w); + }, + VertexAttribIPointer: (index, size, type, stride, offset) => { + this.assertWebGL2(); + this.ctx.vertexAttribIPointer(index, size, type, stride, offset); + }, + + /* Writing to the drawing buffer */ + VertexAttribDivisor: (index, divisor) => { + this.assertWebGL2(); + this.ctx.vertexAttribDivisor(index, divisor); + }, + DrawArraysInstanced: (mode, first, count, instanceCount) => { + this.assertWebGL2(); + this.ctx.drawArraysInstanced(mode, first, count, instanceCount); + }, + DrawElementsInstanced: (mode, count, type, offset, instanceCount) => { + this.assertWebGL2(); + this.ctx.drawElementsInstanced(mode, count, type, offset, instanceCount); + }, + DrawRangeElements: (mode, start, end, count, type, offset) => { + this.assertWebGL2(); + this.ctx.drawRangeElements(mode, start, end, count, type, offset); + }, + + /* Multiple Render Targets */ + DrawBuffers: (buffers_ptr, buffers_len) => { + this.assertWebGL2(); + let array = this.mem.loadU32Array(buffers_ptr, buffers_len); + this.ctx.drawBuffers(array); + }, + ClearBufferfv: (buffer, drawbuffer, values_ptr, values_len) => { + this.assertWebGL2(); + let array = this.mem.loadF32Array(values_ptr, values_len); + this.ctx.clearBufferfv(buffer, drawbuffer, array); + }, + ClearBufferiv: (buffer, drawbuffer, values_ptr, values_len) => { + this.assertWebGL2(); + let array = this.mem.loadI32Array(values_ptr, values_len); + this.ctx.clearBufferiv(buffer, drawbuffer, array); + }, + ClearBufferuiv: (buffer, drawbuffer, values_ptr, values_len) => { + this.assertWebGL2(); + let array = this.mem.loadU32Array(values_ptr, values_len); + this.ctx.clearBufferuiv(buffer, drawbuffer, array); + }, + ClearBufferfi: (buffer, drawbuffer, depth, stencil) => { + this.assertWebGL2(); + this.ctx.clearBufferfi(buffer, drawbuffer, depth, stencil); + }, + + /* Query Objects */ + CreateQuery: () => { + this.assertWebGL2(); + let query = this.ctx.createQuery(); + let id = this.getNewId(this.queries); + query.name = id; + this.queries[id] = query; + return id; + }, + DeleteQuery: (id) => { + this.assertWebGL2(); + let obj = this.queries[id]; + if (obj && id != 0) { + this.ctx.deleteQuery(obj); + this.queries[id] = null; + } + }, + IsQuery: (query) => { + this.assertWebGL2(); + return this.ctx.isQuery(this.queries[query]); + }, + BeginQuery: (target, query) => { + this.assertWebGL2(); + this.ctx.beginQuery(target, this.queries[query]) + }, + EndQuery: (target) => { + this.assertWebGL2(); + this.ctx.endQuery(target); + }, + GetQuery: (target, pname) => { + this.assertWebGL2(); + let query = this.ctx.getQuery(target, pname); + if (!query) { + return 0; + } + if (this.queries.indexOf(query) !== -1) { + return query.name; + } + let id = this.getNewId(this.queries); + query.name = id; + this.queries[id] = query; + return id; + }, + + /* Sampler Objects */ + CreateSampler: () => { + this.assertWebGL2(); + let sampler = this.ctx.createSampler(); + let id = this.getNewId(this.samplers); + sampler.name = id; + this.samplers[id] = sampler; + return id; + }, + DeleteSampler: (id) => { + this.assertWebGL2(); + let obj = this.samplers[id]; + if (obj && id != 0) { + this.ctx.deleteSampler(obj); + this.samplers[id] = null; + } + }, + IsSampler: (sampler) => { + this.assertWebGL2(); + return this.ctx.isSampler(this.samplers[sampler]); + }, + BindSampler: (unit, sampler) => { + this.assertWebGL2(); + this.ctx.bindSampler(unit, this.samplers[sampler]); + }, + SamplerParameteri: (sampler, pname, param) => { + this.assertWebGL2(); + this.ctx.samplerParameteri(this.samplers[sampler], pname, param); + }, + SamplerParameterf: (sampler, pname, param) => { + this.assertWebGL2(); + this.ctx.samplerParameterf(this.samplers[sampler], pname, param); + }, + + /* Sync objects */ + FenceSync: (condition, flags) => { + this.assertWebGL2(); + let sync = this.ctx.fenceSync(condition, flags); + let id = this.getNewId(this.syncs); + sync.name = id; + this.syncs[id] = sync; + return id; + }, + IsSync: (sync) => { + this.assertWebGL2(); + return this.ctx.isSync(this.syncs[sync]); + }, + DeleteSync: (id) => { + this.assertWebGL2(); + let obj = this.syncs[id]; + if (obj && id != 0) { + this.ctx.deleteSampler(obj); + this.syncs[id] = null; + } + }, + ClientWaitSync: (sync, flags, timeout) => { + this.assertWebGL2(); + return this.ctx.clientWaitSync(this.syncs[sync], flags, timeout); + }, + WaitSync: (sync, flags, timeout) => { + this.assertWebGL2(); + this.ctx.waitSync(this.syncs[sync], flags, timeout) ; + }, + + + /* Transform Feedback */ + CreateTransformFeedback: () => { + this.assertWebGL2(); + let transformFeedback = this.ctx.createTransformFeedback(); + let id = this.getNewId(this.transformFeedbacks); + transformFeedback.name = id; + this.transformFeedbacks[id] = transformFeedback; + return id; + }, + DeleteTransformFeedback: (id) => { + this.assertWebGL2(); + let obj = this.transformFeedbacks[id]; + if (obj && id != 0) { + this.ctx.deleteTransformFeedback(obj); + this.transformFeedbacks[id] = null; + } + }, + IsTransformFeedback: (tf) => { + this.assertWebGL2(); + return this.ctx.isTransformFeedback(this.transformFeedbacks[tf]); + }, + BindTransformFeedback: (target, tf) => { + this.assertWebGL2(); + this.ctx.bindTransformFeedback(target, this.transformFeedbacks[tf]); + }, + BeginTransformFeedback: (primitiveMode) => { + this.assertWebGL2(); + this.ctx.beginTransformFeedback(primitiveMode); + }, + EndTransformFeedback: () => { + this.assertWebGL2(); + this.ctx.endTransformFeedback(); + }, + TransformFeedbackVaryings: (program, varyings_ptr, varyings_len, bufferMode) => { + this.assertWebGL2(); + const stringSize = this.mem.intSize*2; + let varyings = []; + for (let i = 0; i < varyings_len; i++) { + let ptr = this.mem.loadPtr(varyings_ptr + i*stringSize + 0*4); + let len = this.mem.loadPtr(varyings_ptr + i*stringSize + 1*4); + varyings.push(this.mem.loadString(ptr, len)); + } + this.ctx.transformFeedbackVaryings(this.programs[program], varyings, bufferMode); + }, + PauseTransformFeedback: () => { + this.assertWebGL2(); + this.ctx.pauseTransformFeedback(); + }, + ResumeTransformFeedback: () => { + this.assertWebGL2(); + this.ctx.resumeTransformFeedback(); + }, + + + /* Uniform Buffer Objects and Transform Feedback Buffers */ + BindBufferBase: (target, index, buffer) => { + this.assertWebGL2(); + this.ctx.bindBufferBase(target, index, this.buffers[buffer]); + }, + BindBufferRange: (target, index, buffer, offset, size) => { + this.assertWebGL2(); + this.ctx.bindBufferRange(target, index, this.buffers[buffer], offset, size); + }, + GetUniformBlockIndex: (program, uniformBlockName_ptr, uniformBlockName_len) => { + this.assertWebGL2(); + return this.ctx.getUniformBlockIndex(this.programs[program], this.mem.loadString(uniformBlockName_ptr, uniformBlockName_len)); + }, + // any getActiveUniformBlockParameter(WebGLProgram program, GLuint uniformBlockIndex, GLenum pname); + GetActiveUniformBlockName: (program, uniformBlockIndex, buf_ptr, buf_len, length_ptr) => { + this.assertWebGL2(); + let name = this.ctx.getActiveUniformBlockName(this.programs[program], uniformBlockIndex); + + let n = Math.min(buf_len, name.length); + name = name.substring(0, n); + this.mem.loadBytes(buf_ptr, buf_len).set(new TextEncoder().encode(name)) + this.mem.storeInt(length_ptr, n); + }, + UniformBlockBinding: (program, uniformBlockIndex, uniformBlockBinding) => { + this.assertWebGL2(); + this.ctx.uniformBlockBinding(this.programs[program], uniformBlockIndex, uniformBlockBinding); + }, + + /* Vertex Array Objects */ + CreateVertexArray: () => { + this.assertWebGL2(); + let vao = this.ctx.createVertexArray(); + let id = this.getNewId(this.vaos); + vao.name = id; + this.vaos[id] = vao; + return id; + }, + DeleteVertexArray: (id) => { + this.assertWebGL2(); + let obj = this.vaos[id]; + if (obj && id != 0) { + this.ctx.deleteVertexArray(obj); + this.vaos[id] = null; + } + }, + IsVertexArray: (vertexArray) => { + this.assertWebGL2(); + return this.ctx.isVertexArray(this.vaos[vertexArray]); + }, + BindVertexArray: (vertexArray) => { + this.assertWebGL2(); + this.ctx.bindVertexArray(this.vaos[vertexArray]); + }, + }; + } +}; + + +function odinSetupDefaultImports(wasmMemoryInterface, consoleElement, memory) { + const MAX_INFO_CONSOLE_LINES = 512; + let infoConsoleLines = new Array(); + let currentLine = {}; + currentLine[false] = ""; + currentLine[true] = ""; + let prevIsError = false; + + let event_temp = {}; + + const onEventReceived = (event_data, data, callback) => { + event_temp.data = event_data; + + const exports = wasmMemoryInterface.exports; + const odin_ctx = exports.default_context_ptr(); + + exports.odin_dom_do_event_callback(data, callback, odin_ctx); + + event_temp.data = null; + }; + + const writeToConsole = (line, isError) => { + if (!line) { + return; + } + + const println = (text, forceIsError) => { + let style = [ + "color: #eee", + "background-color: #d20", + "padding: 2px 4px", + "border-radius: 2px", + ].join(";"); + let doIsError = isError; + if (forceIsError !== undefined) { + doIsError = forceIsError; + } + + if (doIsError) { + console.log("%c"+text, style); + } else { + console.log(text); + } + + }; + + // Print to console + if (line == "\n") { + println(currentLine[isError]); + currentLine[isError] = ""; + } else if (!line.includes("\n")) { + currentLine[isError] = currentLine[isError].concat(line); + } else { + let lines = line.trimEnd().split("\n"); + let printLast = lines.length > 1 && line.endsWith("\n"); + println(currentLine[isError].concat(lines[0])); + currentLine[isError] = ""; + for (let i = 1; i < lines.length-1; i++) { + println(lines[i]); + } + if (lines.length > 1) { + let last = lines[lines.length-1]; + if (printLast) { + println(last); + } else { + currentLine[isError] = last; + } + } + } + + if (prevIsError != isError) { + if (prevIsError) { + println(currentLine[prevIsError], prevIsError); + currentLine[prevIsError] = ""; + } + } + prevIsError = isError; + + + // HTML based console + if (!consoleElement) { + return; + } + const wrap = (x) => { + if (isError) { + return ''+x+''; + } + return x; + }; + + if (line == "\n") { + infoConsoleLines.push(line); + } else if (!line.includes("\n")) { + let prevLine = ""; + if (infoConsoleLines.length > 0) { + prevLine = infoConsoleLines.pop(); + } + infoConsoleLines.push(prevLine.concat(wrap(line))); + } else { + let lines = line.split("\n"); + let lastHasNewline = lines.length > 1 && line.endsWith("\n"); + + let prevLine = ""; + if (infoConsoleLines.length > 0) { + prevLine = infoConsoleLines.pop(); + } + infoConsoleLines.push(prevLine.concat(wrap(lines[0]).concat("\n"))); + + for (let i = 1; i < lines.length-1; i++) { + infoConsoleLines.push(wrap(lines[i]).concat("\n")); + } + let last = lines[lines.length-1]; + if (lastHasNewline) { + infoConsoleLines.push(last.concat("\n")); + } else { + infoConsoleLines.push(last); + } + } + + if (infoConsoleLines.length > MAX_INFO_CONSOLE_LINES) { + infoConsoleLines.shift(MAX_INFO_CONSOLE_LINES); + } + + let data = ""; + for (let i = 0; i < infoConsoleLines.length; i++) { + data = data.concat(infoConsoleLines[i]); + } + + let info = consoleElement; + info.innerHTML = data; + info.scrollTop = info.scrollHeight; + }; + + const listener_key = (id, name, data, callback, useCapture) => { + return `${id}-${name}-data:${data}-callback:${callback}-useCapture:${useCapture}`; + }; + + let webglContext = new WebGLInterface(wasmMemoryInterface); + + const env = {}; + + if (memory) { + env.memory = memory; + } + + return { + env, + "odin_env": { + write: (fd, ptr, len) => { + const str = wasmMemoryInterface.loadString(ptr, len); + if (fd == 1) { + writeToConsole(str, false); + return; + } else if (fd == 2) { + writeToConsole(str, true); + return; + } else { + throw new Error("Invalid fd to 'write'" + stripNewline(str)); + } + }, + trap: () => { throw new Error() }, + alert: (ptr, len) => { alert(wasmMemoryInterface.loadString(ptr, len)) }, + abort: () => { Module.abort() }, + evaluate: (str_ptr, str_len) => { eval.call(null, wasmMemoryInterface.loadString(str_ptr, str_len)); }, + + open: (url_ptr, url_len, name_ptr, name_len, specs_ptr, specs_len) => { + const url = wasmMemoryInterface.loadString(url_ptr, url_len); + const name = wasmMemoryInterface.loadString(name_ptr, name_len); + const specs = wasmMemoryInterface.loadString(specs_ptr, specs_len); + window.open(url, name, specs); + }, + + // return a bigint to be converted to i64 + time_now: () => BigInt(Date.now()), + tick_now: () => performance.now(), + time_sleep: (duration_ms) => { + if (duration_ms > 0) { + // TODO(bill): Does this even make any sense? + } + }, + + sqrt: Math.sqrt, + sin: Math.sin, + cos: Math.cos, + pow: Math.pow, + fmuladd: (x, y, z) => x*y + z, + ln: Math.log, + exp: Math.exp, + ldexp: (x, exp) => x * Math.pow(2, exp), + + rand_bytes: (ptr, len) => { + const view = new Uint8Array(wasmMemoryInterface.memory.buffer, ptr, len) + crypto.getRandomValues(view) + }, + }, + "odin_dom": { + init_event_raw: (ep) => { + const W = wasmMemoryInterface.intSize; + let offset = ep; + let off = (amount, alignment) => { + if (alignment === undefined) { + alignment = Math.min(amount, W); + } + if (offset % alignment != 0) { + offset += alignment - (offset%alignment); + } + let x = offset; + offset += amount; + return x; + }; + + let align = (alignment) => { + const modulo = offset & (alignment-1); + if (modulo != 0) { + offset += alignment - modulo + } + }; + + let wmi = wasmMemoryInterface; + + if (!event_temp.data) { + return; + } + + let e = event_temp.data.event; + + wmi.storeU32(off(4), event_temp.data.name_code); + if (e.target == document) { + wmi.storeU32(off(4), 1); + } else if (e.target == window) { + wmi.storeU32(off(4), 2); + } else { + wmi.storeU32(off(4), 0); + } + if (e.currentTarget == document) { + wmi.storeU32(off(4), 1); + } else if (e.currentTarget == window) { + wmi.storeU32(off(4), 2); + } else { + wmi.storeU32(off(4), 0); + } + + align(W); + + wmi.storeI32(off(W), event_temp.data.id_ptr); + wmi.storeUint(off(W), event_temp.data.id_len); + + align(8); + wmi.storeF64(off(8), e.timeStamp*1e-3); + + wmi.storeU8(off(1), e.eventPhase); + let options = 0; + if (!!e.bubbles) { options |= 1<<0; } + if (!!e.cancelable) { options |= 1<<1; } + if (!!e.composed) { options |= 1<<2; } + wmi.storeU8(off(1), options); + wmi.storeU8(off(1), !!e.isComposing); + wmi.storeU8(off(1), !!e.isTrusted); + + align(8); + if (e instanceof WheelEvent) { + wmi.storeF64(off(8), e.deltaX); + wmi.storeF64(off(8), e.deltaY); + wmi.storeF64(off(8), e.deltaZ); + wmi.storeU32(off(4), e.deltaMode); + } else if (e instanceof MouseEvent) { + wmi.storeI64(off(8), e.screenX); + wmi.storeI64(off(8), e.screenY); + wmi.storeI64(off(8), e.clientX); + wmi.storeI64(off(8), e.clientY); + wmi.storeI64(off(8), e.offsetX); + wmi.storeI64(off(8), e.offsetY); + wmi.storeI64(off(8), e.pageX); + wmi.storeI64(off(8), e.pageY); + wmi.storeI64(off(8), e.movementX); + wmi.storeI64(off(8), e.movementY); + + wmi.storeU8(off(1), !!e.ctrlKey); + wmi.storeU8(off(1), !!e.shiftKey); + wmi.storeU8(off(1), !!e.altKey); + wmi.storeU8(off(1), !!e.metaKey); + + wmi.storeI16(off(2), e.button); + wmi.storeU16(off(2), e.buttons); + + if (e instanceof PointerEvent) { + wmi.storeF64(off(8), e.altitudeAngle); + wmi.storeF64(off(8), e.azimuthAngle); + wmi.storeInt(off(W), e.persistentDeviceId); + wmi.storeInt(off(W), e.pointerId); + wmi.storeInt(off(W), e.width); + wmi.storeInt(off(W), e.height); + wmi.storeF64(off(8), e.pressure); + wmi.storeF64(off(8), e.tangentialPressure); + wmi.storeF64(off(8), e.tiltX); + wmi.storeF64(off(8), e.tiltY); + wmi.storeF64(off(8), e.twist); + if (e.pointerType == "pen") { + wmi.storeU8(off(1), 1); + } else if (e.pointerType == "touch") { + wmi.storeU8(off(1), 2); + } else { + wmi.storeU8(off(1), 0); + } + wmi.storeU8(off(1), !!e.isPrimary); + } + + } else if (e instanceof KeyboardEvent) { + // Note: those strings are constructed + // on the native side from buffers that + // are filled later, so skip them + const keyPtr = off(W*2, W); + const codePtr = off(W*2, W); + + wmi.storeU8(off(1), e.location); + + wmi.storeU8(off(1), !!e.ctrlKey); + wmi.storeU8(off(1), !!e.shiftKey); + wmi.storeU8(off(1), !!e.altKey); + wmi.storeU8(off(1), !!e.metaKey); + + wmi.storeU8(off(1), !!e.repeat); + + wmi.storeI32(off(4), e.charCode); + + wmi.storeInt(off(W, W), e.key.length) + wmi.storeInt(off(W, W), e.code.length) + wmi.storeString(off(32, 1), e.key); + wmi.storeString(off(32, 1), e.code); + } else if (e.type === 'scroll') { + wmi.storeF64(off(8, 8), window.scrollX); + wmi.storeF64(off(8, 8), window.scrollY); + } else if (e.type === 'visibilitychange') { + wmi.storeU8(off(1), !document.hidden); + } else if (e instanceof GamepadEvent) { + const idPtr = off(W*2, W); + const mappingPtr = off(W*2, W); + + wmi.storeI32(off(W, W), e.gamepad.index); + wmi.storeU8(off(1), !!e.gamepad.connected); + wmi.storeF64(off(8, 8), e.gamepad.timestamp); + + wmi.storeInt(off(W, W), e.gamepad.buttons.length); + wmi.storeInt(off(W, W), e.gamepad.axes.length); + + for (let i = 0; i < 64; i++) { + if (i < e.gamepad.buttons.length) { + let b = e.gamepad.buttons[i]; + wmi.storeF64(off(8, 8), b.value); + wmi.storeU8(off(1), !!b.pressed); + wmi.storeU8(off(1), !!b.touched); + } else { + off(16, 8); + } + } + for (let i = 0; i < 16; i++) { + if (i < e.gamepad.axes.length) { + let a = e.gamepad.axes[i]; + wmi.storeF64(off(8, 8), a); + } else { + off(8, 8); + } + } + + let idLength = e.gamepad.id.length; + let id = e.gamepad.id; + if (idLength > 96) { + idLength = 96; + id = id.slice(0, 93) + '...'; + } + + let mappingLength = e.gamepad.mapping.length; + let mapping = e.gamepad.mapping; + if (mappingLength > 64) { + mappingLength = 61; + mapping = mapping.slice(0, 61) + '...'; + } + + wmi.storeInt(off(W, W), idLength); + wmi.storeInt(off(W, W), mappingLength); + wmi.storeString(off(96, 1), id); + wmi.storeString(off(64, 1), mapping); + } + }, + + add_event_listener: (id_ptr, id_len, name_ptr, name_len, name_code, data, callback, use_capture) => { + let id = wasmMemoryInterface.loadString(id_ptr, id_len); + let name = wasmMemoryInterface.loadString(name_ptr, name_len); + let element = getElement(id); + if (element == undefined) { + return false; + } + let key = listener_key(id, name, data, callback, !!use_capture); + if (wasmMemoryInterface.listenerMap.has(key)) { + return false; + } + + let listener = (e) => { + let event_data = {}; + event_data.id_ptr = id_ptr; + event_data.id_len = id_len; + event_data.event = e; + event_data.name_code = name_code; + + onEventReceived(event_data, data, callback); + }; + wasmMemoryInterface.listenerMap.set(key, listener); + element.addEventListener(name, listener, !!use_capture); + return true; + }, + + add_window_event_listener: (name_ptr, name_len, name_code, data, callback, use_capture) => { + let name = wasmMemoryInterface.loadString(name_ptr, name_len); + let element = window; + let key = listener_key('window', name, data, callback, !!use_capture); + if (wasmMemoryInterface.listenerMap.has(key)) { + return false; + } + + let listener = (e) => { + let event_data = {}; + event_data.id_ptr = 0; + event_data.id_len = 0; + event_data.event = e; + event_data.name_code = name_code; + + onEventReceived(event_data, data, callback); + }; + wasmMemoryInterface.listenerMap.set(key, listener); + element.addEventListener(name, listener, !!use_capture); + return true; + }, + + add_document_event_listener: (name_ptr, name_len, name_code, data, callback, use_capture) => { + let name = wasmMemoryInterface.loadString(name_ptr, name_len); + let element = document; + let key = listener_key('document', name, data, callback, !!use_capture); + if (wasmMemoryInterface.listenerMap.has(key)) { + return false; + } + + let listener = (e) => { + let event_data = {}; + event_data.id_ptr = 0; + event_data.id_len = 0; + event_data.event = e; + event_data.name_code = name_code; + + onEventReceived(event_data, data, callback); + }; + wasmMemoryInterface.listenerMap.set(key, listener); + element.addEventListener(name, listener, !!use_capture); + return true; + }, + + remove_event_listener: (id_ptr, id_len, name_ptr, name_len, data, callback, use_capture) => { + let id = wasmMemoryInterface.loadString(id_ptr, id_len); + let name = wasmMemoryInterface.loadString(name_ptr, name_len); + let element = getElement(id); + if (element == undefined) { + return false; + } + + let key = listener_key(id, name, data, callback, !!use_capture); + let listener = wasmMemoryInterface.listenerMap.get(key); + if (listener === undefined) { + return false; + } + wasmMemoryInterface.listenerMap.delete(key); + + element.removeEventListener(name, listener, !!use_capture); + return true; + }, + remove_window_event_listener: (name_ptr, name_len, data, callback, use_capture) => { + let name = wasmMemoryInterface.loadString(name_ptr, name_len); + let element = window; + + let key = listener_key('window', name, data, callback, !!use_capture); + let listener = wasmMemoryInterface.listenerMap.get(key); + if (listener === undefined) { + return false; + } + wasmMemoryInterface.listenerMap.delete(key); + + element.removeEventListener(name, listener, !!use_capture); + return true; + }, + remove_document_event_listener: (name_ptr, name_len, data, callback, use_capture) => { + let name = wasmMemoryInterface.loadString(name_ptr, name_len); + let element = document; + + let key = listener_key('document', name, data, callback, !!use_capture); + let listener = wasmMemoryInterface.listenerMap.get(key); + if (listener === undefined) { + return false; + } + wasmMemoryInterface.listenerMap.delete(key); + + element.removeEventListener(name, listener, !!use_capture); + return true; + }, + + event_stop_propagation: () => { + if (event_temp.data && event_temp.data.event) { + event_temp.data.event.stopPropagation(); + } + }, + event_stop_immediate_propagation: () => { + if (event_temp.data && event_temp.data.event) { + event_temp.data.event.stopImmediatePropagation(); + } + }, + event_prevent_default: () => { + if (event_temp.data && event_temp.data.event) { + event_temp.data.event.preventDefault(); + } + }, + + dispatch_custom_event: (id_ptr, id_len, name_ptr, name_len, options_bits) => { + let id = wasmMemoryInterface.loadString(id_ptr, id_len); + let name = wasmMemoryInterface.loadString(name_ptr, name_len); + let options = { + bubbles: (options_bits & (1<<0)) !== 0, + cancelable: (options_bits & (1<<1)) !== 0, + composed: (options_bits & (1<<2)) !== 0, + }; + + let element = getElement(id); + if (element) { + element.dispatchEvent(new Event(name, options)); + return true; + } + return false; + }, + + get_gamepad_state: (gamepad_id, ep) => { + let index = gamepad_id; + let gps = navigator.getGamepads(); + if (0 <= index && index < gps.length) { + let gamepad = gps[index]; + if (!gamepad) { + return false; + } + + const W = wasmMemoryInterface.intSize; + let offset = ep; + let off = (amount, alignment) => { + if (alignment === undefined) { + alignment = Math.min(amount, W); + } + if (offset % alignment != 0) { + offset += alignment - (offset%alignment); + } + let x = offset; + offset += amount; + return x; + }; + + let align = (alignment) => { + const modulo = offset & (alignment-1); + if (modulo != 0) { + offset += alignment - modulo + } + }; + + let wmi = wasmMemoryInterface; + + const idPtr = off(W*2, W); + const mappingPtr = off(W*2, W); + + wmi.storeI32(off(W), gamepad.index); + wmi.storeU8(off(1), !!gamepad.connected); + wmi.storeF64(off(8), gamepad.timestamp); + + wmi.storeInt(off(W), gamepad.buttons.length); + wmi.storeInt(off(W), gamepad.axes.length); + + for (let i = 0; i < 64; i++) { + if (i < gamepad.buttons.length) { + let b = gamepad.buttons[i]; + wmi.storeF64(off(8, 8), b.value); + wmi.storeU8(off(1), !!b.pressed); + wmi.storeU8(off(1), !!b.touched); + } else { + off(16, 8); + } + } + for (let i = 0; i < 16; i++) { + if (i < gamepad.axes.length) { + wmi.storeF64(off(8, 8), gamepad.axes[i]); + } else { + off(8, 8); + } + } + + let idLength = gamepad.id.length; + let id = gamepad.id; + if (idLength > 96) { + idLength = 96; + id = id.slice(0, 93) + '...'; + } + + let mappingLength = gamepad.mapping.length; + let mapping = gamepad.mapping; + if (mappingLength > 64) { + mappingLength = 61; + mapping = mapping.slice(0, 61) + '...'; + } + + wmi.storeInt(off(W, W), idLength); + wmi.storeInt(off(W, W), mappingLength); + wmi.storeString(off(96, 1), id); + wmi.storeString(off(64, 1), mapping); + + return true; + } + return false; + }, + + get_element_value_f64: (id_ptr, id_len) => { + let id = wasmMemoryInterface.loadString(id_ptr, id_len); + let element = getElement(id); + return element ? element.value : 0; + }, + get_element_value_string: (id_ptr, id_len, buf_ptr, buf_len) => { + let id = wasmMemoryInterface.loadString(id_ptr, id_len); + let element = getElement(id); + if (element) { + let str = element.value; + if (buf_len > 0 && buf_ptr) { + let n = Math.min(buf_len, str.length); + str = str.substring(0, n); + this.mem.loadBytes(buf_ptr, buf_len).set(new TextEncoder().encode(str)) + return n; + } + } + return 0; + }, + get_element_value_string_length: (id_ptr, id_len) => { + let id = wasmMemoryInterface.loadString(id_ptr, id_len); + let element = getElement(id); + if (element) { + return element.value.length; + } + return 0; + }, + get_element_min_max: (ptr_array2_f64, id_ptr, id_len) => { + let id = wasmMemoryInterface.loadString(id_ptr, id_len); + let element = getElement(id); + if (element) { + let values = wasmMemoryInterface.loadF64Array(ptr_array2_f64, 2); + values[0] = element.min; + values[1] = element.max; + } + }, + set_element_value_f64: (id_ptr, id_len, value) => { + let id = wasmMemoryInterface.loadString(id_ptr, id_len); + let element = getElement(id); + if (element) { + element.value = value; + } + }, + set_element_value_string: (id_ptr, id_len, value_ptr, value_len) => { + let id = wasmMemoryInterface.loadString(id_ptr, id_len); + let value = wasmMemoryInterface.loadString(value_ptr, value_len); + let element = getElement(id); + if (element) { + element.value = value; + } + }, + + set_element_style: (id_ptr, id_len, key_ptr, key_len, value_ptr, value_len) => { + let id = wasmMemoryInterface.loadString(id_ptr, id_len); + let key = wasmMemoryInterface.loadString(key_ptr, key_len); + let value = wasmMemoryInterface.loadString(value_ptr, value_len); + let element = getElement(id); + if (element) { + element.style[key] = value; + } + }, + + get_element_key_f64: (id_ptr, id_len, key_ptr, key_len) => { + let id = wasmMemoryInterface.loadString(id_ptr, id_len); + let key = wasmMemoryInterface.loadString(key_ptr, key_len); + let element = getElement(id); + return element ? element[key] : 0; + }, + get_element_key_string: (id_ptr, id_len, key_ptr, key_len, buf_ptr, buf_len) => { + let id = wasmMemoryInterface.loadString(id_ptr, id_len); + let key = wasmMemoryInterface.loadString(key_ptr, key_len); + let element = getElement(id); + if (element) { + let str = element[key]; + if (buf_len > 0 && buf_ptr) { + let n = Math.min(buf_len, str.length); + str = str.substring(0, n); + this.mem.loadBytes(buf_ptr, buf_len).set(new TextEncoder().encode(str)) + return n; + } + } + return 0; + }, + get_element_key_string_length: (id_ptr, id_len, key_ptr, key_len) => { + let id = wasmMemoryInterface.loadString(id_ptr, id_len); + let key = wasmMemoryInterface.loadString(key_ptr, key_len); + let element = getElement(id); + if (element && element[key]) { + return element[key].length; + } + return 0; + }, + + set_element_key_f64: (id_ptr, id_len, key_ptr, key_len, value) => { + let id = wasmMemoryInterface.loadString(id_ptr, id_len); + let key = wasmMemoryInterface.loadString(key_ptr, key_len); + let element = getElement(id); + if (element) { + element[key] = value; + } + }, + set_element_key_string: (id_ptr, id_len, key_ptr, key_len, value_ptr, value_len) => { + let id = wasmMemoryInterface.loadString(id_ptr, id_len); + let key = wasmMemoryInterface.loadString(key_ptr, key_len); + let value = wasmMemoryInterface.loadString(value_ptr, value_len); + let element = getElement(id); + if (element) { + element[key] = value; + } + }, + + + get_bounding_client_rect: (rect_ptr, id_ptr, id_len) => { + let id = wasmMemoryInterface.loadString(id_ptr, id_len); + let element = getElement(id); + if (element) { + let values = wasmMemoryInterface.loadF64Array(rect_ptr, 4); + let rect = element.getBoundingClientRect(); + values[0] = rect.left; + values[1] = rect.top; + values[2] = rect.right - rect.left; + values[3] = rect.bottom - rect.top; + } + }, + window_get_rect: (rect_ptr) => { + let values = wasmMemoryInterface.loadF64Array(rect_ptr, 4); + values[0] = window.screenX; + values[1] = window.screenY; + values[2] = window.screen.width; + values[3] = window.screen.height; + }, + + window_get_scroll: (pos_ptr) => { + let values = wasmMemoryInterface.loadF64Array(pos_ptr, 2); + values[0] = window.scrollX; + values[1] = window.scrollY; + }, + window_set_scroll: (x, y) => { + window.scroll(x, y); + }, + + device_pixel_ratio: () => { + return window.devicePixelRatio; + }, + + }, + + "webgl": webglContext.getWebGL1Interface(), + "webgl2": webglContext.getWebGL2Interface(), + }; +}; + +/** + * @param {string} wasmPath - Path to the WASM module to run + * @param {?HTMLPreElement} consoleElement - Optional console/pre element to append output to, in addition to the console + * @param {any} extraForeignImports - Imports, in addition to the default runtime to provide the module + * @param {?WasmMemoryInterface} wasmMemoryInterface - Optional memory to use instead of the defaults + * @param {?int} intSize - Size (in bytes) of the integer type, should be 4 on `js_wasm32` and 8 on `js_wasm64p32` + */ +async function runWasm(wasmPath, consoleElement, extraForeignImports, wasmMemoryInterface, intSize = 4) { + if (!wasmMemoryInterface) { + wasmMemoryInterface = new WasmMemoryInterface(); + } + wasmMemoryInterface.setIntSize(intSize); + + let imports = odinSetupDefaultImports(wasmMemoryInterface, consoleElement, wasmMemoryInterface.memory); + let exports = {}; + + if (extraForeignImports !== undefined) { + imports = { + ...imports, + ...extraForeignImports, + }; + } + + const response = await fetch(wasmPath); + const file = await response.arrayBuffer(); + const wasm = await WebAssembly.instantiate(file, imports); + exports = wasm.instance.exports; + wasmMemoryInterface.setExports(exports); + + if (exports.memory) { + if (wasmMemoryInterface.memory) { + console.warn("WASM module exports memory, but `runWasm` was given an interface with existing memory too"); + } + wasmMemoryInterface.setMemory(exports.memory); + } + + exports._start(); + + // Define a `@export step :: proc(delta_time: f64) -> (keep_going: bool) {` + // in your app and it will get called every frame. + // return `false` to stop the execution of the module. + if (exports.step) { + const odin_ctx = exports.default_context_ptr(); + + let prevTimeStamp = undefined; + function step(currTimeStamp) { + if (prevTimeStamp == undefined) { + prevTimeStamp = currTimeStamp; + } + + const dt = (currTimeStamp - prevTimeStamp)*0.001; + prevTimeStamp = currTimeStamp; + + if (!exports.step(dt, odin_ctx)) { + exports._end(); + return; + } + + window.requestAnimationFrame(step); + } + + window.requestAnimationFrame(step); + } else { + exports._end(); + } + + return; +}; + +window.odin = { + // Interface Types + WasmMemoryInterface: WasmMemoryInterface, + WebGLInterface: WebGLInterface, + + // Functions + setupDefaultImports: odinSetupDefaultImports, + runWasm: runWasm, +}; +})(); diff --git a/wgpu/sdl3/game-of-life/web/wgpu.js b/wgpu/sdl3/game-of-life/web/wgpu.js new file mode 100644 index 0000000..6104417 --- /dev/null +++ b/wgpu/sdl3/game-of-life/web/wgpu.js @@ -0,0 +1,3311 @@ +(function() { + +const STATUS_SUCCESS = 1; +const STATUS_ERROR = 2; + +const ENUMS = { + FeatureName: [undefined, "depth-clip-control", "depth32float-stencil8", "timestamp-query", "texture-compression-bc", "texture-compression-bc-sliced-3d", "texture-compression-etc2", "texture-compression-astc", "texture-compression-astc-sliced-3d", "indirect-first-instance", "shader-f16", "rg11b10ufloat-renderable", "bgra8unorm-storage", "float32-filterable", "float32-blendable", "clip-distances", "dual-source-blending" ], + StoreOp: [undefined, "store", "discard", ], + LoadOp: [undefined, "load", "clear", ], + BufferBindingType: [null, undefined, "uniform", "storage", "read-only-storage", ], + SamplerBindingType: [null, undefined, "filtering", "non-filtering", "comparison", ], + TextureSampleType: [null, undefined, "float", "unfilterable-float", "depth", "sint", "uint", ], + TextureViewDimension: [undefined, "1d", "2d", "2d-array", "cube", "cube-array", "3d", ], + StorageTextureAccess: [null, undefined, "write-only", "read-only", "read-write", ], + TextureFormat: [undefined, "r8unorm", "r8snorm", "r8uint", "r8sint", "r16uint", "r16sint", "r16float", "rg8unorm", "rg8snorm", "rg8uint", "rg8sint", "r32float", "r32uint", "r32sint", "rg16uint", "rg16sint", "rg16float", "rgba8unorm", "rgba8unorm-srgb", "rgba8snorm", "rgba8uint", "rgba8sint", "bgra8unorm", "bgra8unorm-srgb", "rgb10a2uint", "rgb10a2unorm", "rg11b10ufloat", "rgb9e5ufloat", "rg32float", "rg32uint", "rg32sint", "rgba16uint", "rgba16sint", "rgba16float", "rgba32float", "rgba32uint", "rgba32sint", "stencil8", "depth16unorm", "depth24plus", "depth24plus-stencil8", "depth32float", "depth32float-stencil8", "bc1-rgba-unorm", "bc1-rgba-unorm-srgb", "bc2-rgba-unorm", "bc2-rgba-unorm-srgb", "bc3-rgba-unorm", "bc3-rgba-unorm-srgb", "bc4-r-unorm", "bc4-r-snorm", "bc5-rg-unorm", "bc5-rg-snorm", "bc6h-rgb-ufloat", "bc6h-rgb-float", "bc7-rgba-unorm", "bc7-rgba-unorm-srgb", "etc2-rgb8unorm", "etc2-rgb8unorm-srgb", "etc2-rgb8a1unorm", "etc2-rgb8a1unorm-srgb", "etc2-rgba8unorm", "etc2-rgba8unorm-srgb", "eac-r11unorm", "eac-r11snorm", "eac-rg11unorm", "eac-rg11snorm", "astc-4x4-unorm", "astc-4x4-unorm-srgb", "astc-5x4-unorm", "astc-5x4-unorm-srgb", "astc-5x5-unorm", "astc-5x5-unorm-srgb", "astc-6x5-unorm", "astc-6x5-unorm-srgb", "astc-6x6-unorm", "astc-6x6-unorm-srgb", "astc-8x5-unorm", "astc-8x5-unorm-srgb", "astc-8x6-unorm", "astc-8x6-unorm-srgb", "astc-8x8-unorm", "astc-8x8-unorm-srgb", "astc-10x5-unorm", "astc-10x5-unorm-srgb", "astc-10x6-unorm", "astc-10x6-unorm-srgb", "astc-10x8-unorm", "astc-10x8-unorm-srgb", "astc-10x10-unorm", "astc-10x10-unorm-srgb", "astc-12x10-unorm", "astc-12x10-unorm-srgb", "astc-12x12-unorm", "astc-12x12-unorm-srgb", ], + QueryType: [undefined, "occlusion", "timestamp", ], + VertexStepMode: [null, undefined, "vertex", "instance", ], + VertexFormat: [undefined, "uint8", "uint8x2", "uint8x4", "sint8", "sint8x2", "sint8x4", "unorm8", "unorm8x2", "unorm8x4", "snorm8", "snorm8x2", "snorm8x4", "uint16", "uint16x2", "uint16x4", "sint16", "sint16x2", "sint16x4", "unorm16", "unorm16x2", "unorm16x4", "snorm16", "snorm16x2", "snorm16x4", "float16", "float16x2", "float16x4", "float32", "float32x2", "float32x3", "float32x4", "uint32", "uint32x2", "uint32x3", "uint32x4", "sint32", "sint32x2", "sint32x3", "sint32x4", "unorm10-10-2", "unorm8x4-bgra" ], + PrimitiveTopology: [undefined, "point-list", "line-list", "line-strip", "triangle-list", "triangle-strip", ], + IndexFormat: [undefined, "uint16", "uint32", ], + FrontFace: [undefined, "ccw", "cw", ], + CullMode: [undefined, "none", "front", "back", ], + AddressMode: [undefined, "clamp-to-edge", "repeat", "mirror-repeat", ], + FilterMode: [undefined, "nearest", "linear", ], + MipmapFilterMode: [undefined, "nearest", "linear", ], + CompareFunction: [undefined, "never", "less", "equal", "less-equal", "greater", "not-equal", "greater-equal", "always", ], + TextureDimension: [undefined, "1d", "2d", "3d", ], + ErrorType: [undefined, "no-error", "validation", "out-of-memory", "internal", "unknown", ], + WGSLLanguageFeatureName: [undefined, "readonly_and_readwrite_storage_textures", "packed_4x8_integer_dot_product", "unrestricted_pointer_parameters", "pointer_composite_access", ], + PowerPreference: [undefined, "low-power", "high-performance", ], + CompositeAlphaMode: ["auto", "opaque", "premultiplied", "unpremultiplied", "inherit", ], + StencilOperation: [undefined, "keep", "zero", "replace", "invert", "increment-clamp", "decrement-clamp", "increment-wrap", "decrement-wrap", ], + BlendOperation: ["add", "subtract", "reverse-subtract", "min", "max", ], + BlendFactor: [undefined, "zero", "one", "src", "one-minus-src", "src-alpha", "one-minus-src-alpha", "dst", "one-minus-dst", "dst-alpha", "one-minus-dst-alpha", "src-alpha-saturated", "constant", "one-minus-constant", "src1", "one-minus-src1", "src1-alpha", "one-minus-src1-alpha" ], + PresentMode: [undefined, "fifo", "fifo-relaxed", "immediate", "mailbox", ], + TextureAspect: [undefined, "all", "stencil-only", "depth-only"], + DeviceLostReason: [undefined, "unknown", "destroyed", "instance-dropped", "failed-creation"], + BufferMapState: [undefined, "unmapped", "pending", "mapped"], + OptionalBool: [false, true, undefined], + + // WARN: used with indexOf to pass to WASM, if we would pass to JS, this needs to use official naming convention (not like Odin enums) like the ones above. + BackendType: [undefined, null, "WebGPU", "D3D11", "D3D12", "Metal", "Vulkan", "OpenGL", "OpenGLES"], + AdapterType: [undefined, "DiscreteGPU", "IntegratedGPU", "CPU", "Unknown"], + RequestDeviceStatus: [undefined, "Success", "InstanceDropped", "Error", "Unknown"], + MapAsyncStatus: [undefined, "Success", "InstanceDropped", "Error", "Aborted", "Unknown"], + CreatePipelineAsyncStatus: [undefined, "Success", "InstanceDropped", "ValidationError", "InternalError", "Unknown"], + PopErrorScopeStatus: [undefined, "Success", "InstanceDropped", "EmptyStack"], + RequestAdapterStatus: [undefined, "Success", "InstanceDropped", "Unavailable", "Error", "Unknown"], + QueueWorkDoneStatus: [undefined, "Success", "InstanceDropped", "Error", "Unknown"], + CompilationInfoRequestStatus: [undefined, "Success", "InstanceDropped", "Error", "Unknown"], +}; + +/** + * Assumptions: + * - Ability to allocate memory, set the context to allocate with using the global `wgpu.g_context` + * - Exports a function table (for callbacks), added with `-extra-linker-flags:"--export-table"` + */ +class WebGPUInterface { + + /** + * @param {WasmMemoryInterface} mem + */ + constructor(mem) { + this.mem = mem; + + this.sizes = { + Color: [32, 8], + BufferBindingLayout: [24, 8], + SamplerBindingLayout: [8, 4], + TextureBindingLayout: [16, 4], + StorageTextureBindingLayout: [16, 4], + StringView: [2*this.mem.intSize, this.mem.intSize], + ConstantEntry: [this.mem.intSize === 8 ? 32 : 24, 8], + ProgrammableStageDescriptor: [8 + this.mem.intSize*4, this.mem.intSize], + VertexBufferLayout: [16 + this.mem.intSize*2, 8], + VertexAttribute: [24, 8], + VertexState: [8 + this.mem.intSize*6, this.mem.intSize], + PrimitiveState: [24, 4], + MultisampleState: [16, 4], + StencilFaceState: [16, 4], + ColorTargetState: [24, 8], + BlendComponent: [12, 4], + TexelCopyBufferLayout: [16, 8], + Origin3D: [12, 4], + QueueDescriptor: [this.mem.intSize*3, this.mem.intSize], + CallbackInfo: [20, 4], + UncapturedErrorCallbackInfo: [16, 4], + RenderPassColorAttachment: [56, 8], + BindGroupEntry: [40, 8], + BindGroupLayoutEntry: [80, 8], + Extent3D: [12, 4], + CompilationMessage: [this.mem.intSize == 8 ? 64 : 48, 8], + }; + + /** @type {WebGPUObjectManager<{}>} */ + this.instances = new WebGPUObjectManager("Instance", this.mem); + + /** @type {WebGPUObjectManager} */ + this.adapters = new WebGPUObjectManager("Adapter", this.mem); + + /** @type {WebGPUObjectManager} */ + this.bindGroups = new WebGPUObjectManager("BindGroup", this.mem); + + /** @type {WebGPUObjectManager} */ + this.bindGroupLayouts = new WebGPUObjectManager("BindGroupLayout", this.mem); + + /** @type {WebGPUObjectManager<{ buffer: GPUBuffer, mapping: ?{ range: ArrayBuffer, ptr: number, size: number } }>} */ + this.buffers = new WebGPUObjectManager("Buffer", this.mem); + + /** @type {WebGPUObjectManager} */ + this.devices = new WebGPUObjectManager("Device", this.mem); + + /** @type {WebGPUObjectManager} */ + this.commandBuffers = new WebGPUObjectManager("CommandBuffer", this.mem); + + /** @type {WebGPUObjectManager} */ + this.commandEncoders = new WebGPUObjectManager("CommandEncoder", this.mem); + + /** @type {WebGPUObjectManager} */ + this.computePassEncoders = new WebGPUObjectManager("ComputePassEncoder", this.mem); + + /** @type {WebGPUObjectManager} */ + this.renderPassEncoders = new WebGPUObjectManager("RenderPassEncoder", this.mem); + + /** @type {WebGPUObjectManager} */ + this.querySets = new WebGPUObjectManager("QuerySet", this.mem); + + /** @type {WebGPUObjectManager} */ + this.computePipelines = new WebGPUObjectManager("ComputePipeline", this.mem); + + /** @type {WebGPUObjectManager} */ + this.pipelineLayouts = new WebGPUObjectManager("PipelineLayout", this.mem); + + /** @type {WebGPUObjectManager} */ + this.queues = new WebGPUObjectManager("Queue", this.mem); + + /** @type {WebGPUObjectManager} */ + this.renderBundles = new WebGPUObjectManager("RenderBundle", this.mem); + + /** @type {WebGPUObjectManager} */ + this.renderBundleEncoders = new WebGPUObjectManager("RenderBundleEncoder", this.mem); + + /** @type {WebGPUObjectManager} */ + this.renderPipelines = new WebGPUObjectManager("RenderPipeline", this.mem); + + /** @type {WebGPUObjectManager} */ + this.samplers = new WebGPUObjectManager("Sampler", this.mem); + + /** @type {WebGPUObjectManager} */ + this.shaderModules = new WebGPUObjectManager("ShaderModule", this.mem); + + /** @type {WebGPUObjectManager} */ + this.surfaces = new WebGPUObjectManager("Surface", this.mem); + + /** @type {WebGPUObjectManager} */ + this.textures = new WebGPUObjectManager("Texture", this.mem); + + /** @type {WebGPUObjectManager} */ + this.textureViews = new WebGPUObjectManager("TextureView", this.mem); + + this.zeroMessageAddr = 0; + } + + struct(start) { + let offset = start; + + return (size, alignment = null) => { + if (alignment === null) { + if (Array.isArray(size)) { + [size, alignment] = size; + } else { + alignment = size; + } + } + + // Align the offset to the required boundary + offset = Math.ceil(offset / alignment) * alignment; + let currentOffset = offset; + offset += size; + + return currentOffset; + }; + } + + /** + * @param {number|BigInt} src + * @returns {number|BigInt} + */ + uint(src) { + if (this.mem.intSize == 8) { + return BigInt(src); + } else if (this.mem.intSize == 4) { + return src; + } else { + throw new Error("unreachable"); + } + } + + /** + * @param {number|BigInt} src + * @returns {number} + */ + unwrapBigInt(src) { + if (typeof src == "number") { + return src; + } + + const MAX_SAFE_INTEGER = 9007199254740991n; + if (typeof src != "bigint") { + throw new TypeError(`unwrapBigInt got invalid param of type ${typeof src}`); + } + + if (src > MAX_SAFE_INTEGER) { + throw new Error(`unwrapBigInt precision would be lost converting ${src}`); + } + + return Number(src); + } + + /** + * @param {boolean} condition + * @param {string} message + */ + assert(condition, message = "assertion failure") { + if (!condition) { + throw new Error(message); + } + } + + /** + * @template T + * + * @param {number} count + * @param {number} start + * @param {function(number): T} decoder + * @param {number} stride + * @returns {Array} + */ + array(count, start, decoder, stride) { + if (count == 0) { + return []; + } + this.assert(start != 0); + + const out = []; + for (let i = 0; i < count; i += 1) { + out.push(decoder.call(this, start)); + start += stride; + } + return out; + } + + /** + * @param {string} name + * @param {number} ptr + * @returns {`GPU${name}`} + */ + enumeration(name, ptr) { + const int = this.mem.loadI32(ptr); + return ENUMS[name][int]; + } + + /** + * @param {GPUSupportedFeatures} features + * @param {number} ptr + */ + genericGetFeatures(features, ptr) { + this.assert(ptr != 0); + + const availableFeatures = []; + ENUMS.FeatureName.forEach((feature, value) => { + if (!feature) { + return; + } + + if (features.has(feature)) { + availableFeatures.push(value); + } + }); + + if (availableFeatures.length === 0) { + return; + } + + const featuresAddr = this.mem.exports.wgpu_alloc(availableFeatures.length * 4); + this.assert(featuresAddr != 0); + + let off = this.struct(ptr); + this.mem.storeUint(off(this.mem.intSize), availableFeatures.length); + this.mem.storeI32(off(4), featuresAddr); + + off = this.struct(featuresAddr); + for (let i = 0; i < availableFeatures.length; i += 1) { + this.mem.storeI32(off(4), availableFeatures[i]); + } + } + + /** + * @param {GPUSupportedLimits} limits + * @param {number} ptr + * @returns {number} + */ + genericGetLimits(limits, supportedLimitsPtr) { + this.assert(supportedLimitsPtr != 0); + + const off = this.struct(supportedLimitsPtr); + off(4); + + this.mem.storeU32(off(4), limits.maxTextureDimension1D); + this.mem.storeU32(off(4), limits.maxTextureDimension2D); + this.mem.storeU32(off(4), limits.maxTextureDimension3D); + this.mem.storeU32(off(4), limits.maxTextureArrayLayers); + this.mem.storeU32(off(4), limits.maxBindGroups); + this.mem.storeU32(off(4), limits.maxBindGroupsPlusVertexBuffers); + this.mem.storeU32(off(4), limits.maxBindingsPerBindGroup); + this.mem.storeU32(off(4), limits.maxDynamicUniformBuffersPerPipelineLayout); + this.mem.storeU32(off(4), limits.maxDynamicStorageBuffersPerPipelineLayout); + this.mem.storeU32(off(4), limits.maxSampledTexturesPerShaderStage); + this.mem.storeU32(off(4), limits.maxSamplersPerShaderStage); + this.mem.storeU32(off(4), limits.maxStorageBuffersPerShaderStage); + this.mem.storeU32(off(4), limits.maxStorageTexturesPerShaderStage); + this.mem.storeU32(off(4), limits.maxUniformBuffersPerShaderStage); + this.mem.storeU64(off(8), limits.maxUniformBufferBindingSize); + this.mem.storeU64(off(8), limits.maxStorageBufferBindingSize); + this.mem.storeU32(off(4), limits.minUniformBufferOffsetAlignment); + this.mem.storeU32(off(4), limits.minStorageBufferOffsetAlignment); + this.mem.storeU32(off(4), limits.maxVertexBuffers); + this.mem.storeU64(off(8), limits.maxBufferSize); + this.mem.storeU32(off(4), limits.maxVertexAttributes); + this.mem.storeU32(off(4), limits.maxVertexBufferArrayStride); + this.mem.storeU32(off(4), limits.maxInterStageShaderVariables); + this.mem.storeU32(off(4), limits.maxColorAttachments); + this.mem.storeU32(off(4), limits.maxColorAttachmentBytesPerSample); + this.mem.storeU32(off(4), limits.maxComputeWorkgroupStorageSize); + this.mem.storeU32(off(4), limits.maxComputeInvocationsPerWorkgroup); + this.mem.storeU32(off(4), limits.maxComputeWorkgroupSizeX); + this.mem.storeU32(off(4), limits.maxComputeWorkgroupSizeY); + this.mem.storeU32(off(4), limits.maxComputeWorkgroupSizeZ); + this.mem.storeU32(off(4), limits.maxComputeWorkgroupsPerDimension); + + return STATUS_SUCCESS; + } + + genericGetAdapterInfo(infoPtr) { + this.assert(infoPtr != 0); + + const off = this.struct(infoPtr); + off(4); // nextInChain + off(this.sizes.StringView); // vendor + off(this.sizes.StringView); // architecture + off(this.sizes.StringView); // device + off(this.sizes.StringView); // description + + this.mem.storeI32(off(4), ENUMS.BackendType.indexOf("WebGPU")); + this.mem.storeI32(off(4), ENUMS.AdapterType.indexOf("Unknown")); + + // NOTE: I don't think getting the other fields in this struct is possible. + // `adapter.requestAdapterInfo` is deprecated. + + return STATUS_SUCCESS; + } + + /** + * @param {number} ptr + * @returns {GPUFeatureName} + */ + FeatureNamePtr(ptr) { + return this.FeatureName(this.mem.loadI32(ptr)); + } + + /** + * @param {number} featureInt + * @returns {GPUFeatureName} + */ + FeatureName(featureInt) { + return ENUMS.FeatureName[featureInt]; + } + + /** + * @param {number} ptr + * @returns {GPUSupportedLimits} + */ + RequiredLimitsPtr(ptr) { + const start = this.mem.loadPtr(ptr); + if (start == 0) { + return undefined; + } + + return this.Limits(start + 8); + } + + /** + * @param {number} start + * @return {GPUSupportedLimits} + */ + Limits(start) { + const limitU32 = (ptr) => { + const value = this.mem.loadU32(ptr); + if (value == 0xFFFFFFFF) { // LIMIT_32_UNDEFINED. + return undefined; + } + return value; + }; + + const limitU64 = (ptr) => { + const part1 = this.mem.loadU32(ptr); + const part2 = this.mem.loadU32(ptr + 4); + if (part1 != 0xFFFFFFFF || part2 != 0xFFFFFFFF) { // LIMIT_64_UNDEFINED. + return this.mem.loadU64(ptr); + } + return undefined; + }; + + const off = this.struct(start); + off(4); + + return { + maxTextureDimension1D: limitU32(off(4)), + maxTextureDimension2D: limitU32(off(4)), + maxTextureDimension3D: limitU32(off(4)), + maxTextureArrayLayers: limitU32(off(4)), + maxBindGroups: limitU32(off(4)), + maxBindGroupsPlusVertexBuffers: limitU32(off(4)), + maxBindingsPerBindGroup: limitU32(off(4)), + maxDynamicUniformBuffersPerPipelineLayout: limitU32(off(4)), + maxDynamicStorageBuffersPerPipelineLayout: limitU32(off(4)), + maxSampledTexturesPerShaderStage: limitU32(off(4)), + maxSamplersPerShaderStage: limitU32(off(4)), + maxStorageBuffersPerShaderStage: limitU32(off(4)), + maxStorageTexturesPerShaderStage: limitU32(off(4)), + maxUniformBuffersPerShaderStage: limitU32(off(4)), + maxUniformBufferBindingSize: limitU64(off(8)), + maxStorageBufferBindingSize: limitU64(off(8)), + minUniformBufferOffsetAlignment: limitU32(off(4)), + minStorageBufferOffsetAlignment: limitU32(off(4)), + maxVertexBuffers: limitU32(off(4)), + maxBufferSize: limitU64(off(8)), + maxVertexAttributes: limitU32(off(4)), + maxVertexBufferArrayStride: limitU32(off(4)), + maxInterStageShaderVariables: limitU32(off(4)), + maxColorAttachments: limitU32(off(4)), + maxColorAttachmentBytesPerSample: limitU32(off(4)), + maxComputeWorkgroupStorageSize: limitU32(off(4)), + maxComputeInvocationsPerWorkgroup: limitU32(off(4)), + maxComputeWorkgroupSizeX: limitU32(off(4)), + maxComputeWorkgroupSizeY: limitU32(off(4)), + maxComputeWorkgroupSizeZ: limitU32(off(4)), + maxComputeWorkgroupsPerDimension: limitU32(off(4)), + }; + } + + /** + * @param {number} start + * @returns {GPUQueueDescriptor} + */ + QueueDescriptor(start) { + return { + label: this.StringView(start + 4), + }; + } + + /** + * @param {number} ptr + * @returns {GPUComputePassTimestampWrites} + */ + ComputePassTimestampWritesPtr(ptr) { + const start = this.mem.loadPtr(ptr); + if (start == 0) { + return undefined; + } + + const off = this.struct(start); + return { + querySet: this.querySets.get(this.mem.loadPtr(off(4))), + beginningOfPassWriteIndex: this.mem.loadU32(off(4)), + endOfPassWriteIndex: this.mem.loadU32(off(4)), + }; + } + + /** + * @param {number} start + * @returns {GPURenderPassColorAttachment} + */ + RenderPassColorAttachment(start) { + const off = this.struct(start); + off(4); + + const viewIdx = this.mem.loadPtr(off(4)); + + let depthSlice = this.mem.loadU32(off(4)); + if (depthSlice == 0xFFFFFFFF) { // DEPTH_SLICE_UNDEFINED. + depthSlice = undefined; + } + + const resolveTargetIdx = this.mem.loadPtr(off(4)); + + return { + view: viewIdx > 0 ? this.textureViews.get(viewIdx) : undefined, + resolveTarget: resolveTargetIdx > 0 ? this.textureViews.get(resolveTargetIdx) : undefined, + depthSlice: depthSlice, + loadOp: this.enumeration("LoadOp", off(4)), + storeOp: this.enumeration("StoreOp", off(4)), + clearValue: this.Color(off(this.sizes.Color)), + }; + } + + /** + * @param {number} start + * @returns {GPUColor} + */ + Color(start) { + const off = this.struct(start); + return { + r: this.mem.loadF64(off(8)), + g: this.mem.loadF64(off(8)), + b: this.mem.loadF64(off(8)), + a: this.mem.loadF64(off(8)), + }; + } + + /** + * @param {number} ptr + * @returns {GPURenderPassDepthStencilAttachment} + */ + RenderPassDepthStencilAttachmentPtr(ptr) { + const start = this.mem.loadPtr(ptr); + if (start == 0) { + return undefined; + } + + const off = this.struct(start); + + return { + view: this.textureViews.get(this.mem.loadPtr(off(4))), + depthLoadOp: this.enumeration("LoadOp", off(4)), + depthStoreOp: this.enumeration("StoreOp", off(4)), + depthClearValue: this.mem.loadF32(off(4)), + depthReadOnly: this.mem.loadB32(off(4)), + stencilLoadOp: this.enumeration("LoadOp", off(4)), + stencilStoreOp: this.enumeration("StoreOp", off(4)), + stencilClearValue: this.mem.loadF32(off(4)), + stencilReadOnly: this.mem.loadB32(off(4)), + }; + } + + /** + * @param {number} ptr + * @returns {undefined|GPUQuerySet} + */ + QuerySet(ptr) { + ptr = this.mem.loadPtr(ptr); + if (ptr == 0) { + return undefined; + } + + return this.querySets.get(ptr); + } + + /** + * @param {number} ptr + * @returns {GPURenderPassTimestampWrites} + */ + RenderPassTimestampWritesPtr(ptr) { + return this.ComputePassTimestampWritesPtr(ptr); + } + + /** + * @param {number} start + * @returns {GPUOrigin3D} + */ + Origin3D(start) { + return { + x: this.mem.loadU32(start + 0), + y: this.mem.loadU32(start + 4), + z: this.mem.loadU32(start + 8), + }; + } + + /** + * @param {number} start + * @returns {GPUExtent3D} + */ + Extent3D(start) { + return { + width: this.mem.loadU32(start + 0), + height: this.mem.loadU32(start + 4), + depthOrArrayLayers: this.mem.loadU32(start + 8), + }; + } + + /** + * @param {number} start + * @returns {GPUBindGroupEntry} + */ + BindGroupEntry(start) { + const buffer = this.mem.loadPtr(start + 8); + const sampler = this.mem.loadPtr(start + 32); + const textureView = this.mem.loadPtr(start + 36); + + /** @type {GPUBindingResource} */ + let resource; + if (buffer > 0) { + resource = { + buffer: this.buffers.get(buffer).buffer, + offset: this.mem.loadU64(start + 16), + size: this.mem.loadU64(start + 24), + } + } else if (sampler > 0) { + resource = this.samplers.get(sampler); + } else if (textureView > 0) { + resource = this.textureViews.get(textureView); + } + + return { + binding: this.mem.loadU32(start + 4), + resource: resource, + }; + } + + /** + * @param {number} start + * @returns {GPUBindGroupLayoutEntry} + */ + BindGroupLayoutEntry(start) { + const off = this.struct(start); + off(4); + + const entry = { + binding: this.mem.loadU32(off(4)), + visibility: this.mem.loadU64(off(8)), + buffer: this.BufferBindingLayout(off(this.sizes.BufferBindingLayout)), + sampler: this.SamplerBindingLayout(off(this.sizes.SamplerBindingLayout)), + texture: this.TextureBindingLayout(off(this.sizes.TextureBindingLayout)), + storageTexture: this.StorageTextureBindingLayout(off(this.sizes.StorageTextureBindingLayout)), + }; + if (!entry.buffer.type) { + entry.buffer = undefined; + } + if (!entry.sampler.type) { + entry.sampler = undefined; + } + if (!entry.texture.sampleType) { + entry.texture = undefined; + } + if (!entry.storageTexture.access) { + entry.storageTexture = undefined; + } + return entry; + } + + /** + * @param {number} start + * @returns {GPUBufferBindingLayout} + */ + BufferBindingLayout(start) { + return { + type: this.enumeration("BufferBindingType", start + 4), + hasDynamicOffset: this.mem.loadB32(start + 8), + minBindingSize: this.mem.loadU64(start + 16), + }; + } + + /** + * @param {number} start + * @returns {GPUSamplerBindingLayout} + */ + SamplerBindingLayout(start) { + return { + type: this.enumeration("SamplerBindingType", start + 4), + }; + } + + /** + * @param {number} start + * @returns {GPUTextureBindingLayout} + */ + TextureBindingLayout(start) { + return { + sampleType: this.enumeration("TextureSampleType", start + 4), + viewDimension: this.enumeration("TextureViewDimension", start + 8), + multisampled: this.mem.loadB32(start + 12), + }; + } + + /** + * @param {number} start + * @returns {GPUStorageTextureBindingLayout} + */ + StorageTextureBindingLayout(start) { + return { + access: this.enumeration("StorageTextureAccess", start + 4), + format: this.enumeration("TextureFormat", start + 8), + viewDimension: this.enumeration("TextureViewDimension", start + 12), + }; + } + + /** + * @param {number} start + * @returns {GPUProgrammableStage} + */ + ProgrammableStageDescriptor(start) { + const off = this.struct(start); + off(4); + + const shaderModule = this.shaderModules.get(this.mem.loadPtr(off(4))); + const entryPoint = this.StringView(off(this.sizes.StringView)); + + const constantsArray = this.array( + this.mem.loadUint(off(this.mem.intSize)), + this.mem.loadPtr(off(4)), + this.ConstantEntry, + this.sizes.ConstantEntry[0], + ); + + return { + module: shaderModule, + entryPoint: entryPoint, + constants: constantsArray.reduce((prev, curr) => { + prev[curr.key] = curr.value; + return prev; + }, {}), + }; + } + + /** + * @param {number} start + * @returns {{ key: string, value: number }} + */ + ConstantEntry(start) { + const off = this.struct(start); + off(4); + + return { + key: this.StringView(off(this.sizes.StringView)), + value: this.mem.loadF64(off(8)), + }; + } + + /** + * @param {number} start + * @returns {GPUComputePipelineDescriptor} + */ + ComputePipelineDescriptor(start) { + const off = this.struct(start); + off(4); + + const label = this.StringView(off(this.sizes.StringView)); + const layoutIdx = this.mem.loadPtr(off(4)); + return { + label: label, + layout: layoutIdx > 0 ? this.pipelineLayouts.get(layoutIdx) : "auto", + compute: this.ProgrammableStageDescriptor(off(this.sizes.ProgrammableStageDescriptor)), + }; + } + + /** + * @param {number} start + * @returns {GPUVertexState} + */ + VertexState(start) { + const off = this.struct(start); + off(4); + + const shaderModuleIdx = this.mem.loadPtr(off(4)); + const entryPoint = this.StringView(off(this.sizes.StringView)); + + const constantsArray = this.array( + this.mem.loadUint(off(this.mem.intSize)), + this.mem.loadPtr(off(4)), + this.ConstantEntry, + this.sizes.ConstantEntry[0], + ); + + return { + module: this.shaderModules.get(shaderModuleIdx), + entryPoint: entryPoint, + constants: constantsArray.reduce((prev, curr) => { + prev[curr.key] = curr.value; + return prev; + }, {}), + buffers: this.array( + this.mem.loadUint(off(this.mem.intSize)), + this.mem.loadPtr(off(4)), + this.VertexBufferLayout, + this.sizes.VertexBufferLayout[0], + ), + }; + } + + /** + * @param {number} start + * @returns {?GPUVertexBufferLayout} + */ + VertexBufferLayout(start) { + const off = this.struct(start); + + const stepMode = this.enumeration("VertexStepMode", off(4)); + if (stepMode == null) { + return null; + } + + return { + arrayStride: this.mem.loadU64(off(8)), + stepMode: stepMode, + attributes: this.array( + this.mem.loadUint(off(this.mem.intSize)), + this.mem.loadPtr(off(4)), + this.VertexAttribute, + this.sizes.VertexAttribute[0], + ), + }; + } + + /** + * @param {number} start + * @returns {GPUVertexAttribute} + */ + VertexAttribute(start) { + const off = this.struct(start); + return { + format: this.enumeration("VertexFormat", off(4)), + offset: this.mem.loadU64(off(8)), + shaderLocation: this.mem.loadU32(off(4)), + }; + } + + /** + * @param {number} start + * @returns {GPUPrimitiveState} + */ + PrimitiveState(start) { + const off = this.struct(start); + off(4); + + return { + topology: this.enumeration("PrimitiveTopology", off(4)), + stripIndexFormat: this.enumeration("IndexFormat", off(4)), + frontFace: this.enumeration("FrontFace", off(4)), + cullMode: this.enumeration("CullMode", off(4)), + unclippedDepth: this.mem.loadB32(off(4)), + }; + } + + /** + * @param {number} start + * @returns {GPURenderPipelineDescriptor} + */ + RenderPipelineDescriptor(start) { + const off = this.struct(start); + off(4); + + const label = this.StringView(off(this.sizes.StringView)); + const layoutIdx = this.mem.loadPtr(off(4)); + return { + label: label, + layout: layoutIdx > 0 ? this.pipelineLayouts.get(layoutIdx) : "auto", + vertex: this.VertexState(off(this.sizes.VertexState)), + primitive: this.PrimitiveState(off(this.sizes.PrimitiveState)), + depthStencil: this.DepthStencilStatePtr(off(4)), + multisample: this.MultisampleState(off(this.sizes.MultisampleState)), + fragment: this.FragmentStatePtr(off(4)), + }; + } + + /** + * @param {number} ptr + * @returns {?GPUDepthStencilState} + */ + DepthStencilStatePtr(ptr) { + const start = this.mem.loadPtr(ptr); + if (start == 0) { + return undefined; + } + + const off = this.struct(start); + off(4); + + return { + format: this.enumeration("TextureFormat", off(4)), + depthWriteEnabled: this.enumeration("OptionalBool", off(4)), + depthCompare: this.enumeration("CompareFunction", off(4)), + stencilFront: this.StencilFaceState(off(this.sizes.StencilFaceState)), + stencilBack: this.StencilFaceState(off(this.sizes.StencilFaceState)), + stencilReadMask: this.mem.loadU32(off(4)), + stencilWriteMask: this.mem.loadU32(off(4)), + depthBias: this.mem.loadI32(off(4)), + depthBiasSlopeScale: this.mem.loadF32(off(4)), + depthBiasClamp: this.mem.loadF32(off(4)), + }; + } + + /** + * @param {number} start + * @returns {GPUStencilFaceState} + */ + StencilFaceState(start) { + return { + compare: this.enumeration("CompareFunction", start + 0), + failOp: this.enumeration("StencilOperation", start + 4), + depthFailOp: this.enumeration("StencilOperation", start + 8), + passOp: this.enumeration("StencilOperation", start + 12), + }; + } + + /** + * @param {number} start + * @returns {GPUMultisampleState} + */ + MultisampleState(start) { + return { + count: this.mem.loadU32(start + 4), + mask: this.mem.loadU32(start + 8), + alphaToCoverageEnabled: this.mem.loadB32(start + 12), + }; + } + + /** + * @param {number} ptr + * @returns {?GPUFragmentState} + */ + FragmentStatePtr(ptr) { + const start = this.mem.loadPtr(ptr); + if (start == 0) { + return undefined; + } + + const off = this.struct(start); + off(4); + + const shaderModule = this.shaderModules.get(this.mem.loadPtr(off(4))); + const entryPoint = this.StringView(off(this.sizes.StringView)); + + const constantsArray = this.array( + this.mem.loadUint(off(this.mem.intSize)), + this.mem.loadPtr(off(4)), + this.ConstantEntry, + this.sizes.ConstantEntry[0], + ); + + return { + module: shaderModule, + entryPoint: entryPoint, + constants: constantsArray.reduce((prev, curr) => { + prev[curr.key] = curr.value; + return prev; + }, {}), + targets: this.array( + this.mem.loadUint(off(this.mem.intSize)), + this.mem.loadPtr(off(4)), + this.ColorTargetState, + this.sizes.ColorTargetState[0], + ), + }; + } + + /** + * @param {number} start + * @returns {GPUColorTargetState} + */ + ColorTargetState(start) { + const off = this.struct(start); + off(4); + return { + format: this.enumeration("TextureFormat", off(4)), + blend: this.BlendStatePtr(off(4)), + writeMask: this.mem.loadU64(off(8)), + }; + } + + /** + * @param {number} ptr + * @returns {?GPUBlendState} + */ + BlendStatePtr(ptr) { + const start = this.mem.loadPtr(ptr); + if (start == 0) { + return undefined; + } + + const off = this.struct(start); + + return { + color: this.BlendComponent(off(this.sizes.BlendComponent)), + alpha: this.BlendComponent(off(this.sizes.BlendComponent)), + }; + } + + /** + * @param {number} start + * @returns {?GPUBlendComponent} + */ + BlendComponent(start) { + return { + operation: this.enumeration("BlendOperation", start + 0), + srcFactor: this.enumeration("BlendFactor", start + 4), + dstFactor: this.enumeration("BlendFactor", start + 8), + }; + } + + TexelCopyBufferInfo(start) { + const off = this.struct(start); + const layout = this.TexelCopyBufferLayout(off(this.sizes.TexelCopyBufferLayout)); + const bufferIdx = this.mem.loadPtr(off(4)); + return { + buffer: this.buffers.get(bufferIdx).buffer, + offset: layout.offset, + bytesPerRow: layout.bytesPerRow, + rowsPerImage: layout.rowsPerImage, + }; + } + + TexelCopyBufferLayout(start) { + const off = this.struct(start); + return { + offset: this.mem.loadU64(off(8)), + bytesPerRow: this.mem.loadU32(off(4)), + rowsPerImage: this.mem.loadU32(off(4)), + }; + } + + TexelCopyTextureInfo(start) { + const off = this.struct(start); + return { + texture: this.textures.get(this.mem.loadPtr(off(4))), + mipLevel: this.mem.loadU32(off(4)), + origin: this.Origin3D(off(this.sizes.Origin3D)), + aspect: this.enumeration("TextureAspect", off(4)), + }; + } + + StringView(start) { + const data = this.mem.loadPtr(start); + return this.mem.loadString(data, this.mem.loadUint(start + this.mem.intSize)); + } + + CallbackInfoPtr(ptr) { + const start = this.mem.loadPtr(ptr); + if (start === 0) { + return null; + } + + return CallbackInfo(start); + } + + CallbackInfo(start) { + const off = this.struct(start); + off(4); + // TODO: callback mode? + off(4); + return { + callback: this.mem.exports.__indirect_function_table.get(this.mem.loadPtr(off(4))), + userdata1: this.mem.loadPtr(off(4)), + userdata2: this.mem.loadPtr(off(4)), + }; + } + + UncapturedErrorCallbackInfo(start) { + const off = this.struct(start); + off(4); + return { + callback: this.mem.exports.__indirect_function_table.get(this.mem.loadPtr(off(4))), + userdata1: this.mem.loadPtr(off(4)), + userdata2: this.mem.loadPtr(off(4)), + }; + } + + callCallback(callback, args) { + args.push(callback.userdata1); + args.push(callback.userdata2); + callback.callback(...args); + } + + zeroMessageArg() { + if (this.zeroMessageAddr > 0) { + return this.zeroMessageAddr; + } + + this.zeroMessageAddr = this.mem.exports.wgpu_alloc(this.sizes.StringView[0]); + return this.zeroMessageAddr; + } + + makeMessageArg(message) { + if (message.length == 0) { + return this.zeroMessageArg(); + } + + const messageLength = new TextEncoder().encode(message).length; + const stringSize = this.sizes.StringView[0]; + + const addr = this.mem.exports.wgpu_alloc(stringSize + messageLength); + + const messageAddr = addr + stringSize; + + this.mem.storeI32(addr, messageAddr); + this.mem.storeUint(addr + this.mem.intSize, messageLength); + + this.mem.storeString(messageAddr, message); + + return addr; + } + + getInterface() { + return { + /** + * @param {0|number} descriptorPtr + * @returns {number} + */ + wgpuCreateInstance: (descriptorPtr) => { + if (!navigator.gpu) { + console.error("WebGPU is not supported by this browser"); + return 0; + } + + // TODO: instance capabilities for futures? + + return this.instances.create({}); + }, + + /** + * @param {number} capabilitiesPtr + * @returns {number} + */ + wgpuGetInstanceCapabilities: (capabilitiesPtr) => { + // TODO: implement (futures). + return STATUS_ERROR; + }, + + /** + * @param {number} procNamePtr + * @param {number} procNameLen + * @returns {number} + */ + wgpuGetProcAddress: (procNamePtr, procNameLen) => { + console.error(`unimplemented: wgpuGetProcAddress`); + return 0; + }, + + /* ---------------------- Adapter ---------------------- */ + + /** + * @param {number} adapterIdx + * @param {number} featuresPtr + */ + wgpuAdapterGetFeatures: (adapterIdx, featuresPtr) => { + const adapter = this.adapters.get(adapterIdx); + this.genericGetFeatures(adapter.features, featuresPtr); + }, + + /** + * @param {number} adapterIdx + * @param {number} infoPtr + * @returns {number} + */ + wgpuAdapterGetInfo: (adapterIdx, infoPtr) => { + return this.genericGetAdapterInfo(infoPtr); + }, + + /** + * @param {number} adapterIdx + * @param {number} limitsPtr + * @returns {number} + */ + wgpuAdapterGetLimits: (adapterIdx, limitsPtr) => { + const adapter = this.adapters.get(adapterIdx); + return this.genericGetLimits(adapter.limits, limitsPtr); + }, + + /** + * @param {number} adapterIdx + * @param {number} featureInt + * @returns {boolean} + */ + wgpuAdapterHasFeature: (adapterIdx, featureInt) => { + const adapter = this.adapters.get(adapterIdx); + return adapter.features.has(ENUMS.FeatureName[featureInt]); + }, + + /** + * @param {number} adapterIdx + * @param {0|number} descriptorPtr + * @param {number} callbackInfoPtr + * @return {number} + */ + wgpuAdapterRequestDevice: (adapterIdx, descriptorPtr, callbackInfoPtr) => { + const adapter = this.adapters.get(adapterIdx); + + const off = this.struct(descriptorPtr); + off(4); + + /** @type {GPUDeviceDescriptor} */ + let descriptor; + if (descriptorPtr != 0) { + descriptor = { + label: this.StringView(off(this.sizes.StringView)), + requiredFeatures: this.array( + this.mem.loadUint(off(this.mem.intSize)), + this.mem.loadPtr(off(4)), + this.FeatureNamePtr, + 4, + ), + requiredLimits: this.RequiredLimitsPtr(off(4)), + defaultQueue: this.QueueDescriptor(off(this.sizes.QueueDescriptor)), + }; + } + + const callbackInfo = this.CallbackInfo(callbackInfoPtr); + + const deviceLostCallbackInfo = this.CallbackInfo(off(this.sizes.CallbackInfo)); + const uncapturedErrorCallbackInfo = this.UncapturedErrorCallbackInfo(off(this.sizes.UncapturedErrorCallbackInfo)); + + adapter.requestDevice(descriptor) + .catch((e) => { + const messageAddr = this.makeMessageArg(e.message); + this.callCallback(callbackInfo, [ENUMS.RequestDeviceStatus.indexOf("Error"), messageAddr]); + this.mem.exports.wgpu_free(messageAddr); + }) + .then((device) => { + const deviceIdx = this.devices.create(device); + + if (deviceLostCallbackInfo.callback !== null) { + device.lost.then((info) => { + const reason = ENUMS.DeviceLostReason.indexOf(info.reason); + + const devicePtr = this.mem.exports.wgpu_alloc(4); + this.mem.storeI32(devicePtr, deviceIdx); + + const messageAddr = this.makeMessageArg(info.message); + this.callCallback(deviceLostCallbackInfo, [devicePtr, reason, messageAddr]); + + this.mem.exports.wgpu_free(devicePtr); + this.mem.exports.wgpu_free(messageAddr); + }); + } + + if (uncapturedErrorCallbackInfo.callback !== null) { + device.onuncapturederror = (ev) => { + let status; + if (ev.error instanceof GPUValidationError) { + status = ENUMS.ErrorType.indexOf("validation"); + } else if (ev.error instanceof GPUOutOfMemoryError) { + status = ENUMS.ErrorType.indexOf("out-of-memory"); + } else if (ev.error instanceof GPUInternalError) { + status = ENUMS.ErrorType.indexOf("internal"); + } else { + status = ENUMS.ErrorType.indexOf("unknown"); + } + + const messageAddr = this.makeMessageArg(ev.error.message); + this.callCallback(uncapturedErrorCallbackInfo, [deviceIdx, status, messageAddr]); + this.mem.exports.wgpu_free(messageAddr); + }; + } + + this.callCallback(callbackInfo, [ENUMS.ErrorType.indexOf("no-error"), deviceIdx, this.zeroMessageArg()]); + }); + + // TODO: returning a future? WARN that requires refactor removing await + return BigInt(0); + }, + + ...this.adapters.interface(), + + /** + * @param {number} infoPtr + */ + wgpuAdapterInfoFreeMembers: (infoPtr) => { + // NOTE: nothing to free. + }, + + /* ---------------------- BindGroup ---------------------- */ + + ...this.bindGroups.interface(true), + + /* ---------------------- BindGroupLayout ---------------------- */ + + ...this.bindGroupLayouts.interface(true), + + /* ---------------------- Buffer ---------------------- */ + + /** @param {number} bufferIdx */ + wgpuBufferDestroy: (bufferIdx) => { + const buffer = this.buffers.get(bufferIdx); + buffer.buffer.destroy(); + }, + + /** + * @param {number} bufferIdx + * @param {number|BigInt} offset + * @param {number|BigInt} size + * @returns {number} + */ + wgpuBufferGetConstMappedRange: (bufferIdx, offset, size) => { + const buffer = this.buffers.get(bufferIdx); + offset = this.unwrapBigInt(offset); + size = this.unwrapBigInt(size); + + // TODO: does constMappedRange need to do something else? + + this.assert(!buffer.mapping, "buffer already mapped"); + + const range = buffer.buffer.getMappedRange(offset, size); + + const ptr = this.mem.exports.wgpu_alloc(range.byteLength); + + const mapping = new Uint8Array(this.mem.memory.buffer, ptr, size); + mapping.set(new Uint8Array(range)); + + buffer.mapping = { range: range, ptr: ptr, size: range.byteLength }; + return ptr; + }, + + /** + * @param {number} bufferIdx + * @return {number} + */ + wgpuBufferGetMapState: (bufferIdx) => { + const buffer = this.buffers.get(bufferIdx); + return ENUMS.BufferMapState.indexOf(buffer.mapState); + }, + + /** + * @param {number} bufferIdx + * @param {number|BigInt} offset + * @param {number|BigInt} size + * @returns {number} + */ + wgpuBufferGetMappedRange: (bufferIdx, offset, size) => { + const buffer = this.buffers.get(bufferIdx); + offset = this.unwrapBigInt(offset); + size = this.unwrapBigInt(size); + + this.assert(!buffer.mapping, "buffer already mapped"); + + const range = buffer.buffer.getMappedRange(offset, size); + + const ptr = this.mem.exports.wgpu_alloc(range.byteLength); + + const mapping = new Uint8Array(this.mem.memory.buffer, ptr, size); + mapping.set(new Uint8Array(range)); + + buffer.mapping = { range: range, ptr: ptr, size: range.byteLength }; + return ptr; + }, + + /** + * @param {number} bufferIdx + * @returns {BigInt} + */ + wgpuBufferGetSize: (bufferIdx) => { + const buffer = this.buffers.get(bufferIdx); + return BigInt(buffer.buffer.size); + }, + + /** + * @param {number} bufferIdx + * @returns {number} + */ + wgpuBufferGetUsage: (bufferIdx) => { + const buffer = this.buffers.get(bufferIdx); + return buffer.buffer.usage; + }, + + /** + * @param {number} bufferIdx + * @param {number} mode + * @param {number|BigInt} offset + * @param {number|BigInt} size + * @param {number} callbackInfo + * @return {number} + */ + wgpuBufferMapAsync: (bufferIdx, mode, offset, size, callbackInfoPtr) => { + const buffer = this.buffers.get(bufferIdx); + mode = this.unwrapBigInt(mode); + offset = this.unwrapBigInt(offset); + size = this.unwrapBigInt(size); + + const callbackInfo = this.CallbackInfo(callbackInfoPtr); + buffer.buffer.mapAsync(mode, offset, size) + .catch((e) => { + const messageAddr = this.makeMessageArg(e.message); + this.callCallback(callbackInfo, [ENUMS.MapAsyncStatus.indexOf("Error"), messageAddr]); + this.mem.exports.wgpu_free(messageAddr); + }) + .then(() => { + this.callCallback(callbackInfo, [ENUMS.MapAsyncStatus.indexOf("Success"), this.zeroMessageArg()]); + }); + + // TODO: returning a future? WARN that requires refactor removing await + return BigInt(0); + }, + + /** + * @param {number} bufferIdx + * @param {number} labelPtr + * @param {number} labelLen + */ + wgpuBufferSetLabel: (bufferIdx, labelPtr, labelLen) => { + const buffer = this.buffers.get(bufferIdx); + buffer.buffer.label = this.mem.loadString(labelPtr, labelLen); + }, + + /** + * @param {number} bufferIdx + */ + wgpuBufferUnmap: (bufferIdx) => { + const buffer = this.buffers.get(bufferIdx); + this.assert(buffer.mapping, "buffer not mapped"); + + const mapping = new Uint8Array(this.mem.memory.buffer, buffer.mapping.ptr, buffer.mapping.size); + (new Uint8Array(buffer.mapping.range)).set(mapping); + + buffer.buffer.unmap(); + + this.mem.exports.wgpu_free(buffer.mapping.ptr); + buffer.mapping = null; + }, + + ...this.buffers.interface(), + + /* ---------------------- CommandBuffer ---------------------- */ + + ...this.commandBuffers.interface(true), + + /* ---------------------- CommandEncoder ---------------------- */ + + /** + * @param {number} commandEncoderIdx + * @param {0|number} descriptorPtr + * @return {number} The compute pass encoder + */ + wgpuCommandEncoderBeginComputePass: (commandEncoderIdx, descriptorPtr) => { + const commandEncoder = this.commandEncoders.get(commandEncoderIdx); + + /** @type {?GPUComputePassDescriptor} */ + let descriptor; + if (descriptorPtr != 0) { + const off = this.struct(descriptorPtr); + off(4); + descriptor = { + label: this.StringView(off(this.sizes.StringView)), + timestampWrites: this.ComputePassTimestampWritesPtr(off(4)), + }; + } + + const computePassEncoder = commandEncoder.beginComputePass(descriptor); + return this.computePassEncoders.create(computePassEncoder); + }, + + /** + * @param {number} commandEncoderIdx + * @param {number} descriptorPtr + * @return {number} The render pass encoder + */ + wgpuCommandEncoderBeginRenderPass: (commandEncoderIdx, descriptorPtr) => { + const commandEncoder = this.commandEncoders.get(commandEncoderIdx); + this.assert(descriptorPtr != 0); + + const off = this.struct(descriptorPtr); + + let maxDrawCount = undefined; + const nextInChain = this.mem.loadPtr(off(4)); + if (nextInChain != 0) { + const nextInChainType = this.mem.loadI32(nextInChain + 4); + // RenderPassMaxDrawCount = 0x00000003, + if (nextInChainType == 0x00000003) { + maxDrawCount = this.mem.loadU64(nextInChain + 8); + } + } + + /** @type {GPURenderPassDescriptor} */ + const descriptor = { + label: this.StringView(off(this.sizes.StringView)), + colorAttachments: this.array( + this.mem.loadUint(off(this.mem.intSize)), + this.mem.loadPtr(off(4)), + this.RenderPassColorAttachment, + this.sizes.RenderPassColorAttachment[0], + ), + depthStencilAttachment: this.RenderPassDepthStencilAttachmentPtr(off(4)), + occlusionQuerySet: this.QuerySet(off(4)), + timestampWrites: this.RenderPassTimestampWritesPtr(off(4)), + maxDrawCount: maxDrawCount, + }; + + const renderPassEncoder = commandEncoder.beginRenderPass(descriptor); + return this.renderPassEncoders.create(renderPassEncoder); + }, + + /** + * @param {number} commandEncoderIdx + * @param {number} bufferIdx + * @param {BigInt} offset + * @param {BigInt} size + */ + wgpuCommandEncoderClearBuffer: (commandEncoderIdx, bufferIdx, offset, size) => { + const commandEncoder = this.commandEncoders.get(commandEncoderIdx); + const buffer = this.buffers.get(bufferIdx); + offset = this.unwrapBigInt(offset); + size = this.unwrapBigInt(size); + commandEncoder.clearBuffer(buffer.buffer, offset, size); + }, + + /** + * @param {number} commandEncoderIdx + * @param {number} sourceIdx + * @param {BigInt} sourceOffset + * @param {number} destinationIdx + * @param {BigInt} destinationOffset + * @param {BigInt} size + */ + wgpuCommandEncoderCopyBufferToBuffer: (commandEncoderIdx, sourceIdx, sourceOffset, destinationIdx, destinationOffset, size) => { + const commandEncoder = this.commandEncoders.get(commandEncoderIdx); + const source = this.buffers.get(sourceIdx); + const destination = this.buffers.get(destinationIdx); + sourceOffset = this.unwrapBigInt(sourceOffset); + destinationOffset = this.unwrapBigInt(destinationOffset); + size = this.unwrapBigInt(size); + commandEncoder.copyBufferToBuffer(source.buffer, sourceOffset, destination.buffer, destinationOffset, size); + }, + + /** + * @param {number} commandEncoderIdx + * @param {number} sourcePtr + * @param {number} destinationPtr + * @param {number} copySizePtr + */ + wgpuCommandEncoderCopyBufferToTexture: (commandEncoderIdx, sourcePtr, destinationPtr, copySizePtr) => { + const commandEncoder = this.commandEncoders.get(commandEncoderIdx); + commandEncoder.copyBufferToTexture( + this.TexelCopyBufferInfo(sourcePtr), + this.TexelCopyTextureInfo(destinationPtr), + this.Extent3D(copySizePtr), + ); + }, + + /** + * @param {number} commandEncoderIdx + * @param {number} sourcePtr + * @param {number} destinationPtr + * @param {number} copySizePtr + */ + wgpuCommandEncoderCopyTextureToBuffer: (commandEncoderIdx, sourcePtr, destinationPtr, copySizePtr) => { + const commandEncoder = this.commandEncoders.get(commandEncoderIdx); + commandEncoder.copyTextureToBuffer( + this.TexelCopyTextureInfo(sourcePtr), + this.TexelCopyBufferInfo(destinationPtr), + this.Extent3D(copySizePtr), + ); + }, + + /** + * @param {number} commandEncoderIdx + * @param {number} sourcePtr + * @param {number} destinationPtr + * @param {number} copySizePtr + */ + wgpuCommandEncoderCopyTextureToTexture: (commandEncoderIdx, sourcePtr, destinationPtr, copySizePtr) => { + const commandEncoder = this.commandEncoders.get(commandEncoderIdx); + commandEncoder.copyTextureToTexture( + this.TexelCopyTextureInfo(sourcePtr), + this.TexelCopyTextureInfo(destinationPtr), + this.Extent3D(copySizePtr), + ); + }, + + /** + * @param {number} commandEncoderIdx + * @param {0|number} descriptorPtr + * @returns {number} The command buffer. + */ + wgpuCommandEncoderFinish: (commandEncoderIdx, descriptorPtr) => { + const commandEncoder = this.commandEncoders.get(commandEncoderIdx); + + /** @type {undefined|GPUCommandBufferDescriptor} */ + let descriptor; + if (descriptorPtr != 0) { + descriptor = { + label: this.StringView(descriptorPtr + 4), + }; + } + + const commandBuffer = commandEncoder.finish(descriptor); + return this.commandBuffers.create(commandBuffer); + }, + + /** + * @param {number} commandEncoderIdx + * @param {number} markerLabelPtr + * @param {number} markerLabelLen + */ + wgpuCommandEncoderInsertDebugMarker: (commandEncoderIdx, markerLabelPtr, markerLabelLen) => { + const commandEncoder = this.commandEncoders.get(commandEncoderIdx); + commandEncoder.insertDebugMarker(this.mem.loadString(markerLabelPtr, markerLabelLen)); + }, + + /** + * @param {number} commandEncoderIdx + */ + wgpuCommandEncoderPopDebugGroup: (commandEncoderIdx) => { + const commandEncoder = this.commandEncoders.get(commandEncoderIdx); + commandEncoder.popDebugGroup(); + }, + + /** + * @param {number} commandEncoderIdx + * @param {number} groupLabelPtr + * @param {number} groupLabelLen + */ + wgpuCommandEncoderPushDebugGroup: (commandEncoderIdx, groupLabelPtr, groupLabelLen) => { + const commandEncoder = this.commandEncoders.get(commandEncoderIdx); + commandEncoder.pushDebugGroup(this.mem.loadString(groupLabelPtr, groupLabelLen)); + }, + + /** + * @param {number} commandEncoderIdx + * @param {number} querySetIdx + * @param {number} firstQuery + * @param {number} queryCount + * @param {number} destinationIdx + * @param {BigInt} destinationOffset + */ + wgpuCommandEncoderResolveQuerySet: (commandEncoderIdx, querySetIdx, firstQuery, queryCount, destinationIdx, destinationOffset) => { + const commandEncoder = this.commandEncoders.get(commandEncoderIdx); + const querySet = this.querySets.get(querySetIdx); + const destination = this.buffers.get(destinationIdx); + destinationOffset = this.unwrapBigInt(destinationOffset); + commandEncoder.resolveQuerySet(querySet, firstQuery, queryCount, destination.buffer, destinationOffset); + }, + + /** + * @param {number} commandEncoderIdx + * @param {number} querySetIdx + * @param {number} queryIndex + */ + wgpuCommandEncoderWriteTimestamp: (commandEncoderIdx, querySetIdx, queryIndex) => { + const commandEncoder = this.commandEncoders.get(commandEncoderIdx); + const querySet = this.querySets.get(querySetIdx); + commandEncoder.writeTimestamp(querySet, queryIndex); + }, + + ...this.commandEncoders.interface(true), + + /* ---------------------- ComputePassEncoder ---------------------- */ + + + /** + * @param {number} computePassEncoderIdx + * @param {number} workgroupCountX + * @param {number} workgroupCountY + * @param {number} workgroupCountZ + */ + wgpuComputePassEncoderDispatchWorkgroups: (computePassEncoderIdx, workgroupCountX, workgroupCountY, workgroupCountZ) => { + const computePassEncoder = this.computePassEncoders.get(computePassEncoderIdx); + computePassEncoder.dispatchWorkgroups(workgroupCountX, workgroupCountY, workgroupCountZ); + }, + + /** + * @param {number} computePassEncoderIdx + * @param {number} indirectBufferIdx + * @param {BigInt} indirectOffset + */ + wgpuComputePassEncoderDispatchWorkgroupsIndirect: (computePassEncoderIdx, indirectBufferIdx, indirectOffset) => { + const computePassEncoder = this.computePassEncoders.get(computePassEncoderIdx); + const indirectBuffer = this.buffers.get(indirectBufferIdx); + indirectOffset = this.unwrapBigInt(indirectOffset); + computePassEncoder.dispatchWorkgroupsIndirect(indirectBuffer.buffer, indirectOffset); + }, + + /** + * @param {number} computePassEncoderIdx + */ + wgpuComputePassEncoderEnd: (computePassEncoderIdx) => { + const computePassEncoder = this.computePassEncoders.get(computePassEncoderIdx); + computePassEncoder.end(); + }, + + /** + * @param {number} computePassEncoderIdx + * @param {number} markerLabelPtr + * @param {number} markerLabelLen + */ + wgpuComputePassEncoderInsertDebugMarker: (computePassEncoderIdx, markerLabelPtr, markerLabelLen) => { + const computePassEncoder = this.computePassEncoders.get(computePassEncoderIdx); + computePassEncoder.insertDebugMarker(this.mem.loadString(markerLabelPtr, markerLabelLen)); + }, + + /** + * @param {number} computePassEncoderIdx + */ + wgpuComputePassEncoderPopDebugGroup: (computePassEncoderIdx) => { + const computePassEncoder = this.computePassEncoders.get(computePassEncoderIdx); + computePassEncoder.popDebugGroup(); + }, + + /** + * @param {number} computePassEncoderIdx + * @param {number} groupLabelPtr + * @param {number} groupLabelLen + */ + wgpuComputePassEncoderPushDebugGroup: (computePassEncoderIdx, groupLabelPtr, groupLabelLen) => { + const computePassEncoder = this.computePassEncoders.get(computePassEncoderIdx); + computePassEncoder.pushDebugGroup(this.mem.loadString(groupLabelPtr, groupLabelLen)); + }, + + /** + * @param {number} computePassEncoderIdx + * @param {number} groupIndex + * @param {0|number} groupIdx + * @param {number|BigInt} dynamicOffsetCount + * @param {number} dynamicOffsetsPtr + */ + wgpuComputePassEncoderSetBindGroup: (computePassEncoderIdx, groupIndex, groupIdx, dynamicOffsetCount, dynamicOffsetsPtr) => { + const computePassEncoder = this.computePassEncoders.get(computePassEncoderIdx); + dynamicOffsetCount = this.unwrapBigInt(dynamicOffsetCount); + + let bindGroup; + if (groupIdx != 0) { + bindGroup = this.bindGroups.get(groupIdx); + } + + const dynamicOffsets = []; + for (let i = 0; i < dynamicOffsetCount; i += 1) { + dynamicOffsets.push(this.mem.loadU32(dynamicOffsetsPtr)); + dynamicOffsetsPtr += 4; + } + + computePassEncoder.setBindGroup(groupIndex, bindGroup, dynamicOffsets); + }, + + /** + * @param {number} computePassEncoderIdx + * @param {number} pipelineIdx + */ + wgpuComputePassEncoderSetPipeline: (computePassEncoderIdx, pipelineIdx) => { + const computePassEncoder = this.computePassEncoders.get(computePassEncoderIdx); + const pipeline = this.computePipelines.get(pipelineIdx); + computePassEncoder.setPipeline(pipeline); + }, + + ...this.computePassEncoders.interface(true), + + /* ---------------------- ComputePipeline ---------------------- */ + + /** + * @param {number} computePipelineIdx + * @param {number} groupIndex + * @returns {number} + */ + wgpuComputePipelineGetBindGroupLayout: (computePipelineIdx, groupIndex) => { + const computePipeline = this.computePipelines.get(computePipelineIdx); + const bindGroupLayout = computePipeline.getBindGroupLayout(groupIndex); + return this.bindGroupLayouts.create(bindGroupLayout); + }, + + ...this.computePipelines.interface(true), + + /* ---------------------- Device ---------------------- */ + + /** + * @param {number} deviceIdx + * @param {number} descriptorPtr + * @returns {number} The bind group. + */ + wgpuDeviceCreateBindGroup: (deviceIdx, descriptorPtr) => { + const device = this.devices.get(deviceIdx); + this.assert(descriptorPtr != 0); + + const off = this.struct(descriptorPtr); + off(4); + + /** @type {GPUBindGroupDescriptor} */ + const descriptor = { + label: this.StringView(off(this.sizes.StringView)), + layout: this.bindGroupLayouts.get(this.mem.loadPtr(off(4))), + entries: this.array( + this.mem.loadUint(off(this.mem.intSize)), + this.mem.loadPtr(off(4)), + this.BindGroupEntry, + this.sizes.BindGroupEntry[0], + ), + }; + + const bindGroup = device.createBindGroup(descriptor); + return this.bindGroups.create(bindGroup); + }, + + /** + * @param {number} deviceIdx + * @param {number} descriptorPtr + * @returns {number} The bind group layout. + */ + wgpuDeviceCreateBindGroupLayout: (deviceIdx, descriptorPtr) => { + const device = this.devices.get(deviceIdx); + this.assert(descriptorPtr != 0); + + const off = this.struct(descriptorPtr); + off(4); + + /** @type {GPUBindGroupLayoutDescriptor} */ + const descriptor = { + label: this.StringView(off(this.sizes.StringView)), + entries: this.array( + this.mem.loadUint(off(this.mem.intSize)), + this.mem.loadPtr(off(4)), + this.BindGroupLayoutEntry, + this.sizes.BindGroupLayoutEntry[0], + ), + }; + + const bindGroupLayout = device.createBindGroupLayout(descriptor); + return this.bindGroupLayouts.create(bindGroupLayout); + }, + + /** + * @param {number} deviceIdx + * @param {number} descriptorPtr + * @returns {number} The buffer. + */ + wgpuDeviceCreateBuffer: (deviceIdx, descriptorPtr) => { + const device = this.devices.get(deviceIdx); + this.assert(descriptorPtr != 0); + + const off = this.struct(descriptorPtr); + off(4); + + /** @type {GPUBufferDescriptor} */ + const descriptor = { + label: this.StringView(off(this.sizes.StringView)), + usage: this.mem.loadU64(off(8)), + size: this.mem.loadU64(off(8)), + mappedAtCreation: this.mem.loadB32(off(4)), + }; + + const buffer = device.createBuffer(descriptor); + return this.buffers.create({buffer: buffer, mapping: null}); + }, + + /** + * @param {number} deviceIdx + * @param {0|number} descriptorPtr + * @returns {number} The command encoder. + */ + wgpuDeviceCreateCommandEncoder: (deviceIdx, descriptorPtr) => { + const device = this.devices.get(deviceIdx); + + /** @type {GPUCommandEncoderDescriptor} */ + let descriptor; + if (descriptor != 0) { + descriptor = { + label: this.StringView(descriptorPtr + 4), + }; + } + + const commandEncoder = device.createCommandEncoder(descriptor); + return this.commandEncoders.create(commandEncoder); + }, + + /** + * @param {number} deviceIdx + * @param {number} descriptorPtr + * @returns {number} The compute pipeline. + */ + wgpuDeviceCreateComputePipeline: (deviceIdx, descriptorPtr) => { + const device = this.devices.get(deviceIdx); + this.assert(descriptorPtr != 0); + const computePipeline = device.createComputePipeline(this.ComputePipelineDescriptor(descriptorPtr)); + return this.computePipelines.create(computePipeline); + }, + + /** + * @param {number} deviceIdx + * @param {number} descriptorPtr + * @param {number} callbackInfo + */ + wgpuDeviceCreateComputePipelineAsync: (deviceIdx, descriptorPtr, callbackInfoPtr) => { + const device = this.devices.get(deviceIdx); + + this.assert(descriptorPtr != 0); + + const callbackInfo = this.CallbackInfo(callbackInfoPtr); + device.createComputePipelineAsync(this.ComputePipelineDescriptor(descriptorPtr)) + .catch((e) => { + const messageAddr = this.makeMessageArg(e.message); + this.callCallback(callbackInfo, [ENUMS.CreatePipelineAsyncStatus.indexOf("Unknown"), 0, messageAddr]); + this.mem.exports.wgpu_free(messageAddr); + }) + .then((computePipeline) => { + const pipelineIdx = this.computePipelines.create(computePipeline); + this.callCallback(callbackInfo, [ENUMS.CreatePipelineAsyncStatus.indexOf("Success"), pipelineIdx, this.zeroMessageArg()]); + }); + + // TODO: returning futures? + return BigInt(0); + }, + + /** + * @param {number} deviceIdx + * @param {number} descriptorPtr + * @returns {number} The pipeline layout. + */ + wgpuDeviceCreatePipelineLayout: (deviceIdx, descriptorPtr) => { + const device = this.devices.get(deviceIdx); + this.assert(descriptorPtr != 0); + + const off = this.struct(descriptorPtr); + off(4); + + /** @type {GPUPipelineLayoutDescriptor} */ + const descriptor = { + label: this.StringView(off(this.sizes.StringView)), + bindGroupLayouts: this.array( + this.mem.loadUint(off(this.mem.intSize)), + this.mem.loadPtr(off(4)), + (ptr) => this.bindGroupLayouts.get(this.mem.loadPtr(ptr)), + 4, + ), + }; + + const pipelineLayout = device.createPipelineLayout(descriptor); + return this.pipelineLayouts.create(pipelineLayout); + }, + + /** + * @param {number} deviceIdx + * @param {number} descriptorPtr + * @returns {number} The query set. + */ + wgpuDeviceCreateQuerySet: (deviceIdx, descriptorPtr) => { + const device = this.devices.get(deviceIdx); + this.assert(descriptorPtr != 0); + + const off = this.struct(descriptorPtr); + off(4); + + /** @type {GPUQuerySetDescriptor} */ + const descriptor = { + label: this.StringView(off(this.sizes.StringView)), + type: this.enumeration("QueryType", off(4)), + count: this.mem.loadU32(off(4)), + }; + + const querySet = device.createQuerySet(descriptor); + return this.querySets.create(querySet); + }, + + /** + * @param {number} deviceIdx + * @param {number} descriptorPtr + * @returns {number} The query set. + */ + wgpuDeviceCreateRenderBundleEncoder: (deviceIdx, descriptorPtr) => { + const device = this.devices.get(deviceIdx); + this.assert(descriptorPtr != 0); + + const off = this.struct(descriptorPtr); + off(4); + + /** @type {GPURenderBundleEncoderDescriptor} */ + const descriptor = { + label: this.StringView(off(this.sizes.StringView)), + colorFormats: this.array( + this.mem.loadUint(off(this.mem.intSize)), + this.mem.loadPtr(off(4)), + this.TextureFormat, + 4, + ), + depthStencilFormat: this.enumeration("TextureFormat", off(4)), + sampleCount: this.mem.loadU32(off(4)), + depthReadOnly: this.mem.loadB32(off(4)), + stencilReadOnly: this.mem.loadB32(off(4)), + }; + + const renderBundleEncoder = device.createRenderBundleEncoder(descriptor); + return this.renderBundleEncoders.create(renderBundleEncoder); + }, + + /** + * @param {number} deviceIdx + * @param {number} descriptorPtr + * @returns {number} The render pipeline. + */ + wgpuDeviceCreateRenderPipeline: (deviceIdx, descriptorPtr) => { + const device = this.devices.get(deviceIdx); + this.assert(descriptorPtr != 0); + + const descriptor = this.RenderPipelineDescriptor(descriptorPtr); + const renderPipeline = device.createRenderPipeline(descriptor); + return this.renderPipelines.create(renderPipeline); + }, + + /** + * @param {number} deviceIdx + * @param {number} descriptorPtr + * @param {number} callbackInfo + */ + wgpuDeviceCreateRenderPipelineAsync: (deviceIdx, descriptorPtr, callbackInfoPtr) => { + const device = this.devices.get(deviceIdx); + this.assert(descriptorPtr != 0); + + const callbackInfo = this.CallbackInfo(callbackInfoPtr); + device.createRenderPipelineAsync(this.RenderPipelineDescriptor(descriptorPtr)) + .catch((e) => { + const messageAddr = this.makeMessageArg(e.message); + this.callCallback(callbackInfo, [ENUMS.CreatePipelineAsyncStatus.indexOf("Unknown"), 0, messageAddr]); + this.mem.exports.wgpu_free(messageAddr); + }) + .then((renderPipeline) => { + const renderPipelineIdx = this.renderPipelines.create(renderPipeline); + this.callCallback(callbackInfo, [ENUMS.CreatePipelineAsyncStatus.indexOf("Success"), renderPipelineIdx, this.zeroMessageArg()]); + }); + + // TODO: returning futures? + return BigInt(0); + }, + + /** + * @param {number} deviceIdx + * @param {0|number} descriptorPtr + * @returns {number} The sampler. + */ + wgpuDeviceCreateSampler: (deviceIdx, descriptorPtr) => { + const device = this.devices.get(deviceIdx); + + /** @type {?GPUSamplerDescriptor} */ + let descriptor; + if (descriptorPtr != 0) { + const off = this.struct(descriptorPtr); + off(4); + descriptor = { + label: this.StringView(off(this.sizes.StringView)), + addressModeU: this.enumeration("AddressMode", off(4)), + addressModeV: this.enumeration("AddressMode", off(4)), + addressModeW: this.enumeration("AddressMode", off(4)), + magFilter: this.enumeration("FilterMode", off(4)), + minFilter: this.enumeration("FilterMode", off(4)), + mipmapFilter: this.enumeration("MipmapFilterMode", off(4)), + lodMinClamp: this.mem.loadF32(off(4)), + lodMaxClamp: this.mem.loadF32(off(4)), + compare: this.enumeration("CompareFunction", off(4)), + maxAnisotropy: this.mem.loadU16(off(2)), + }; + } + + const sampler = device.createSampler(descriptor); + return this.samplers.create(sampler); + }, + + /** + * @param {number} deviceIdx + * @param {number} descriptorPtr + * @returns {number} The shader module. + */ + wgpuDeviceCreateShaderModule: (deviceIdx, descriptorPtr) => { + const device = this.devices.get(deviceIdx); + this.assert(descriptorPtr != 0); + + const off = this.struct(descriptorPtr); + + const nextInChain = this.mem.loadPtr(off(4)); + + const chainOff = this.struct(nextInChain); + chainOff(4); + + const nextInChainType = this.mem.loadI32(chainOff(4)); + + // ShaderSourceWGSL = 0x00000002, + if (nextInChainType != 2) { + throw new TypeError(`Descriptor type should be 'ShaderSourceWGSL', got ${nextInChainType}`); + } + + /** @type {GPUShaderModuleDescriptor} */ + const descriptor = { + label: this.StringView(off(this.sizes.StringView)), + code: this.StringView(chainOff(this.sizes.StringView)), + }; + + const shaderModule = device.createShaderModule(descriptor); + return this.shaderModules.create(shaderModule); + }, + + /** + * @param {number} deviceIdx + * @param {number} descriptorPtr + * @returns {number} The texture. + */ + wgpuDeviceCreateTexture: (deviceIdx, descriptorPtr) => { + const device = this.devices.get(deviceIdx); + this.assert(descriptorPtr != 0); + + const off = this.struct(descriptorPtr); + off(4); + + /** @type {GPUTextureDescriptor} */ + const descriptor = { + label: this.StringView(off(this.sizes.StringView)), + usage: this.mem.loadU64(off(8)), + dimension: this.enumeration("TextureDimension", off(4)), + size: this.Extent3D(off(this.sizes.Extent3D)), + format: this.enumeration("TextureFormat", off(4)), + mipLevelCount: this.mem.loadU32(off(4)), + sampleCount: this.mem.loadU32(off(4)), + viewFormats: this.array( + this.mem.loadUint(off(this.mem.intSize)), + this.mem.loadPtr(off(4)), + (ptr) => this.enumeration("TextureFormat", ptr), + 4, + ), + }; + + const texture = device.createTexture(descriptor); + return this.textures.create(texture); + }, + + /** + * @param {number} deviceIdx + */ + wgpuDeviceDestroy: (deviceIdx) => { + const device = this.devices.get(deviceIdx); + device.destroy(); + }, + + /** + * @param {number} deviceIdx + * @param {number} infoPtr + * @returns {number} + */ + wgpuDeviceGetAdapterInfo: (deviceIdx, infoPtr) => { + return this.genericGetAdapterInfo(infoPtr); + }, + + /** + * @param {number} deviceIdx + * @param {number} featuresPtr + */ + wgpuDeviceGetFeatures: (deviceIdx, featuresPtr) => { + const device = this.devices.get(deviceIdx); + return this.genericGetFeatures(device.features, featuresPtr); + }, + + /** + * @param {number} deviceIdx + * @param {number} limitsPtr + * @returns {number} + */ + wgpuDeviceGetLimits: (deviceIdx, limitsPtr) => { + const device = this.devices.get(deviceIdx); + return this.genericGetLimits(device.limits, limitsPtr); + }, + + /** + * @param {number} deviceIdx + * @returns {number} + */ + wgpuDeviceGetLostFuture: (deviceIdx) => { + // TODO: futures? + return BigInt(0); + }, + + /** + * @param {number} deviceIdx + * @returns {number} + */ + wgpuDeviceGetQueue: (deviceIdx) => { + const device = this.devices.get(deviceIdx); + return this.queues.create(device.queue); + }, + + /** + * @param {number} deviceIdx + * @param {number} featureInt + * @returns {boolean} + */ + wgpuDeviceHasFeature: (deviceIdx, featureInt) => { + const device = this.devices.get(deviceIdx); + return device.features.has(ENUMS.FeatureName[featureInt]); + }, + + /** + * @param {number} deviceIdx + * @param {number} callbackInfo + * @returns {number} + */ + wgpuDevicePopErrorScope: (deviceIdx, callbackInfoPtr) => { + const device = this.devices.get(deviceIdx); + + const callbackInfo = this.CallbackInfo(callbackInfoPtr); + device.popErrorScope() + .then((error) => { + if (!error) { + this.callCallback(callbackInfo, [ENUMS.PopErrorScopeStatus.indexOf("Success"), ENUMS.ErrorType.indexOf("no-error"), this.zeroMessageArg()]); + return; + } + + let status; + if (error instanceof GPUValidationError) { + status = ENUMS.ErrorType.indexOf("validation"); + } else if (error instanceof GPUOutOfMemoryError) { + status = ENUMS.ErrorType.indexOf("out-of-memory"); + } else if (error instanceof GPUInternalError) { + status = ENUMS.ErrorType.indexOf("internal"); + } else { + status = ENUMS.ErrorType.indexOf("unknown"); + } + + const messageAddr = error.message; + this.callCallback(callbackInfo, [ENUMS.PopErrorScopeStatus.indexOf("Success"), status, messageAddr]); + this.mem.exports.wgpu_free(messageAddr); + }); + + // TODO: futures? + return BigInt(0); + }, + + /** + * @param {number} deviceIdx + * @param {number} filterInt + */ + wgpuDevicePushErrorScope: (deviceIdx, filterInt) => { + const device = this.devices.get(deviceIdx); + device.pushErrorScope(ENUMS.ErrorFilter[filterInt]); + }, + + ...this.devices.interface(true), + + /* ---------------------- Instance ---------------------- */ + + /** + * @param {number} instanceIdx + * @param {number} descriptorPtr + */ + wgpuInstanceCreateSurface: (instanceIdx, descriptorPtr) => { + this.assert(instanceIdx > 0); + this.assert(descriptorPtr != 0); + + const off = this.struct(descriptorPtr); + + const nextInChain = this.mem.loadPtr(off(4)); + + const chainOff = this.struct(nextInChain); + chainOff(4); + + const nextInChainType = this.mem.loadI32(chainOff(4)); + + // SurfaceSourceCanvasHTMLSelector = 0x00040001, + if (nextInChainType != 0x00040001) { + throw new TypeError(`Descriptor type should be 'SurfaceSourceCanvasHTMLSelector', got ${nextInChainType}`); + } + + const selector = this.StringView(chainOff(this.sizes.StringView)); + const surface = document.querySelector(selector); + if (!surface) { + throw new Error(`Selector '${selector}' did not match any element`); + } + if (!(surface instanceof HTMLCanvasElement)) { + throw new Error('Selector matches an element that is not a canvas'); + } + + return this.surfaces.create(surface); + }, + + /** + * @param {number} instanceIdx + * @param {number} featurePtr + * @returns {number} + */ + wgpuInstanceGetWGSLLanguageFeatures: (instanceIdx, featuresPtr) => { + this.assert(featuresPtr != 0); + + const availableFeatures = []; + ENUMS.WGSLLanguageFeatureName.forEach((feature, value) => { + if (!feature) { + return; + } + + if (navigator.gpu.wgslLanguageFeatures.has(feature)) { + availableFeatures.push(value); + } + }); + + if (availableFeatures.length === 0) { + return; + } + + const featuresAddr = this.mem.exports.wgpu_alloc(availableFeatures.length * 4); + this.assert(featuresAddr != 0); + + let off = this.struct(featuresPtr); + this.mem.storeUint(off(this.mem.intSize), availableFeatures.length); + this.mem.storeI32(off(4), featuresAddr); + + off = this.struct(featuresAddr); + for (let i = 0; i < availableFeatures.length; i += 1) { + this.mem.storeI32(off(4), availableFeatures[i]); + } + + return STATUS_SUCCESS; + }, + + /** + * @param {number} instanceIdx + * @param {number} featureInt + * @returns {boolean} + */ + wgpuInstanceHasWGSLLanguageFeature: (instanceIdx, featureInt) => { + return navigator.gpu.wgslLanguageFeatures.has(ENUMS.WGSLLanguageFeatureName[featureInt]); + }, + + /** + * @param {number} instanceIdx + */ + wgpuInstanceProcessEvents: (instanceIdx) => { + console.warn("unimplemented: wgpuInstanceProcessEvents"); + }, + + /** + * @param {number} instanceIdx + * @param {0|number} optionsPtr + * @param {number} callbackInfo + * @returns {number} + */ + wgpuInstanceRequestAdapter: (instanceIdx, optionsPtr, callbackInfoPtr) => { + this.assert(instanceIdx > 0); + + /** @type {GPURequestAdapterOptions} */ + let options; + if (optionsPtr != 0) { + const off = this.struct(optionsPtr); + off(4); // nextInChain + off(4); // featureLevel + options = { + powerPreference: this.enumeration("PowerPreference", off(4)), + forceFallbackAdapter: this.mem.loadB32(off(4)), + }; + } + + const callbackInfo = this.CallbackInfo(callbackInfoPtr); + navigator.gpu.requestAdapter(options) + .catch((e) => { + const messageAddr = this.makeMessageArg(e.message); + this.callCallback(callbackInfo, [ENUMS.RequestAdapterStatus.indexOf("Error"), null, messageAddr]); + this.mem.exports.wgpu_free(messageAddr); + }) + .then((adapter) => { + const adapterIdx = this.adapters.create(adapter); + + this.callCallback(callbackInfo, [ENUMS.RequestAdapterStatus.indexOf("Success"), adapterIdx, this.zeroMessageArg()]); + }); + + // TODO: futures? + return BigInt(0); + }, + + wgpuInstanceWaitAny: (instanceIdx, futureCount, futuresPtr, timeoutNS) => { + // TODO: futures? + console.warn("unimplemented: wgpuInstanceProcessEvents"); + return BigInt(0); + }, + + ...this.instances.interface(false), + + /* ---------------------- PipelineLayout ---------------------- */ + + ...this.pipelineLayouts.interface(true), + + /* ---------------------- QuerySet ---------------------- */ + + /** + * @param {number} querySetIdx + */ + wgpuQuerySetDestroy: (querySetIdx) => { + const querySet = this.querySets.get(querySetIdx); + querySet.destroy(); + }, + + /** + * @param {number} querySetIdx + * @returns {number} + */ + wgpuQuerySetGetCount: (querySetIdx) => { + const querySet = this.querySets.get(querySetIdx); + return querySet.count; + }, + + /** + * @param {number} querySetIdx + * @returns {number} + */ + wgpuQuerySetGetType: (querySetIdx) => { + const querySet = this.querySets.get(querySetIdx); + return ENUMS.QueryType.indexOf(querySet.type); + }, + + ...this.querySets.interface(true), + + /* ---------------------- Queue ---------------------- */ + + /** + * @param {number} queueIdx + * @param {number} callbackInfo + */ + wgpuQueueOnSubmittedWorkDone: (queueIdx, callbackInfoPtr) => { + const queue = this.queues.get(queueIdx); + + const callbackInfo = this.CallbackInfo(callbackInfoPtr); + queue.onSubmittedWorkDone() + .catch((e) => { + console.warn(e); + this.callCallback(callbackInfo, [ENUMS.QueueWorkDoneStatus.indexOf("Error")]); + }) + .then(() => { + this.callCallback(callbackInfo, [ENUMS.QueueWorkDoneStatus.indexOf("Success")]); + }); + + // TODO: futures? + return BigInt(0); + }, + + /** + * @param {number} queueIdx + * @param {BigInt|number} commandCount + * @param {number} commandsPtr + */ + wgpuQueueSubmit: (queueIdx, commandCount, commandsPtr) => { + const queue = this.queues.get(queueIdx); + const commands = this.array( + this.unwrapBigInt(commandCount), + commandsPtr, + (ptr) => this.commandBuffers.get(this.mem.loadPtr(ptr)), + 4, + ); + queue.submit(commands); + }, + + /** + * @param {number} queueIdx + * @param {number} bufferIdx + * @param {BigInt} bufferOffset + * @param {number} dataPtr + * @param {number|BigInt} size + */ + wgpuQueueWriteBuffer: (queueIdx, bufferIdx, bufferOffset, dataPtr, size) => { + const queue = this.queues.get(queueIdx); + const buffer = this.buffers.get(bufferIdx); + bufferOffset = this.unwrapBigInt(bufferOffset); + size = this.unwrapBigInt(size); + queue.writeBuffer(buffer.buffer, bufferOffset, this.mem.loadBytes(dataPtr, size), 0, size); + }, + + /** + * @param {number} queueIdx + * @param {number} destinationPtr + * @param {number} dataPtr + * @param {number|BigInt} dataSize + * @param {number} dataLayoutPtr + * @param {number} writeSizePtr + */ + wgpuQueueWriteTexture: (queueIdx, destinationPtr, dataPtr, dataSize, dataLayoutPtr, writeSizePtr) => { + const queue = this.queues.get(queueIdx); + const destination = this.TexelCopyTextureInfo(destinationPtr); + dataSize = this.unwrapBigInt(dataSize); + const dataLayout = this.TexelCopyBufferLayout(dataLayoutPtr); + const writeSize = this.Extent3D(writeSizePtr); + queue.writeTexture(destination, this.mem.loadBytes(dataPtr, dataSize), dataLayout, writeSize); + }, + + ...this.queues.interface(true), + + /* ---------------------- RenderBundle ---------------------- */ + + ...this.renderBundles.interface(true), + + /* ---------------------- RenderBundleEncoder ---------------------- */ + + /** + * @param {number} renderBundleEncoderIdx + * @param {number} vertexCount + * @param {number} instanceCount + * @param {number} firstVertex + * @param {number} firstInstance + */ + wgpuRenderBundleEncoderDraw: (renderBundleEncoderIdx, vertexCount, instanceCount, firstVertex, firstInstance) => { + const renderBundleEncoder = this.renderBundleEncoders.get(renderBundleEncoderIdx); + renderBundleEncoder.draw(vertexCount, instanceCount, firstVertex, firstInstance); + }, + + /** + * @param {number} renderBundleEncoderIdx + * @param {number} indexCount + * @param {number} instanceCount + * @param {number} firstIndex + * @param {number} baseVertex + * @param {number} firstInstance + */ + wgpuRenderBundleEncoderDrawIndexed: (renderBundleEncoderIdx, indexCount, instanceCount, firstIndex, baseVertex, firstInstance) => { + const renderBundleEncoder = this.renderBundleEncoders.get(renderBundleEncoderIdx); + renderBundleEncoder.drawIndexed(indexCount, instanceCount, firstIndex, baseVertex, firstInstance); + }, + + /** + * @param {number} renderBundleEncoderIdx + * @param {number} indirectBufferIdx + * @param {BigInt} indirectOffset + */ + wgpuRenderBundleEncoderDrawIndexedIndirect: (renderBundleEncoderIdx, indirectBufferIdx, indirectOffset) => { + const renderBundleEncoder = this.renderBundleEncoders.get(renderBundleEncoderIdx); + indirectOffset = this.unwrapBigInt(indirectOffset); + const buffer = this.buffers.get(indirectBufferIdx); + renderBundleEncoder.drawIndexedIndirect(buffer.buffer, indirectOffset); + }, + + /** + * @param {number} renderBundleEncoderIdx + * @param {number} indirectBufferIdx + * @param {BigInt} indirectOffset + */ + wgpuRenderBundleEncoderDrawIndirect: (renderBundleEncoderIdx, indirectBufferIdx, indirectOffset) => { + const renderBundleEncoder = this.renderBundleEncoders.get(renderBundleEncoderIdx); + indirectOffset = this.unwrapBigInt(indirectOffset); + const buffer = this.buffers.get(indirectBufferIdx); + renderBundleEncoder.drawIndirect(buffer.buffer, indirectOffset); + }, + + /** + * @param {number} renderBundleEncoderIdx + * @param {0|number} descriptorPtr + * @returns {number} + */ + wgpuRenderBundleEncoderFinish: (renderBundleEncoderIdx, descriptorPtr) => { + const renderBundleEncoder = this.renderBundleEncoders.get(renderBundleEncoderIdx); + + /** @type {?GPURenderBundleDescriptor} */ + let descriptor; + if (descriptorPtr != 0) { + descriptor = { + label: this.StringView(descriptorPtr + 4), + }; + } + + const renderBundle = renderBundleEncoder.finish(descriptor); + return this.renderBundles.create(renderBundle); + }, + + /** + * @param {number} renderBundleEncoderIdx + * @param {number} markerLabelPtr + * @param {number} markerLabelLen + */ + wgpuRenderBundleEncoderInsertDebugMarker: (renderBundleEncoderIdx, markerLabelPtr, markerLabelLen) => { + const renderBundleEncoder = this.renderBundleEncoders.get(renderBundleEncoderIdx); + this.assert(markerLabelPtr != 0); + const markerLabel = this.mem.loadString(markerLabelPtr, markerLabelLen); + renderBundleEncoder.insertDebugMarker(markerLabel); + }, + + /** + * @param {number} renderBundleEncoderIdx + */ + wgpuRenderBundleEncoderPopDebugGroup: (renderBundleEncoderIdx) => { + const renderBundleEncoder = this.renderBundleEncoders.get(renderBundleEncoderIdx); + renderBundleEncoder.popDebugGroup(); + }, + + /** + * @param {number} renderBundleEncoderIdx + * @param {number} groupLabelPtr + * @param {number} grouplabelLen + */ + wgpuRenderBundleEncoderPushDebugGroup: (renderBundleEncoderIdx, groupLabelPtr, grouplabelLen) => { + const renderBundleEncoder = this.renderBundleEncoders.get(renderBundleEncoderIdx); + this.assert(groupLabelPtr!= 0); + const groupLabel = this.mem.loadString(groupLabelPtr, groupLabelLen); + renderBundleEncoder.pushDebugGroup(groupLabel); + }, + + /** + * @param {number} renderBundleEncoderIdx + * @param {number} groupIndex + * @param {0|number} groupIdx + * @param {number|BigInt} dynamicOffsetCount + * @param {number} dynamicOffsetsPtr + */ + wgpuRenderBundleEncoderSetBindGroup: (renderBundleEncoderIdx, groupIndex, groupIdx, dynamicOffsetCount, dynamicOffsetsPtr) => { + const renderBundleEncoder = this.renderBundleEncoders.get(renderBundleEncoderIdx); + + let group; + if (groupIdx > 0) { + group = this.bindGroups.get(groupIdx); + } + + dynamicOffsetCount = this.unwrapBigInt(dynamicOffsetCount); + const dynamicOffsets = this.array( + dynamicOffsetCount, + dynamicOffsetsPtr, + (ptr) => this.mem.loadU32(ptr), + 4 + ); + + renderBundleEncoder.setBindGroup(groupIndex, group, dynamicOffsets); + }, + + /** + * @param {number} renderBundleEncoderIdx + * @param {number} bufferIdx + * @param {number} formatInt + * @param {BigInt} offset + * @param {BigInt} size + */ + wgpuRenderBundleEncoderSetIndexBuffer: (renderBundleEncoderIdx, bufferIdx, formatInt, offset, size) => { + const renderBundleEncoder = this.renderBundleEncoders.get(renderBundleEncoderIdx); + const buffer = this.buffers.get(bufferIdx); + const format = ENUMS.IndexFormat[formatInt]; + offset = this.unwrapBigInt(offset); + size = this.unwrapBigInt(size); + renderBundleEncoder.setIndexBuffer(buffer.buffer, format, offset, size); + }, + + /** + * @param {number} renderBundleEncoderIdx + * @param {number} pipelineIdx + */ + wgpuRenderBundleEncoderSetPipeline: (renderBundleEncoderIdx, pipelineIdx) => { + const renderBundleEncoder = this.renderBundleEncoders.get(renderBundleEncoderIdx); + const pipeline = this.renderPipelines.get(pipelineIdx); + renderBundleEncoder.setPipeline(pipeline); + }, + + /** + * @param {number} renderBundleEncoderIdx + * @param {number} slot + * @param {0|number} bufferIdx + * @param {BigInt} offset + * @param {BigInt} size + */ + wgpuRenderBundleEncoderSetVertexBuffer: (renderBundleEncoderIdx, slot, bufferIdx, offset, size) => { + const renderBundleEncoder = this.renderBundleEncoders.get(renderBundleEncoderIdx); + + let buffer; + if (bufferIdx > 0) { + buffer = this.buffers.get(bufferIdx).buffer; + } + + offset = this.unwrapBigInt(offset); + size = this.unwrapBigInt(size); + renderBundleEncoder.setVertexBuffer(slot, buffer, offset, size); + }, + + ...this.renderBundleEncoders.interface(true), + + /* ---------------------- RenderPassEncoder ---------------------- */ + + /** + * @param {number} renderPassEncoderIdx + * @param {number} queryIndex + */ + wgpuRenderPassEncoderBeginOcclusionQuery: (renderPassEncoderIdx, queryIndex) => { + const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); + renderPassEncoder.beginOcclusionQuery(queryIndex); + }, + + /** + * @param {number} renderPassEncoderIdx + * @param {number} vertexCount + * @param {number} instanceCount + * @param {number} firstVertex + * @param {number} firstInstance + */ + wgpuRenderPassEncoderDraw: (renderPassEncoderIdx, vertexCount, instanceCount, firstVertex, firstInstance) => { + const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); + renderPassEncoder.draw(vertexCount, instanceCount, firstVertex, firstInstance); + }, + + /** + * @param {number} renderPassEncoderIdx + * @param {number} indexCount + * @param {number} instanceCount + * @param {number} firstIndex + * @param {number} baseVertex + * @param {number} firstInstance + */ + wgpuRenderPassEncoderDrawIndexed: (renderPassEncoderIdx, indexCount, instanceCount, firstIndex, baseVertex, firstInstance) => { + const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); + renderPassEncoder.drawIndexed(indexCount, instanceCount, firstIndex, baseVertex, firstInstance); + }, + + /** + * @param {number} renderPassEncoderIdx + * @param {number} indirectBufferIdx + * @param {BigInt} indirectOffset + */ + wgpuRenderPassEncoderDrawIndexedIndirect: (renderPassEncoderIdx, indirectBufferIdx, indirectOffset) => { + const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); + const buffer = this.buffers.get(indirectBufferIdx); + indirectOffset = this.unwrapBigInt(indirectOffset); + renderPassEncoder.drawIndexedIndirect(buffer.buffer, indirectOffset); + }, + + /** + * @param {number} renderPassEncoderIdx + * @param {number} indirectBufferIdx + * @param {BigInt} indirectOffset + */ + wgpuRenderPassEncoderDrawIndirect: (renderPassEncoderIdx, indirectBufferIdx, indirectOffset) => { + const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); + const buffer = this.buffers.get(indirectBufferIdx); + indirectOffset = this.unwrapBigInt(indirectOffset); + renderPassEncoder.drawIndirect(buffer.buffer, indirectOffset); + }, + + /** + * @param {number} renderPassEncoderIdx + */ + wgpuRenderPassEncoderEnd: (renderPassEncoderIdx) => { + const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); + renderPassEncoder.end(); + }, + + /** + * @param {number} renderPassEncoderIdx + */ + wgpuRenderPassEncoderEndOcclusionQuery: (renderPassEncoderIdx) => { + const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); + renderPassEncoder.endOcclusionQuery(); + }, + + /** + * @param {number} renderPassEncoderIdx + * @param {number|BigInt} bundleCount + * @param {number} bundlesPtr + */ + wgpuRenderPassEncoderExecuteBundles: (renderPassEncoderIdx, bundleCount, bundlesPtr) => { + const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); + bundleCount = this.unwrapBigInt(bundleCount); + const bundles = this.array( + bundleCount, + bundlesPtr, + (ptr) => this.renderBundles.get(this.mem.loadPtr(ptr)), + 4, + ); + renderPassEncoder.executeBundles(bundles); + }, + + /** + * @param {number} renderPassEncoderIdx + * @param {number} markerLabelPtr + * @param {number} markerLabelLen + */ + wgpuRenderPassEncoderInsertDebugMarker: (renderPassEncoderIdx, markerLabelPtr, markerLabelLen) => { + const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); + const markerLabel = this.mem.loadString(markerLabelPtr, markerLabelLen); + renderPassEncoder.insertDebugMarker(markerLabel); + }, + + /** + * @param {number} renderPassEncoderIdx + */ + wgpuRenderPassEncoderPopDebugGroup: (renderPassEncoderIdx) => { + const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); + renderPassEncoder.popDebugGroup(); + }, + + /** + * @param {number} renderPassEncoderIdx + * @param {number} groupLabelPtr + * @param {number} groupLabelLen + */ + wgpuRenderPassEncoderPushDebugGroup: (renderPassEncoderIdx, groupLabelPtr, groupLabelLen) => { + const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); + const groupLabel = this.mem.loadString(groupLabelPtr, groupLabelLen); + renderPassEncoder.pushDebugGroup(groupLabel); + }, + + /** + * @param {number} renderPassEncoderIdx + * @param {number} groupIndex + * @param {0|number} groupIdx + * @param {number|BigInt} dynamicOffsetCount + * @param {number} dynamicOffsetsPtr + */ + wgpuRenderPassEncoderSetBindGroup: (renderPassEncoderIdx, groupIndex, groupIdx, dynamicOffsetCount, dynamicOffsetsPtr) => { + const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); + + let group; + if (groupIdx > 0) { + group = this.bindGroups.get(groupIdx); + } + + dynamicOffsetCount = this.unwrapBigInt(dynamicOffsetCount); + const dynamicOffsets = this.array( + dynamicOffsetCount, + dynamicOffsetsPtr, + (ptr) => this.mem.loadU32(ptr), + 4 + ); + + renderPassEncoder.setBindGroup(groupIndex, group, dynamicOffsets); + }, + + /** + * @param {number} renderPassEncoderIdx + * @param {number} colorPtr + */ + wgpuRenderPassEncoderSetBlendConstant: (renderPassEncoderIdx, colorPtr) => { + const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); + this.assert(colorPtr != 0); + renderPassEncoder.setBlendConstant(this.Color(colorPtr)); + }, + + /** + * @param {number} renderPassEncoderIdx + * @param {number} bufferIdx + * @param {number} formatInt + * @param {BigInt} offset + * @param {BigInt} size + */ + wgpuRenderPassEncoderSetIndexBuffer: (renderPassEncoderIdx, bufferIdx, formatInt, offset, size) => { + const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); + const buffer = this.buffers.get(bufferIdx); + const format = ENUMS.IndexFormat[formatInt]; + offset = this.unwrapBigInt(offset); + size = this.unwrapBigInt(size); + renderPassEncoder.setIndexBuffer(buffer.buffer, format, offset, size); + }, + + /** + * @param {number} renderPassEncoderIdx + * @param {number} pipelineIdx + */ + wgpuRenderPassEncoderSetPipeline: (renderPassEncoderIdx, pipelineIdx) => { + const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); + const pipeline = this.renderPipelines.get(pipelineIdx); + renderPassEncoder.setPipeline(pipeline); + }, + + /** + * @param {number} renderPassEncoderIdx + * @param {number} x + * @param {number} y + * @param {number} width + * @param {number} height + */ + wgpuRenderPassEncoderSetScissorRect: (renderPassEncoderIdx, x, y, width, height) => { + const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); + renderPassEncoder.setScissorRect(x, y, width, height); + }, + + /** + * @param {number} renderPassEncoderIdx + * @param {number} reference + */ + wgpuRenderPassEncoderSetStencilReference: (renderPassEncoderIdx, reference) => { + const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); + renderPassEncoder.setStencilReference(reference); + }, + + /** + * @param {number} renderPassEncoderIdx + * @param {number} slot + * @param {0|number} bufferIdx + * @param {BigInt} offset + * @param {BigInt} size + */ + wgpuRenderPassEncoderSetVertexBuffer: (renderPassEncoderIdx, slot, bufferIdx, offset, size) => { + const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); + + let buffer; + if (bufferIdx > 0) { + buffer = this.buffers.get(bufferIdx).buffer; + } + + offset = this.unwrapBigInt(offset); + size = this.unwrapBigInt(size); + renderPassEncoder.setVertexBuffer(slot, buffer, offset, size); + }, + + /** + * @param {number} renderPassEncoderIdx + * @param {number} x + * @param {number} y + * @param {number} width + * @param {number} height + * @param {number} minDepth + * @param {number} maxDepth + */ + wgpuRenderPassEncoderSetViewport: (renderPassEncoderIdx, x, y, width, height, minDepth, maxDepth) => { + const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); + renderPassEncoder.setViewport(x, y, width, height, minDepth, maxDepth); + }, + + ...this.renderPassEncoders.interface(true), + + /* ---------------------- RenderPipeline ---------------------- */ + + /** + * @param {number} renderPipelineIdx + * @param {number} groupIndex + * @returns {number} + */ + wgpuRenderPipelineGetBindGroupLayout: (renderPipelineIdx, groupIndex) => { + const renderPipeline = this.renderPipelines.get(renderPipelineIdx); + const bindGroupLayout = renderPipeline.getBindGroupLayout(groupIndex); + return this.bindGroupLayouts.create(bindGroupLayout); + }, + + ...this.renderPipelines.interface(true), + + /* ---------------------- Sampler ---------------------- */ + + ...this.samplers.interface(true), + + /* ---------------------- ShaderModule ---------------------- */ + + /** + * @param {number} shaderModuleIdx + * @param {number} callbackInfo + */ + wgpuShaderModuleGetCompilationInfo: (shaderModuleIdx, callbackInfoPtr) => { + const shaderModule = this.shaderModules.get(shaderModuleIdx); + + const callbackInfo = this.CallbackInfo(callbackInfoPtr); + shaderModule.getCompilationInfo() + .catch((e) => { + console.warn(e); + this.callCallback(callbackInfo, [ENUMS.CompilationInfoRequestStatus.indexOf("Error"), null]); + }) + .then((compilationInfo) => { + const ptrsToFree = []; + + const compilationMessageSize = this.sizes.CompilationMessage[0]; + + const size = compilationInfo.messages.length * compilationMessageSize; + + const addr = this.mem.exports.wgpu_alloc(size); + ptrsToFree.push(addr); + + compilationInfo.messages.forEach((message, i) => { + const messageLength = new TextEncoder().encode(message.message).length; + const messageAddr = this.mem.exports.wgpu_alloc(messageLength); + ptrsToFree.push(messageAddr); + this.mem.storeString(messageAddr, message.message); + + const off = this.struct(addr + (i * compilationMessageSize)); + off(4); + + const messageStart = off(this.sizes.StringView); + this.mem.storeI32(messageStart, messageAddr); + this.mem.storeUint(messageStart + this.mem.intSize, messageLength); + + this.mem.storeI32(off(4), ENUMS.CompilationMessageType.indexOf(message.type)); + + this.mem.storeU64(off(8), message.lineNum); + this.mem.storeU64(off(8), message.linePos); + this.mem.storeU64(off(8), message.offset); + this.mem.storeU64(off(8), message.length); + }); + + const retAddr = this.mem.exports.wgpu_alloc(3*this.mem.intSize); + ptrsToFree.push(retAddr); + this.mem.storeUint(retAddr + this.mem.intSize, compilationInfo.messages.length); + this.mem.storeI32(retAddr + this.mem.intSize*2, addr); + + this.callCallback(callbackInfo, [ENUMS.CompilationInfoRequestStatus.indexOf("Success"), retAddr]); + + ptrsToFree.forEach(ptr => this.mem.exports.wgpu_free(ptr)); + }); + + // TODO: futures? + return BigInt(0); + }, + + ...this.shaderModules.interface(true), + + /* ---------------------- SupportedFeatures ---------------------- */ + + wgpuSupportedFeaturesFreeMembers: (supportedFeaturesCount, supportedFeaturesPtr) => { + this.mem.exports.wgpu_free(supportedFeaturesPtr); + }, + + /* ---------------------- SupportedWGSLLanguageFeatures ---------------------- */ + + wgpuSupportedWGSLLanguageFeaturesFreeMembers: (supportedFeaturesCount, supportedFeaturesPtr) => { + this.mem.exports.wgpu_free(supportedFeaturesPtr); + }, + + /* ---------------------- Surface ---------------------- */ + + /** + * @param {number} surfaceIdx + * @param {number} configPtr + */ + wgpuSurfaceConfigure: (surfaceIdx, configPtr) => { + const surface = this.surfaces.get(surfaceIdx); + const context = surface.getContext("webgpu"); + + const off = this.struct(configPtr); + off(4); + const device = this.devices.get(this.mem.loadPtr(off(4))); + const format = this.enumeration("TextureFormat", off(4)); + const usage = this.mem.loadU64(off(8)); + const width = this.mem.loadU32(off(4)); + const height = this.mem.loadU32(off(4)); + const viewFormats = this.array( + this.mem.loadUint(off(this.mem.intSize)), + this.mem.loadPtr(off(4)), + (ptr) => this.enumeration("TextureFormat", ptr), + 4, + ); + const alphaMode = this.enumeration("CompositeAlphaMode", off(4)); + // NOTE: present mode seems unused. + const presentMode = this.enumeration("PresentMode", off(4)); + + surface.width = width; + surface.height = height; + + /** @type {GPUCanvasConfiguration} */ + const config = { + device: device, + format: format, + usage: usage, + viewFormats: viewFormats, + alphaMode: alphaMode, + presentMode: presentMode, + }; + + context.configure(config); + }, + + /** + * @param {number} surfaceIdx + * @param {number} adapterIdx + * @param {number} capabilitiesPtr + * @return {number} + */ + wgpuSurfaceGetCapabilities: (surfaceIdx, adapterIdx, capabilitiesPtr) => { + const off = this.struct(capabilitiesPtr); + off(4); // nextInChain + off(8); // usages TODO: can we pass this? + + const formatStr = navigator.gpu.getPreferredCanvasFormat(); + const format = ENUMS.TextureFormat.indexOf(formatStr); + + this.mem.storeUint(off(this.mem.intSize), 1); + const formatAddr = this.mem.exports.wgpu_alloc(4); + this.mem.storeI32(formatAddr, format); + this.mem.storeI32(off(4), formatAddr); + + // NOTE: present modes don't seem to actually do anything in JS, we can just give back a default FIFO though. + this.mem.storeUint(off(this.mem.intSize), 1); + const presentModesAddr = this.mem.exports.wgpu_alloc(4); + this.mem.storeI32(presentModesAddr, ENUMS.PresentMode.indexOf("fifo")); + this.mem.storeI32(off(4), presentModesAddr); + + // Browser seems to support opaque and premultiplied. + this.mem.storeUint(off(this.mem.intSize), 2); + const alphaModesAddr = this.mem.exports.wgpu_alloc(8); + this.mem.storeI32(alphaModesAddr + 0, ENUMS.CompositeAlphaMode.indexOf("opaque")); + this.mem.storeI32(alphaModesAddr + 4, ENUMS.CompositeAlphaMode.indexOf("premultiplied")); + this.mem.storeI32(off(4), alphaModesAddr); + + return STATUS_SUCCESS; + }, + + /** + * @param {number} surfaceIdx + * @param {number} texturePtr + */ + wgpuSurfaceGetCurrentTexture: (surfaceIdx, texturePtr) => { + const surface = this.surfaces.get(surfaceIdx); + const context = surface.getContext('webgpu'); + const texture = context.getCurrentTexture(); + + const textureIdx = this.textures.create(texture); + this.mem.storeI32(texturePtr + 4, textureIdx); + + // TODO: determine suboptimal and/or status. + }, + + /** + * @param {number} surfaceIdx + */ + wgpuSurfacePresent: (surfaceIdx) => { + // NOTE: Not really anything to do here. + }, + + /** + * @param {number} surfaceIdx + */ + wgpuSurfaceUnconfigure: (surfaceIdx) => { + const surface = this.surfaces.get(surfaceIdx); + surface.getContext('webgpu').unconfigure(); + }, + + ...this.surfaces.interface(true), + + /* ---------------------- SurfaceCapabilities ---------------------- */ + + /** + * @param {number} surfaceCapabilitiesPtr + */ + wgpuSurfaceCapabilitiesFreeMembers: (surfaceCapabilitiesPtr) => { + const off = this.struct(surfaceCapabilitiesPtr); + off(4); // nextInChain + off(8); // usages + off(this.mem.intSize); // formatCount + + const formatsAddr = this.mem.loadPtr(off(4)); + this.mem.exports.wgpu_free(formatsAddr); + + off(this.mem.intSize); // presentModeCount + + const presentModesAddr = this.mem.loadPtr(off(4)); + this.mem.exports.wgpu_free(presentModesAddr); + + off(this.mem.intSize); // alphaModeCount + + const alphaModesAddr = this.mem.loadPtr(off(4)); + this.mem.exports.wgpu_free(alphaModesAddr); + }, + + /* ---------------------- Texture ---------------------- */ + + /** + * @param {number} textureIdx + * @param {0|number} descriptorPtr + * @returns {number} + */ + wgpuTextureCreateView: (textureIdx, descriptorPtr) => { + const texture = this.textures.get(textureIdx); + + /** @type {?GPUTextureViewDescriptor} */ + let descriptor; + if (descriptorPtr != 0) { + const off = this.struct(descriptorPtr); + off(4); + descriptor = { + label: this.StringView(off(this.sizes.StringView)), + format: this.enumeration("TextureFormat", off(4)), + dimension: this.enumeration("TextureViewDimension", off(4)), + baseMipLevel: this.mem.loadU32(off(4)), + mipLevelCount: this.mem.loadU32(off(4)), + baseArrayLayer: this.mem.loadU32(off(4)), + arrayLayerCount: this.mem.loadU32(off(4)), + aspect: this.enumeration("TextureAspect", off(4)), + usage: this.mem.loadU64(off(8)), + }; + if (descriptor.arrayLayerCount == 0xFFFFFFFF) { + descriptor.arrayLayerCount = undefined; + } + if (descriptor.mipLevelCount == 0xFFFFFFFF) { + descriptor.mipLevelCount = undefined; + } + } + + const textureView = texture.createView(descriptor); + return this.textureViews.create(textureView); + }, + + /** + * @param {number} textureIdx + */ + wgpuTextureDestroy: (textureIdx) => { + const texture = this.textures.get(textureIdx); + texture.destroy(); + }, + + /** + * @param {number} textureIdx + * @returns {number} + */ + wgpuTextureDepthOrArrayLayers: (textureIdx) => { + const texture = this.textures.get(textureIdx); + return texture.depthOrArrayLayers; + }, + + /** + * @param {number} textureIdx + * @returns {number} + */ + wgpuTextureGetDimension: (textureIdx) => { + const texture = this.textures.get(textureIdx); + return ENUMS.TextureDimension.indexOf(texture.dimension); + }, + + /** + * @param {number} textureIdx + * @returns {number} + */ + wgpuTextureGetFormat: (textureIdx) => { + const texture = this.textures.get(textureIdx); + return ENUMS.TextureFormat.indexOf(texture.format); + }, + + /** + * @param {number} textureIdx + * @returns {number} + */ + wgpuTextureGetHeight: (textureIdx) => { + const texture = this.textures.get(textureIdx); + return texture.height; + }, + + /** + * @param {number} textureIdx + * @returns {number} + */ + wgpuTextureGetMipLevelCount: (textureIdx) => { + const texture = this.textures.get(textureIdx); + return texture.mipLevelCount; + }, + + /** + * @param {number} textureIdx + * @returns {number} + */ + wgpuTextureGetSampleCount: (textureIdx) => { + const texture = this.textures.get(textureIdx); + return texture.sampleCount; + }, + + /** + * @param {number} textureIdx + * @returns {number} + */ + wgpuTextureGetUsage: (textureIdx) => { + const texture = this.textures.get(textureIdx); + return texture.usage; + }, + + /** + * @param {number} textureIdx + * @returns {number} + */ + wgpuTextureGetWidth: (textureIdx) => { + const texture = this.textures.get(textureIdx); + return texture.width; + }, + + ...this.textures.interface(true), + + /* ---------------------- TextureView ---------------------- */ + + ...this.textureViews.interface(true), + }; + } +} + +/** @template T */ +class WebGPUObjectManager { + + /** + * @param {string} name + * @param {WasmMemoryInterface} mem + */ + constructor(name, mem) { + this.name = name; + this.mem = mem; + + this.idx = 0; + + /** @type {Record} */ + this.objects = {}; + } + + /** + * @param {T} object + * @returns {number} + */ + create(object) { + this.objects[this.idx] = { references: 1, object }; + this.idx += 1; + return this.idx; + } + + /** + * @param {?number} idx + * @returns {T} + */ + get(idx) { + return this.objects[idx-1]?.object; + } + + /** @param {number} idx */ + release(idx) { + this.objects[idx-1].references -= 1; + if (this.objects[idx-1].references == 0) { + delete this.objects[idx-1]; + } + } + + /** @param {number} idx */ + reference(idx) { + this.objects[idx-1].references += 1; + } + + interface(withLabelSetter = false) { + const inter = {}; + inter[`wgpu${this.name}AddRef`] = this.reference.bind(this); + inter[`wgpu${this.name}Release`] = this.release.bind(this); + if (withLabelSetter) { + inter[`wgpu${this.name}SetLabel`] = (idx, labelPtr, labelLen) => { + const obj = this.get(idx); + obj.label = this.mem.loadString(labelPtr, labelLen); + }; + } + return inter; + } +} + +window.odin = window.odin || {}; +window.odin.WebGPUInterface = WebGPUInterface; + +})(); diff --git a/wgpu/sdl3-triangle/build_web.bat b/wgpu/sdl3/triangle/build_web.bat similarity index 100% rename from wgpu/sdl3-triangle/build_web.bat rename to wgpu/sdl3/triangle/build_web.bat diff --git a/wgpu/sdl3-triangle/build_web.sh b/wgpu/sdl3/triangle/build_web.sh similarity index 100% rename from wgpu/sdl3-triangle/build_web.sh rename to wgpu/sdl3/triangle/build_web.sh diff --git a/wgpu/sdl3-triangle/main.odin b/wgpu/sdl3/triangle/main.odin similarity index 100% rename from wgpu/sdl3-triangle/main.odin rename to wgpu/sdl3/triangle/main.odin diff --git a/wgpu/sdl3-triangle/os_js.odin b/wgpu/sdl3/triangle/os_js.odin similarity index 100% rename from wgpu/sdl3-triangle/os_js.odin rename to wgpu/sdl3/triangle/os_js.odin diff --git a/wgpu/sdl3-triangle/os_sdl3.odin b/wgpu/sdl3/triangle/os_sdl3.odin similarity index 100% rename from wgpu/sdl3-triangle/os_sdl3.odin rename to wgpu/sdl3/triangle/os_sdl3.odin diff --git a/wgpu/sdl3-triangle/web/index.html b/wgpu/sdl3/triangle/web/index.html similarity index 100% rename from wgpu/sdl3-triangle/web/index.html rename to wgpu/sdl3/triangle/web/index.html From d8aa9a6d22a3c038a7f8a3f6a02152f4f069b939 Mon Sep 17 00:00:00 2001 From: sangrail Date: Mon, 13 Oct 2025 19:07:21 +0100 Subject: [PATCH 2/4] Fix path for wgpu/sdl3 triangle check in CI workflow --- .github/workflows/check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index e9c67fc..5faca5d 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -143,7 +143,7 @@ jobs: odin check wgpu/glfw-triangle -target:js_wasm32 $FLAGS odin check wgpu/sdl3/triangle -target:windows_amd64 $FLAGS - odin check wgpu/sdl3.triangle -target:js_wasm32 $FLAGS + odin check wgpu/sdl3/triangle -target:js_wasm32 $FLAGS odin check wgpu/sdl3/game_of_life -target:windows_amd64 $FLAGS odin check wgpu/sdl3/game_of_life -target:js_wasm32 $FLAGS From 9912410796d17eb1c3d3248a9733103853938a6a Mon Sep 17 00:00:00 2001 From: sangrail Date: Mon, 13 Oct 2025 19:12:58 +0100 Subject: [PATCH 3/4] Fix path for wgpu/sdl3 game of life checks in CI workflow --- .github/workflows/check.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 5faca5d..fd69394 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -145,8 +145,8 @@ jobs: odin check wgpu/sdl3/triangle -target:windows_amd64 $FLAGS odin check wgpu/sdl3/triangle -target:js_wasm32 $FLAGS - odin check wgpu/sdl3/game_of_life -target:windows_amd64 $FLAGS - odin check wgpu/sdl3/game_of_life -target:js_wasm32 $FLAGS + odin check wgpu/sdl3/game-of-life -target:windows_amd64 $FLAGS + odin check wgpu/sdl3/game-of-life -target:js_wasm32 $FLAGS odin check win32/game_of_life -target:windows_amd64 $FLAGS odin check win32/open_window -target:windows_amd64 $FLAGS From 8e5beaf5779d424e0dda5816a384f889a39f16e7 Mon Sep 17 00:00:00 2001 From: sangrail Date: Fri, 24 Oct 2025 14:05:01 +0100 Subject: [PATCH 4/4] Remove unneccesary files - created or moved during builds. --- wgpu/sdl3/game-of-life/web/game_of_life.wasm | Bin 161158 -> 0 bytes wgpu/sdl3/game-of-life/web/odin.js | 2157 ------------ wgpu/sdl3/game-of-life/web/wgpu.js | 3311 ------------------ 3 files changed, 5468 deletions(-) delete mode 100755 wgpu/sdl3/game-of-life/web/game_of_life.wasm delete mode 100644 wgpu/sdl3/game-of-life/web/odin.js delete mode 100644 wgpu/sdl3/game-of-life/web/wgpu.js diff --git a/wgpu/sdl3/game-of-life/web/game_of_life.wasm b/wgpu/sdl3/game-of-life/web/game_of_life.wasm deleted file mode 100755 index 06b5a7182902e06c746b7e3eb41da445ad611e0b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 161158 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

PYvw4OXRsE35 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 P>N1+sgOh+ { - let name = this.mem.loadString(name_ptr, name_len); - let element = getElement(name); - return this.setCurrentContext(element, {alpha: true, antialias: true, depth: true, premultipliedAlpha: true}); - }, - CreateCurrentContextById: (name_ptr, name_len, attributes) => { - let name = this.mem.loadString(name_ptr, name_len); - let element = getElement(name); - - let contextSettings = { - alpha: !(attributes & (1<<0)), - antialias: !(attributes & (1<<1)), - depth: !(attributes & (1<<2)), - failIfMajorPerformanceCaveat: !!(attributes & (1<<3)), - premultipliedAlpha: !(attributes & (1<<4)), - preserveDrawingBuffer: !!(attributes & (1<<5)), - stencil: !!(attributes & (1<<6)), - desynchronized: !!(attributes & (1<<7)), - }; - - return this.setCurrentContext(element, contextSettings); - }, - GetCurrentContextAttributes: () => { - if (!this.ctx) { - return 0; - } - let attrs = this.ctx.getContextAttributes(); - let res = 0; - if (!attrs.alpha) res |= 1<<0; - if (!attrs.antialias) res |= 1<<1; - if (!attrs.depth) res |= 1<<2; - if (attrs.failIfMajorPerformanceCaveat) res |= 1<<3; - if (!attrs.premultipliedAlpha) res |= 1<<4; - if (attrs.preserveDrawingBuffer) res |= 1<<5; - if (attrs.stencil) res |= 1<<6; - if (attrs.desynchronized) res |= 1<<7; - return res; - }, - - DrawingBufferWidth: () => this.ctx.drawingBufferWidth, - DrawingBufferHeight: () => this.ctx.drawingBufferHeight, - - IsExtensionSupported: (name_ptr, name_len) => { - let name = this.mem.loadString(name_ptr, name_len); - let extensions = this.ctx.getSupportedExtensions(); - return extensions.indexOf(name) !== -1 - }, - - - GetError: () => { - let err = this.lastError; - this.recordError(0); - if (err) { - return err; - } - return this.ctx.getError(); - }, - - GetWebGLVersion: (major_ptr, minor_ptr) => { - let version = this.ctx.getParameter(0x1F02); - if (version.indexOf("WebGL 2.0") !== -1) { - this.mem.storeI32(major_ptr, 2); - this.mem.storeI32(minor_ptr, 0); - return; - } - - this.mem.storeI32(major_ptr, 1); - this.mem.storeI32(minor_ptr, 0); - }, - GetESVersion: (major_ptr, minor_ptr) => { - let version = this.ctx.getParameter(0x1F02); - if (version.indexOf("OpenGL ES 3.0") !== -1) { - this.mem.storeI32(major_ptr, 3); - this.mem.storeI32(minor_ptr, 0); - return; - } - - this.mem.storeI32(major_ptr, 2); - this.mem.storeI32(minor_ptr, 0); - }, - - - ActiveTexture: (x) => { - this.ctx.activeTexture(x); - }, - AttachShader: (program, shader) => { - this.ctx.attachShader(this.programs[program], this.shaders[shader]); - }, - BindAttribLocation: (program, index, name_ptr, name_len) => { - let name = this.mem.loadString(name_ptr, name_len); - this.ctx.bindAttribLocation(this.programs[program], index, name) - }, - BindBuffer: (target, buffer) => { - let bufferObj = buffer ? this.buffers[buffer] : null; - if (target == 35051) { - this.ctx.currentPixelPackBufferBinding = buffer; - } else { - if (target == 35052) { - this.ctx.currentPixelUnpackBufferBinding = buffer; - } - this.ctx.bindBuffer(target, bufferObj) - } - }, - BindFramebuffer: (target, framebuffer) => { - this.ctx.bindFramebuffer(target, framebuffer ? this.framebuffers[framebuffer] : null) - }, - BindTexture: (target, texture) => { - this.ctx.bindTexture(target, texture ? this.textures[texture] : null) - }, - BindRenderbuffer: (target, renderbuffer) => { - this.ctx.bindRenderbuffer(target, renderbuffer ? this.renderbuffers[renderbuffer] : null) - }, - BlendColor: (red, green, blue, alpha) => { - this.ctx.blendColor(red, green, blue, alpha); - }, - BlendEquation: (mode) => { - this.ctx.blendEquation(mode); - }, - BlendEquationSeparate: (modeRGB, modeAlpha) => { - this.ctx.blendEquationSeparate(modeRGB, modeAlpha); - }, - BlendFunc: (sfactor, dfactor) => { - this.ctx.blendFunc(sfactor, dfactor); - }, - BlendFuncSeparate: (srcRGB, dstRGB, srcAlpha, dstAlpha) => { - this.ctx.blendFuncSeparate(srcRGB, dstRGB, srcAlpha, dstAlpha); - }, - - - BufferData: (target, size, data, usage) => { - if (data) { - this.ctx.bufferData(target, this.mem.loadBytes(data, size), usage); - } else { - this.ctx.bufferData(target, size, usage); - } - }, - BufferSubData: (target, offset, size, data) => { - if (data) { - this.ctx.bufferSubData(target, offset, this.mem.loadBytes(data, size)); - } else { - this.ctx.bufferSubData(target, offset, null); - } - }, - - - Clear: (x) => { - this.ctx.clear(x); - }, - ClearColor: (r, g, b, a) => { - this.ctx.clearColor(r, g, b, a); - }, - ClearDepth: (x) => { - this.ctx.clearDepth(x); - }, - ClearStencil: (x) => { - this.ctx.clearStencil(x); - }, - ColorMask: (r, g, b, a) => { - this.ctx.colorMask(!!r, !!g, !!b, !!a); - }, - CompileShader: (shader) => { - this.ctx.compileShader(this.shaders[shader]); - }, - - - CompressedTexImage2D: (target, level, internalformat, width, height, border, imageSize, data) => { - if (data) { - this.ctx.compressedTexImage2D(target, level, internalformat, width, height, border, this.mem.loadBytes(data, imageSize)); - } else { - this.ctx.compressedTexImage2D(target, level, internalformat, width, height, border, null); - } - }, - CompressedTexSubImage2D: (target, level, xoffset, yoffset, width, height, format, imageSize, data) => { - if (data) { - this.ctx.compressedTexSubImage2D(target, level, xoffset, yoffset, width, height, format, this.mem.loadBytes(data, imageSize)); - } else { - this.ctx.compressedTexSubImage2D(target, level, xoffset, yoffset, width, height, format, null); - } - }, - - CopyTexImage2D: (target, level, internalformat, x, y, width, height, border) => { - this.ctx.copyTexImage2D(target, level, internalformat, x, y, width, height, border); - }, - CopyTexSubImage2D: (target, level, xoffset, yoffset, x, y, width, height) => { - this.ctx.copyTexSubImage2D(target, level, xoffset, yoffset, x, y, width, height); - }, - - - CreateBuffer: () => { - let buffer = this.ctx.createBuffer(); - if (!buffer) { - this.recordError(1282); - return 0; - } - let id = this.getNewId(this.buffers); - buffer.name = id - this.buffers[id] = buffer; - return id; - }, - CreateFramebuffer: () => { - let buffer = this.ctx.createFramebuffer(); - let id = this.getNewId(this.framebuffers); - buffer.name = id - this.framebuffers[id] = buffer; - return id; - }, - CreateProgram: () => { - let program = this.ctx.createProgram(); - let id = this.getNewId(this.programs); - program.name = id; - this.programs[id] = program; - return id; - }, - CreateRenderbuffer: () => { - let buffer = this.ctx.createRenderbuffer(); - let id = this.getNewId(this.renderbuffers); - buffer.name = id; - this.renderbuffers[id] = buffer; - return id; - }, - CreateShader: (shaderType) => { - let shader = this.ctx.createShader(shaderType); - let id = this.getNewId(this.shaders); - shader.name = id; - this.shaders[id] = shader; - return id; - }, - CreateTexture: () => { - let texture = this.ctx.createTexture(); - if (!texture) { - this.recordError(1282) - return 0; - } - let id = this.getNewId(this.textures); - texture.name = id; - this.textures[id] = texture; - return id; - }, - - - CullFace: (mode) => { - this.ctx.cullFace(mode); - }, - - - DeleteBuffer: (id) => { - let obj = this.buffers[id]; - if (obj && id != 0) { - this.ctx.deleteBuffer(obj); - this.buffers[id] = null; - } - }, - DeleteFramebuffer: (id) => { - let obj = this.framebuffers[id]; - if (obj && id != 0) { - this.ctx.deleteFramebuffer(obj); - this.framebuffers[id] = null; - } - }, - DeleteProgram: (id) => { - let obj = this.programs[id]; - if (obj && id != 0) { - this.ctx.deleteProgram(obj); - this.programs[id] = null; - } - }, - DeleteRenderbuffer: (id) => { - let obj = this.renderbuffers[id]; - if (obj && id != 0) { - this.ctx.deleteRenderbuffer(obj); - this.renderbuffers[id] = null; - } - }, - DeleteShader: (id) => { - let obj = this.shaders[id]; - if (obj && id != 0) { - this.ctx.deleteShader(obj); - this.shaders[id] = null; - } - }, - DeleteTexture: (id) => { - let obj = this.textures[id]; - if (obj && id != 0) { - this.ctx.deleteTexture(obj); - this.textures[id] = null; - } - }, - - - DepthFunc: (func) => { - this.ctx.depthFunc(func); - }, - DepthMask: (flag) => { - this.ctx.depthMask(!!flag); - }, - DepthRange: (zNear, zFar) => { - this.ctx.depthRange(zNear, zFar); - }, - DetachShader: (program, shader) => { - this.ctx.detachShader(this.programs[program], this.shaders[shader]); - }, - Disable: (cap) => { - this.ctx.disable(cap); - }, - DisableVertexAttribArray: (index) => { - this.ctx.disableVertexAttribArray(index); - }, - DrawArrays: (mode, first, count) => { - this.ctx.drawArrays(mode, first, count); - }, - DrawElements: (mode, count, type, indices) => { - this.ctx.drawElements(mode, count, type, indices); - }, - - - Enable: (cap) => { - this.ctx.enable(cap); - }, - EnableVertexAttribArray: (index) => { - this.ctx.enableVertexAttribArray(index); - }, - Finish: () => { - this.ctx.finish(); - }, - Flush: () => { - this.ctx.flush(); - }, - FramebufferRenderbuffer: (target, attachment, renderbuffertarget, renderbuffer) => { - this.ctx.framebufferRenderbuffer(target, attachment, renderbuffertarget, this.renderbuffers[renderbuffer]); - }, - FramebufferTexture2D: (target, attachment, textarget, texture, level) => { - this.ctx.framebufferTexture2D(target, attachment, textarget, this.textures[texture], level); - }, - FrontFace: (mode) => { - this.ctx.frontFace(mode); - }, - - - GenerateMipmap: (target) => { - this.ctx.generateMipmap(target); - }, - - - GetAttribLocation: (program, name_ptr, name_len) => { - let name = this.mem.loadString(name_ptr, name_len); - return this.ctx.getAttribLocation(this.programs[program], name); - }, - - - GetParameter: (pname) => { - return this.ctx.getParameter(pname); - }, - GetParameter4i: (pname, v0, v1, v2, v3) => { - const i4 = this.ctx.getParameter(pname); - this.mem.storeI32(v0, i4[0]); - this.mem.storeI32(v1, i4[1]); - this.mem.storeI32(v2, i4[2]); - this.mem.storeI32(v3, i4[3]); - }, - GetProgramParameter: (program, pname) => { - return this.ctx.getProgramParameter(this.programs[program], pname) - }, - GetProgramInfoLog: (program, buf_ptr, buf_len, length_ptr) => { - let log = this.ctx.getProgramInfoLog(this.programs[program]); - if (log === null) { - log = "(unknown error)"; - } - if (buf_len > 0 && buf_ptr) { - let n = Math.min(buf_len, log.length); - log = log.substring(0, n); - this.mem.loadBytes(buf_ptr, buf_len).set(new TextEncoder().encode(log)) - - this.mem.storeInt(length_ptr, n); - } - }, - GetShaderInfoLog: (shader, buf_ptr, buf_len, length_ptr) => { - let log = this.ctx.getShaderInfoLog(this.shaders[shader]); - if (log === null) { - log = "(unknown error)"; - } - if (buf_len > 0 && buf_ptr) { - let n = Math.min(buf_len, log.length); - log = log.substring(0, n); - this.mem.loadBytes(buf_ptr, buf_len).set(new TextEncoder().encode(log)) - - this.mem.storeInt(length_ptr, n); - } - }, - GetShaderiv: (shader, pname, p) => { - if (p) { - if (pname == 35716) { - let log = this.ctx.getShaderInfoLog(this.shaders[shader]); - if (log === null) { - log = "(unknown error)"; - } - this.mem.storeInt(p, log.length+1); - } else if (pname == 35720) { - let source = this.ctx.getShaderSource(this.shaders[shader]); - let sourceLength = (source === null || source.length == 0) ? 0 : source.length+1; - this.mem.storeInt(p, sourceLength); - } else { - let param = this.ctx.getShaderParameter(this.shaders[shader], pname); - this.mem.storeI32(p, param); - } - } else { - this.recordError(1281); - } - }, - - - GetUniformLocation: (program, name_ptr, name_len) => { - let name = this.mem.loadString(name_ptr, name_len); - let arrayOffset = 0; - if (name.indexOf("]", name.length - 1) !== -1) { - let ls = name.lastIndexOf("["), - arrayIndex = name.slice(ls + 1, -1); - if (arrayIndex.length > 0 && (arrayOffset = parseInt(arrayIndex)) < 0) { - return -1; - } - name = name.slice(0, ls) - } - var ptable = this.programInfos[program]; - if (!ptable) { - return -1; - } - var uniformInfo = ptable.uniforms[name]; - return (uniformInfo && arrayOffset < uniformInfo[0]) ? uniformInfo[1] + arrayOffset : -1 - }, - - - GetVertexAttribOffset: (index, pname) => { - return this.ctx.getVertexAttribOffset(index, pname); - }, - - - Hint: (target, mode) => { - this.ctx.hint(target, mode); - }, - - - IsBuffer: (buffer) => this.ctx.isBuffer(this.buffers[buffer]), - IsEnabled: (cap) => this.ctx.isEnabled(cap), - IsFramebuffer: (framebuffer) => this.ctx.isFramebuffer(this.framebuffers[framebuffer]), - IsProgram: (program) => this.ctx.isProgram(this.programs[program]), - IsRenderbuffer: (renderbuffer) => this.ctx.isRenderbuffer(this.renderbuffers[renderbuffer]), - IsShader: (shader) => this.ctx.isShader(this.shaders[shader]), - IsTexture: (texture) => this.ctx.isTexture(this.textures[texture]), - - LineWidth: (width) => { - this.ctx.lineWidth(width); - }, - LinkProgram: (program) => { - this.ctx.linkProgram(this.programs[program]); - this.programInfos[program] = null; - this.populateUniformTable(program); - }, - PixelStorei: (pname, param) => { - this.ctx.pixelStorei(pname, param); - }, - PolygonOffset: (factor, units) => { - this.ctx.polygonOffset(factor, units); - }, - - - ReadnPixels: (x, y, width, height, format, type, bufSize, data) => { - this.ctx.readPixels(x, y, width, height, format, type, this.mem.loadBytes(data, bufSize)); - }, - RenderbufferStorage: (target, internalformat, width, height) => { - this.ctx.renderbufferStorage(target, internalformat, width, height); - }, - SampleCoverage: (value, invert) => { - this.ctx.sampleCoverage(value, !!invert); - }, - Scissor: (x, y, width, height) => { - this.ctx.scissor(x, y, width, height); - }, - ShaderSource: (shader, strings_ptr, strings_length) => { - let source = this.getSource(shader, strings_ptr, strings_length); - this.ctx.shaderSource(this.shaders[shader], source); - }, - - StencilFunc: (func, ref, mask) => { - this.ctx.stencilFunc(func, ref, mask); - }, - StencilFuncSeparate: (face, func, ref, mask) => { - this.ctx.stencilFuncSeparate(face, func, ref, mask); - }, - StencilMask: (mask) => { - this.ctx.stencilMask(mask); - }, - StencilMaskSeparate: (face, mask) => { - this.ctx.stencilMaskSeparate(face, mask); - }, - StencilOp: (fail, zfail, zpass) => { - this.ctx.stencilOp(fail, zfail, zpass); - }, - StencilOpSeparate: (face, fail, zfail, zpass) => { - this.ctx.stencilOpSeparate(face, fail, zfail, zpass); - }, - - - TexImage2D: (target, level, internalformat, width, height, border, format, type, size, data) => { - if (data) { - this.ctx.texImage2D(target, level, internalformat, width, height, border, format, type, this.mem.loadBytes(data, size)); - } else { - this.ctx.texImage2D(target, level, internalformat, width, height, border, format, type, null); - } - }, - TexParameterf: (target, pname, param) => { - this.ctx.texParameterf(target, pname, param); - }, - TexParameteri: (target, pname, param) => { - this.ctx.texParameteri(target, pname, param); - }, - TexSubImage2D: (target, level, xoffset, yoffset, width, height, format, type, size, data) => { - this.ctx.texSubImage2D(target, level, xoffset, yoffset, width, height, format, type, this.mem.loadBytes(data, size)); - }, - - - Uniform1f: (location, v0) => { this.ctx.uniform1f(this.uniforms[location], v0); }, - Uniform2f: (location, v0, v1) => { this.ctx.uniform2f(this.uniforms[location], v0, v1); }, - Uniform3f: (location, v0, v1, v2) => { this.ctx.uniform3f(this.uniforms[location], v0, v1, v2); }, - Uniform4f: (location, v0, v1, v2, v3) => { this.ctx.uniform4f(this.uniforms[location], v0, v1, v2, v3); }, - - Uniform1i: (location, v0) => { this.ctx.uniform1i(this.uniforms[location], v0); }, - Uniform2i: (location, v0, v1) => { this.ctx.uniform2i(this.uniforms[location], v0, v1); }, - Uniform3i: (location, v0, v1, v2) => { this.ctx.uniform3i(this.uniforms[location], v0, v1, v2); }, - Uniform4i: (location, v0, v1, v2, v3) => { this.ctx.uniform4i(this.uniforms[location], v0, v1, v2, v3); }, - - Uniform1fv: (location, count, addr) => { - let array = this.mem.loadF32Array(addr, 1*count); - this.ctx.uniform1fv(this.uniforms[location], array); - }, - Uniform2fv: (location, count, addr) => { - let array = this.mem.loadF32Array(addr, 2*count); - this.ctx.uniform2fv(this.uniforms[location], array); - }, - Uniform3fv: (location, count, addr) => { - let array = this.mem.loadF32Array(addr, 3*count); - this.ctx.uniform3fv(this.uniforms[location], array); - }, - Uniform4fv: (location, count, addr) => { - let array = this.mem.loadF32Array(addr, 4*count); - this.ctx.uniform4fv(this.uniforms[location], array); - }, - - Uniform1iv: (location, count, addr) => { - let array = this.mem.loadI32Array(addr, 1*count); - this.ctx.uniform1iv(this.uniforms[location], array); - }, - Uniform2iv: (location, count, addr) => { - let array = this.mem.loadI32Array(addr, 2*count); - this.ctx.uniform2iv(this.uniforms[location], array); - }, - Uniform3iv: (location, count, addr) => { - let array = this.mem.loadI32Array(addr, 3*count); - this.ctx.uniform3iv(this.uniforms[location], array); - }, - Uniform4iv: (location, count, addr) => { - let array = this.mem.loadI32Array(addr, 4*count); - this.ctx.uniform4iv(this.uniforms[location], array); - }, - - UniformMatrix2fv: (location, addr) => { - let array = this.mem.loadF32Array(addr, 2*2); - this.ctx.uniformMatrix2fv(this.uniforms[location], false, array); - }, - UniformMatrix3fv: (location, addr) => { - let array = this.mem.loadF32Array(addr, 3*3); - this.ctx.uniformMatrix3fv(this.uniforms[location], false, array); - }, - UniformMatrix4fv: (location, addr) => { - let array = this.mem.loadF32Array(addr, 4*4); - this.ctx.uniformMatrix4fv(this.uniforms[location], false, array); - }, - - UseProgram: (program) => { - if (program) this.ctx.useProgram(this.programs[program]); - }, - ValidateProgram: (program) => { - if (program) this.ctx.validateProgram(this.programs[program]); - }, - - - VertexAttrib1f: (index, x) => { - this.ctx.vertexAttrib1f(index, x); - }, - VertexAttrib2f: (index, x, y) => { - this.ctx.vertexAttrib2f(index, x, y); - }, - VertexAttrib3f: (index, x, y, z) => { - this.ctx.vertexAttrib3f(index, x, y, z); - }, - VertexAttrib4f: (index, x, y, z, w) => { - this.ctx.vertexAttrib4f(index, x, y, z, w); - }, - VertexAttribPointer: (index, size, type, normalized, stride, ptr) => { - this.ctx.vertexAttribPointer(index, size, type, !!normalized, stride, ptr); - }, - - Viewport: (x, y, w, h) => { - this.ctx.viewport(x, y, w, h); - }, - }; - } - - getWebGL2Interface() { - return { - /* Buffer objects */ - CopyBufferSubData: (readTarget, writeTarget, readOffset, writeOffset, size) => { - this.assertWebGL2(); - this.ctx.copyBufferSubData(readTarget, writeTarget, readOffset, writeOffset, size); - }, - GetBufferSubData: (target, srcByteOffset, dst_buffer_ptr, dst_buffer_len, dstOffset, length) => { - this.assertWebGL2(); - this.ctx.getBufferSubData(target, srcByteOffset, this.mem.loadBytes(dst_buffer_ptr, dst_buffer_len), dstOffset, length); - }, - - /* Framebuffer objects */ - BlitFramebuffer: (srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter) => { - this.assertWebGL2(); - this.ctx.blitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); - }, - FramebufferTextureLayer: (target, attachment, texture, level, layer) => { - this.assertWebGL2(); - this.ctx.framebufferTextureLayer(target, attachment, this.textures[texture], level, layer); - }, - InvalidateFramebuffer: (target, attachments_ptr, attachments_len) => { - this.assertWebGL2(); - let attachments = this.mem.loadU32Array(attachments_ptr, attachments_len); - this.ctx.invalidateFramebuffer(target, attachments); - }, - InvalidateSubFramebuffer: (target, attachments_ptr, attachments_len, x, y, width, height) => { - this.assertWebGL2(); - let attachments = this.mem.loadU32Array(attachments_ptr, attachments_len); - this.ctx.invalidateSubFramebuffer(target, attachments, x, y, width, height); - }, - ReadBuffer: (src) => { - this.assertWebGL2(); - this.ctx.readBuffer(src); - }, - - /* Renderbuffer objects */ - RenderbufferStorageMultisample: (target, samples, internalformat, width, height) => { - this.assertWebGL2(); - this.ctx.renderbufferStorageMultisample(target, samples, internalformat, width, height); - }, - - /* Texture objects */ - - TexStorage3D: (target, levels, internalformat, width, height, depth) => { - this.assertWebGL2(); - this.ctx.texStorage3D(target, levels, internalformat, width, height, depth); - }, - TexImage3D: (target, level, internalformat, width, height, depth, border, format, type, size, data) => { - this.assertWebGL2(); - if (data) { - this.ctx.texImage3D(target, level, internalformat, width, height, depth, border, format, type, this.mem.loadBytes(data, size)); - } else { - this.ctx.texImage3D(target, level, internalformat, width, height, depth, border, format, type, null); - } - }, - TexSubImage3D: (target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, size, data) => { - this.assertWebGL2(); - this.ctx.texSubImage3D(target, level, xoffset, yoffset, zoffset, width, height, depth, format, type, this.mem.loadBytes(data, size)); - }, - CompressedTexImage3D: (target, level, internalformat, width, height, depth, border, imageSize, data) => { - this.assertWebGL2(); - if (data) { - this.ctx.compressedTexImage3D(target, level, internalformat, width, height, depth, border, this.mem.loadBytes(data, imageSize)); - } else { - this.ctx.compressedTexImage3D(target, level, internalformat, width, height, depth, border, null); - } - }, - CompressedTexSubImage3D: (target, level, xoffset, yoffset, zoffset, width, height, depth, format, imageSize, data) => { - this.assertWebGL2(); - if (data) { - this.ctx.compressedTexSubImage3D(target, level, xoffset, yoffset, zoffset, width, height, depth, format, this.mem.loadBytes(data, imageSize)); - } else { - this.ctx.compressedTexSubImage3D(target, level, xoffset, yoffset, zoffset, width, height, depth, format, null); - } - }, - - CopyTexSubImage3D: (target, level, xoffset, yoffset, zoffset, x, y, width, height) => { - this.assertWebGL2(); - this.ctx.copyTexSubImage3D(target, level, xoffset, yoffset, zoffset, x, y, width, height); - }, - - /* Programs and shaders */ - GetFragDataLocation: (program, name_ptr, name_len) => { - this.assertWebGL2(); - return this.ctx.getFragDataLocation(this.programs[program], this.mem.loadString(name_ptr, name_len)); - }, - - /* Uniforms */ - Uniform1ui: (location, v0) => { - this.assertWebGL2(); - this.ctx.uniform1ui(this.uniforms[location], v0); - }, - Uniform2ui: (location, v0, v1) => { - this.assertWebGL2(); - this.ctx.uniform2ui(this.uniforms[location], v0, v1); - }, - Uniform3ui: (location, v0, v1, v2) => { - this.assertWebGL2(); - this.ctx.uniform3ui(this.uniforms[location], v0, v1, v2); - }, - Uniform4ui: (location, v0, v1, v2, v3) => { - this.assertWebGL2(); - this.ctx.uniform4ui(this.uniforms[location], v0, v1, v2, v3); - }, - - UniformMatrix3x2fv: (location, addr) => { - this.assertWebGL2(); - let array = this.mem.loadF32Array(addr, 3*2); - this.ctx.uniformMatrix3x2fv(this.uniforms[location], false, array); - }, - UniformMatrix4x2fv: (location, addr) => { - this.assertWebGL2(); - let array = this.mem.loadF32Array(addr, 4*2); - this.ctx.uniformMatrix4x2fv(this.uniforms[location], false, array); - }, - UniformMatrix2x3fv: (location, addr) => { - this.assertWebGL2(); - let array = this.mem.loadF32Array(addr, 2*3); - this.ctx.uniformMatrix2x3fv(this.uniforms[location], false, array); - }, - UniformMatrix4x3fv: (location, addr) => { - this.assertWebGL2(); - let array = this.mem.loadF32Array(addr, 4*3); - this.ctx.uniformMatrix4x3fv(this.uniforms[location], false, array); - }, - UniformMatrix2x4fv: (location, addr) => { - this.assertWebGL2(); - let array = this.mem.loadF32Array(addr, 2*4); - this.ctx.uniformMatrix2x4fv(this.uniforms[location], false, array); - }, - UniformMatrix3x4fv: (location, addr) => { - this.assertWebGL2(); - let array = this.mem.loadF32Array(addr, 3*4); - this.ctx.uniformMatrix3x4fv(this.uniforms[location], false, array); - }, - - /* Vertex attribs */ - VertexAttribI4i: (index, x, y, z, w) => { - this.assertWebGL2(); - this.ctx.vertexAttribI4i(index, x, y, z, w); - }, - VertexAttribI4ui: (index, x, y, z, w) => { - this.assertWebGL2(); - this.ctx.vertexAttribI4ui(index, x, y, z, w); - }, - VertexAttribIPointer: (index, size, type, stride, offset) => { - this.assertWebGL2(); - this.ctx.vertexAttribIPointer(index, size, type, stride, offset); - }, - - /* Writing to the drawing buffer */ - VertexAttribDivisor: (index, divisor) => { - this.assertWebGL2(); - this.ctx.vertexAttribDivisor(index, divisor); - }, - DrawArraysInstanced: (mode, first, count, instanceCount) => { - this.assertWebGL2(); - this.ctx.drawArraysInstanced(mode, first, count, instanceCount); - }, - DrawElementsInstanced: (mode, count, type, offset, instanceCount) => { - this.assertWebGL2(); - this.ctx.drawElementsInstanced(mode, count, type, offset, instanceCount); - }, - DrawRangeElements: (mode, start, end, count, type, offset) => { - this.assertWebGL2(); - this.ctx.drawRangeElements(mode, start, end, count, type, offset); - }, - - /* Multiple Render Targets */ - DrawBuffers: (buffers_ptr, buffers_len) => { - this.assertWebGL2(); - let array = this.mem.loadU32Array(buffers_ptr, buffers_len); - this.ctx.drawBuffers(array); - }, - ClearBufferfv: (buffer, drawbuffer, values_ptr, values_len) => { - this.assertWebGL2(); - let array = this.mem.loadF32Array(values_ptr, values_len); - this.ctx.clearBufferfv(buffer, drawbuffer, array); - }, - ClearBufferiv: (buffer, drawbuffer, values_ptr, values_len) => { - this.assertWebGL2(); - let array = this.mem.loadI32Array(values_ptr, values_len); - this.ctx.clearBufferiv(buffer, drawbuffer, array); - }, - ClearBufferuiv: (buffer, drawbuffer, values_ptr, values_len) => { - this.assertWebGL2(); - let array = this.mem.loadU32Array(values_ptr, values_len); - this.ctx.clearBufferuiv(buffer, drawbuffer, array); - }, - ClearBufferfi: (buffer, drawbuffer, depth, stencil) => { - this.assertWebGL2(); - this.ctx.clearBufferfi(buffer, drawbuffer, depth, stencil); - }, - - /* Query Objects */ - CreateQuery: () => { - this.assertWebGL2(); - let query = this.ctx.createQuery(); - let id = this.getNewId(this.queries); - query.name = id; - this.queries[id] = query; - return id; - }, - DeleteQuery: (id) => { - this.assertWebGL2(); - let obj = this.queries[id]; - if (obj && id != 0) { - this.ctx.deleteQuery(obj); - this.queries[id] = null; - } - }, - IsQuery: (query) => { - this.assertWebGL2(); - return this.ctx.isQuery(this.queries[query]); - }, - BeginQuery: (target, query) => { - this.assertWebGL2(); - this.ctx.beginQuery(target, this.queries[query]) - }, - EndQuery: (target) => { - this.assertWebGL2(); - this.ctx.endQuery(target); - }, - GetQuery: (target, pname) => { - this.assertWebGL2(); - let query = this.ctx.getQuery(target, pname); - if (!query) { - return 0; - } - if (this.queries.indexOf(query) !== -1) { - return query.name; - } - let id = this.getNewId(this.queries); - query.name = id; - this.queries[id] = query; - return id; - }, - - /* Sampler Objects */ - CreateSampler: () => { - this.assertWebGL2(); - let sampler = this.ctx.createSampler(); - let id = this.getNewId(this.samplers); - sampler.name = id; - this.samplers[id] = sampler; - return id; - }, - DeleteSampler: (id) => { - this.assertWebGL2(); - let obj = this.samplers[id]; - if (obj && id != 0) { - this.ctx.deleteSampler(obj); - this.samplers[id] = null; - } - }, - IsSampler: (sampler) => { - this.assertWebGL2(); - return this.ctx.isSampler(this.samplers[sampler]); - }, - BindSampler: (unit, sampler) => { - this.assertWebGL2(); - this.ctx.bindSampler(unit, this.samplers[sampler]); - }, - SamplerParameteri: (sampler, pname, param) => { - this.assertWebGL2(); - this.ctx.samplerParameteri(this.samplers[sampler], pname, param); - }, - SamplerParameterf: (sampler, pname, param) => { - this.assertWebGL2(); - this.ctx.samplerParameterf(this.samplers[sampler], pname, param); - }, - - /* Sync objects */ - FenceSync: (condition, flags) => { - this.assertWebGL2(); - let sync = this.ctx.fenceSync(condition, flags); - let id = this.getNewId(this.syncs); - sync.name = id; - this.syncs[id] = sync; - return id; - }, - IsSync: (sync) => { - this.assertWebGL2(); - return this.ctx.isSync(this.syncs[sync]); - }, - DeleteSync: (id) => { - this.assertWebGL2(); - let obj = this.syncs[id]; - if (obj && id != 0) { - this.ctx.deleteSampler(obj); - this.syncs[id] = null; - } - }, - ClientWaitSync: (sync, flags, timeout) => { - this.assertWebGL2(); - return this.ctx.clientWaitSync(this.syncs[sync], flags, timeout); - }, - WaitSync: (sync, flags, timeout) => { - this.assertWebGL2(); - this.ctx.waitSync(this.syncs[sync], flags, timeout) ; - }, - - - /* Transform Feedback */ - CreateTransformFeedback: () => { - this.assertWebGL2(); - let transformFeedback = this.ctx.createTransformFeedback(); - let id = this.getNewId(this.transformFeedbacks); - transformFeedback.name = id; - this.transformFeedbacks[id] = transformFeedback; - return id; - }, - DeleteTransformFeedback: (id) => { - this.assertWebGL2(); - let obj = this.transformFeedbacks[id]; - if (obj && id != 0) { - this.ctx.deleteTransformFeedback(obj); - this.transformFeedbacks[id] = null; - } - }, - IsTransformFeedback: (tf) => { - this.assertWebGL2(); - return this.ctx.isTransformFeedback(this.transformFeedbacks[tf]); - }, - BindTransformFeedback: (target, tf) => { - this.assertWebGL2(); - this.ctx.bindTransformFeedback(target, this.transformFeedbacks[tf]); - }, - BeginTransformFeedback: (primitiveMode) => { - this.assertWebGL2(); - this.ctx.beginTransformFeedback(primitiveMode); - }, - EndTransformFeedback: () => { - this.assertWebGL2(); - this.ctx.endTransformFeedback(); - }, - TransformFeedbackVaryings: (program, varyings_ptr, varyings_len, bufferMode) => { - this.assertWebGL2(); - const stringSize = this.mem.intSize*2; - let varyings = []; - for (let i = 0; i < varyings_len; i++) { - let ptr = this.mem.loadPtr(varyings_ptr + i*stringSize + 0*4); - let len = this.mem.loadPtr(varyings_ptr + i*stringSize + 1*4); - varyings.push(this.mem.loadString(ptr, len)); - } - this.ctx.transformFeedbackVaryings(this.programs[program], varyings, bufferMode); - }, - PauseTransformFeedback: () => { - this.assertWebGL2(); - this.ctx.pauseTransformFeedback(); - }, - ResumeTransformFeedback: () => { - this.assertWebGL2(); - this.ctx.resumeTransformFeedback(); - }, - - - /* Uniform Buffer Objects and Transform Feedback Buffers */ - BindBufferBase: (target, index, buffer) => { - this.assertWebGL2(); - this.ctx.bindBufferBase(target, index, this.buffers[buffer]); - }, - BindBufferRange: (target, index, buffer, offset, size) => { - this.assertWebGL2(); - this.ctx.bindBufferRange(target, index, this.buffers[buffer], offset, size); - }, - GetUniformBlockIndex: (program, uniformBlockName_ptr, uniformBlockName_len) => { - this.assertWebGL2(); - return this.ctx.getUniformBlockIndex(this.programs[program], this.mem.loadString(uniformBlockName_ptr, uniformBlockName_len)); - }, - // any getActiveUniformBlockParameter(WebGLProgram program, GLuint uniformBlockIndex, GLenum pname); - GetActiveUniformBlockName: (program, uniformBlockIndex, buf_ptr, buf_len, length_ptr) => { - this.assertWebGL2(); - let name = this.ctx.getActiveUniformBlockName(this.programs[program], uniformBlockIndex); - - let n = Math.min(buf_len, name.length); - name = name.substring(0, n); - this.mem.loadBytes(buf_ptr, buf_len).set(new TextEncoder().encode(name)) - this.mem.storeInt(length_ptr, n); - }, - UniformBlockBinding: (program, uniformBlockIndex, uniformBlockBinding) => { - this.assertWebGL2(); - this.ctx.uniformBlockBinding(this.programs[program], uniformBlockIndex, uniformBlockBinding); - }, - - /* Vertex Array Objects */ - CreateVertexArray: () => { - this.assertWebGL2(); - let vao = this.ctx.createVertexArray(); - let id = this.getNewId(this.vaos); - vao.name = id; - this.vaos[id] = vao; - return id; - }, - DeleteVertexArray: (id) => { - this.assertWebGL2(); - let obj = this.vaos[id]; - if (obj && id != 0) { - this.ctx.deleteVertexArray(obj); - this.vaos[id] = null; - } - }, - IsVertexArray: (vertexArray) => { - this.assertWebGL2(); - return this.ctx.isVertexArray(this.vaos[vertexArray]); - }, - BindVertexArray: (vertexArray) => { - this.assertWebGL2(); - this.ctx.bindVertexArray(this.vaos[vertexArray]); - }, - }; - } -}; - - -function odinSetupDefaultImports(wasmMemoryInterface, consoleElement, memory) { - const MAX_INFO_CONSOLE_LINES = 512; - let infoConsoleLines = new Array(); - let currentLine = {}; - currentLine[false] = ""; - currentLine[true] = ""; - let prevIsError = false; - - let event_temp = {}; - - const onEventReceived = (event_data, data, callback) => { - event_temp.data = event_data; - - const exports = wasmMemoryInterface.exports; - const odin_ctx = exports.default_context_ptr(); - - exports.odin_dom_do_event_callback(data, callback, odin_ctx); - - event_temp.data = null; - }; - - const writeToConsole = (line, isError) => { - if (!line) { - return; - } - - const println = (text, forceIsError) => { - let style = [ - "color: #eee", - "background-color: #d20", - "padding: 2px 4px", - "border-radius: 2px", - ].join(";"); - let doIsError = isError; - if (forceIsError !== undefined) { - doIsError = forceIsError; - } - - if (doIsError) { - console.log("%c"+text, style); - } else { - console.log(text); - } - - }; - - // Print to console - if (line == "\n") { - println(currentLine[isError]); - currentLine[isError] = ""; - } else if (!line.includes("\n")) { - currentLine[isError] = currentLine[isError].concat(line); - } else { - let lines = line.trimEnd().split("\n"); - let printLast = lines.length > 1 && line.endsWith("\n"); - println(currentLine[isError].concat(lines[0])); - currentLine[isError] = ""; - for (let i = 1; i < lines.length-1; i++) { - println(lines[i]); - } - if (lines.length > 1) { - let last = lines[lines.length-1]; - if (printLast) { - println(last); - } else { - currentLine[isError] = last; - } - } - } - - if (prevIsError != isError) { - if (prevIsError) { - println(currentLine[prevIsError], prevIsError); - currentLine[prevIsError] = ""; - } - } - prevIsError = isError; - - - // HTML based console - if (!consoleElement) { - return; - } - const wrap = (x) => { - if (isError) { - return ''+x+''; - } - return x; - }; - - if (line == "\n") { - infoConsoleLines.push(line); - } else if (!line.includes("\n")) { - let prevLine = ""; - if (infoConsoleLines.length > 0) { - prevLine = infoConsoleLines.pop(); - } - infoConsoleLines.push(prevLine.concat(wrap(line))); - } else { - let lines = line.split("\n"); - let lastHasNewline = lines.length > 1 && line.endsWith("\n"); - - let prevLine = ""; - if (infoConsoleLines.length > 0) { - prevLine = infoConsoleLines.pop(); - } - infoConsoleLines.push(prevLine.concat(wrap(lines[0]).concat("\n"))); - - for (let i = 1; i < lines.length-1; i++) { - infoConsoleLines.push(wrap(lines[i]).concat("\n")); - } - let last = lines[lines.length-1]; - if (lastHasNewline) { - infoConsoleLines.push(last.concat("\n")); - } else { - infoConsoleLines.push(last); - } - } - - if (infoConsoleLines.length > MAX_INFO_CONSOLE_LINES) { - infoConsoleLines.shift(MAX_INFO_CONSOLE_LINES); - } - - let data = ""; - for (let i = 0; i < infoConsoleLines.length; i++) { - data = data.concat(infoConsoleLines[i]); - } - - let info = consoleElement; - info.innerHTML = data; - info.scrollTop = info.scrollHeight; - }; - - const listener_key = (id, name, data, callback, useCapture) => { - return `${id}-${name}-data:${data}-callback:${callback}-useCapture:${useCapture}`; - }; - - let webglContext = new WebGLInterface(wasmMemoryInterface); - - const env = {}; - - if (memory) { - env.memory = memory; - } - - return { - env, - "odin_env": { - write: (fd, ptr, len) => { - const str = wasmMemoryInterface.loadString(ptr, len); - if (fd == 1) { - writeToConsole(str, false); - return; - } else if (fd == 2) { - writeToConsole(str, true); - return; - } else { - throw new Error("Invalid fd to 'write'" + stripNewline(str)); - } - }, - trap: () => { throw new Error() }, - alert: (ptr, len) => { alert(wasmMemoryInterface.loadString(ptr, len)) }, - abort: () => { Module.abort() }, - evaluate: (str_ptr, str_len) => { eval.call(null, wasmMemoryInterface.loadString(str_ptr, str_len)); }, - - open: (url_ptr, url_len, name_ptr, name_len, specs_ptr, specs_len) => { - const url = wasmMemoryInterface.loadString(url_ptr, url_len); - const name = wasmMemoryInterface.loadString(name_ptr, name_len); - const specs = wasmMemoryInterface.loadString(specs_ptr, specs_len); - window.open(url, name, specs); - }, - - // return a bigint to be converted to i64 - time_now: () => BigInt(Date.now()), - tick_now: () => performance.now(), - time_sleep: (duration_ms) => { - if (duration_ms > 0) { - // TODO(bill): Does this even make any sense? - } - }, - - sqrt: Math.sqrt, - sin: Math.sin, - cos: Math.cos, - pow: Math.pow, - fmuladd: (x, y, z) => x*y + z, - ln: Math.log, - exp: Math.exp, - ldexp: (x, exp) => x * Math.pow(2, exp), - - rand_bytes: (ptr, len) => { - const view = new Uint8Array(wasmMemoryInterface.memory.buffer, ptr, len) - crypto.getRandomValues(view) - }, - }, - "odin_dom": { - init_event_raw: (ep) => { - const W = wasmMemoryInterface.intSize; - let offset = ep; - let off = (amount, alignment) => { - if (alignment === undefined) { - alignment = Math.min(amount, W); - } - if (offset % alignment != 0) { - offset += alignment - (offset%alignment); - } - let x = offset; - offset += amount; - return x; - }; - - let align = (alignment) => { - const modulo = offset & (alignment-1); - if (modulo != 0) { - offset += alignment - modulo - } - }; - - let wmi = wasmMemoryInterface; - - if (!event_temp.data) { - return; - } - - let e = event_temp.data.event; - - wmi.storeU32(off(4), event_temp.data.name_code); - if (e.target == document) { - wmi.storeU32(off(4), 1); - } else if (e.target == window) { - wmi.storeU32(off(4), 2); - } else { - wmi.storeU32(off(4), 0); - } - if (e.currentTarget == document) { - wmi.storeU32(off(4), 1); - } else if (e.currentTarget == window) { - wmi.storeU32(off(4), 2); - } else { - wmi.storeU32(off(4), 0); - } - - align(W); - - wmi.storeI32(off(W), event_temp.data.id_ptr); - wmi.storeUint(off(W), event_temp.data.id_len); - - align(8); - wmi.storeF64(off(8), e.timeStamp*1e-3); - - wmi.storeU8(off(1), e.eventPhase); - let options = 0; - if (!!e.bubbles) { options |= 1<<0; } - if (!!e.cancelable) { options |= 1<<1; } - if (!!e.composed) { options |= 1<<2; } - wmi.storeU8(off(1), options); - wmi.storeU8(off(1), !!e.isComposing); - wmi.storeU8(off(1), !!e.isTrusted); - - align(8); - if (e instanceof WheelEvent) { - wmi.storeF64(off(8), e.deltaX); - wmi.storeF64(off(8), e.deltaY); - wmi.storeF64(off(8), e.deltaZ); - wmi.storeU32(off(4), e.deltaMode); - } else if (e instanceof MouseEvent) { - wmi.storeI64(off(8), e.screenX); - wmi.storeI64(off(8), e.screenY); - wmi.storeI64(off(8), e.clientX); - wmi.storeI64(off(8), e.clientY); - wmi.storeI64(off(8), e.offsetX); - wmi.storeI64(off(8), e.offsetY); - wmi.storeI64(off(8), e.pageX); - wmi.storeI64(off(8), e.pageY); - wmi.storeI64(off(8), e.movementX); - wmi.storeI64(off(8), e.movementY); - - wmi.storeU8(off(1), !!e.ctrlKey); - wmi.storeU8(off(1), !!e.shiftKey); - wmi.storeU8(off(1), !!e.altKey); - wmi.storeU8(off(1), !!e.metaKey); - - wmi.storeI16(off(2), e.button); - wmi.storeU16(off(2), e.buttons); - - if (e instanceof PointerEvent) { - wmi.storeF64(off(8), e.altitudeAngle); - wmi.storeF64(off(8), e.azimuthAngle); - wmi.storeInt(off(W), e.persistentDeviceId); - wmi.storeInt(off(W), e.pointerId); - wmi.storeInt(off(W), e.width); - wmi.storeInt(off(W), e.height); - wmi.storeF64(off(8), e.pressure); - wmi.storeF64(off(8), e.tangentialPressure); - wmi.storeF64(off(8), e.tiltX); - wmi.storeF64(off(8), e.tiltY); - wmi.storeF64(off(8), e.twist); - if (e.pointerType == "pen") { - wmi.storeU8(off(1), 1); - } else if (e.pointerType == "touch") { - wmi.storeU8(off(1), 2); - } else { - wmi.storeU8(off(1), 0); - } - wmi.storeU8(off(1), !!e.isPrimary); - } - - } else if (e instanceof KeyboardEvent) { - // Note: those strings are constructed - // on the native side from buffers that - // are filled later, so skip them - const keyPtr = off(W*2, W); - const codePtr = off(W*2, W); - - wmi.storeU8(off(1), e.location); - - wmi.storeU8(off(1), !!e.ctrlKey); - wmi.storeU8(off(1), !!e.shiftKey); - wmi.storeU8(off(1), !!e.altKey); - wmi.storeU8(off(1), !!e.metaKey); - - wmi.storeU8(off(1), !!e.repeat); - - wmi.storeI32(off(4), e.charCode); - - wmi.storeInt(off(W, W), e.key.length) - wmi.storeInt(off(W, W), e.code.length) - wmi.storeString(off(32, 1), e.key); - wmi.storeString(off(32, 1), e.code); - } else if (e.type === 'scroll') { - wmi.storeF64(off(8, 8), window.scrollX); - wmi.storeF64(off(8, 8), window.scrollY); - } else if (e.type === 'visibilitychange') { - wmi.storeU8(off(1), !document.hidden); - } else if (e instanceof GamepadEvent) { - const idPtr = off(W*2, W); - const mappingPtr = off(W*2, W); - - wmi.storeI32(off(W, W), e.gamepad.index); - wmi.storeU8(off(1), !!e.gamepad.connected); - wmi.storeF64(off(8, 8), e.gamepad.timestamp); - - wmi.storeInt(off(W, W), e.gamepad.buttons.length); - wmi.storeInt(off(W, W), e.gamepad.axes.length); - - for (let i = 0; i < 64; i++) { - if (i < e.gamepad.buttons.length) { - let b = e.gamepad.buttons[i]; - wmi.storeF64(off(8, 8), b.value); - wmi.storeU8(off(1), !!b.pressed); - wmi.storeU8(off(1), !!b.touched); - } else { - off(16, 8); - } - } - for (let i = 0; i < 16; i++) { - if (i < e.gamepad.axes.length) { - let a = e.gamepad.axes[i]; - wmi.storeF64(off(8, 8), a); - } else { - off(8, 8); - } - } - - let idLength = e.gamepad.id.length; - let id = e.gamepad.id; - if (idLength > 96) { - idLength = 96; - id = id.slice(0, 93) + '...'; - } - - let mappingLength = e.gamepad.mapping.length; - let mapping = e.gamepad.mapping; - if (mappingLength > 64) { - mappingLength = 61; - mapping = mapping.slice(0, 61) + '...'; - } - - wmi.storeInt(off(W, W), idLength); - wmi.storeInt(off(W, W), mappingLength); - wmi.storeString(off(96, 1), id); - wmi.storeString(off(64, 1), mapping); - } - }, - - add_event_listener: (id_ptr, id_len, name_ptr, name_len, name_code, data, callback, use_capture) => { - let id = wasmMemoryInterface.loadString(id_ptr, id_len); - let name = wasmMemoryInterface.loadString(name_ptr, name_len); - let element = getElement(id); - if (element == undefined) { - return false; - } - let key = listener_key(id, name, data, callback, !!use_capture); - if (wasmMemoryInterface.listenerMap.has(key)) { - return false; - } - - let listener = (e) => { - let event_data = {}; - event_data.id_ptr = id_ptr; - event_data.id_len = id_len; - event_data.event = e; - event_data.name_code = name_code; - - onEventReceived(event_data, data, callback); - }; - wasmMemoryInterface.listenerMap.set(key, listener); - element.addEventListener(name, listener, !!use_capture); - return true; - }, - - add_window_event_listener: (name_ptr, name_len, name_code, data, callback, use_capture) => { - let name = wasmMemoryInterface.loadString(name_ptr, name_len); - let element = window; - let key = listener_key('window', name, data, callback, !!use_capture); - if (wasmMemoryInterface.listenerMap.has(key)) { - return false; - } - - let listener = (e) => { - let event_data = {}; - event_data.id_ptr = 0; - event_data.id_len = 0; - event_data.event = e; - event_data.name_code = name_code; - - onEventReceived(event_data, data, callback); - }; - wasmMemoryInterface.listenerMap.set(key, listener); - element.addEventListener(name, listener, !!use_capture); - return true; - }, - - add_document_event_listener: (name_ptr, name_len, name_code, data, callback, use_capture) => { - let name = wasmMemoryInterface.loadString(name_ptr, name_len); - let element = document; - let key = listener_key('document', name, data, callback, !!use_capture); - if (wasmMemoryInterface.listenerMap.has(key)) { - return false; - } - - let listener = (e) => { - let event_data = {}; - event_data.id_ptr = 0; - event_data.id_len = 0; - event_data.event = e; - event_data.name_code = name_code; - - onEventReceived(event_data, data, callback); - }; - wasmMemoryInterface.listenerMap.set(key, listener); - element.addEventListener(name, listener, !!use_capture); - return true; - }, - - remove_event_listener: (id_ptr, id_len, name_ptr, name_len, data, callback, use_capture) => { - let id = wasmMemoryInterface.loadString(id_ptr, id_len); - let name = wasmMemoryInterface.loadString(name_ptr, name_len); - let element = getElement(id); - if (element == undefined) { - return false; - } - - let key = listener_key(id, name, data, callback, !!use_capture); - let listener = wasmMemoryInterface.listenerMap.get(key); - if (listener === undefined) { - return false; - } - wasmMemoryInterface.listenerMap.delete(key); - - element.removeEventListener(name, listener, !!use_capture); - return true; - }, - remove_window_event_listener: (name_ptr, name_len, data, callback, use_capture) => { - let name = wasmMemoryInterface.loadString(name_ptr, name_len); - let element = window; - - let key = listener_key('window', name, data, callback, !!use_capture); - let listener = wasmMemoryInterface.listenerMap.get(key); - if (listener === undefined) { - return false; - } - wasmMemoryInterface.listenerMap.delete(key); - - element.removeEventListener(name, listener, !!use_capture); - return true; - }, - remove_document_event_listener: (name_ptr, name_len, data, callback, use_capture) => { - let name = wasmMemoryInterface.loadString(name_ptr, name_len); - let element = document; - - let key = listener_key('document', name, data, callback, !!use_capture); - let listener = wasmMemoryInterface.listenerMap.get(key); - if (listener === undefined) { - return false; - } - wasmMemoryInterface.listenerMap.delete(key); - - element.removeEventListener(name, listener, !!use_capture); - return true; - }, - - event_stop_propagation: () => { - if (event_temp.data && event_temp.data.event) { - event_temp.data.event.stopPropagation(); - } - }, - event_stop_immediate_propagation: () => { - if (event_temp.data && event_temp.data.event) { - event_temp.data.event.stopImmediatePropagation(); - } - }, - event_prevent_default: () => { - if (event_temp.data && event_temp.data.event) { - event_temp.data.event.preventDefault(); - } - }, - - dispatch_custom_event: (id_ptr, id_len, name_ptr, name_len, options_bits) => { - let id = wasmMemoryInterface.loadString(id_ptr, id_len); - let name = wasmMemoryInterface.loadString(name_ptr, name_len); - let options = { - bubbles: (options_bits & (1<<0)) !== 0, - cancelable: (options_bits & (1<<1)) !== 0, - composed: (options_bits & (1<<2)) !== 0, - }; - - let element = getElement(id); - if (element) { - element.dispatchEvent(new Event(name, options)); - return true; - } - return false; - }, - - get_gamepad_state: (gamepad_id, ep) => { - let index = gamepad_id; - let gps = navigator.getGamepads(); - if (0 <= index && index < gps.length) { - let gamepad = gps[index]; - if (!gamepad) { - return false; - } - - const W = wasmMemoryInterface.intSize; - let offset = ep; - let off = (amount, alignment) => { - if (alignment === undefined) { - alignment = Math.min(amount, W); - } - if (offset % alignment != 0) { - offset += alignment - (offset%alignment); - } - let x = offset; - offset += amount; - return x; - }; - - let align = (alignment) => { - const modulo = offset & (alignment-1); - if (modulo != 0) { - offset += alignment - modulo - } - }; - - let wmi = wasmMemoryInterface; - - const idPtr = off(W*2, W); - const mappingPtr = off(W*2, W); - - wmi.storeI32(off(W), gamepad.index); - wmi.storeU8(off(1), !!gamepad.connected); - wmi.storeF64(off(8), gamepad.timestamp); - - wmi.storeInt(off(W), gamepad.buttons.length); - wmi.storeInt(off(W), gamepad.axes.length); - - for (let i = 0; i < 64; i++) { - if (i < gamepad.buttons.length) { - let b = gamepad.buttons[i]; - wmi.storeF64(off(8, 8), b.value); - wmi.storeU8(off(1), !!b.pressed); - wmi.storeU8(off(1), !!b.touched); - } else { - off(16, 8); - } - } - for (let i = 0; i < 16; i++) { - if (i < gamepad.axes.length) { - wmi.storeF64(off(8, 8), gamepad.axes[i]); - } else { - off(8, 8); - } - } - - let idLength = gamepad.id.length; - let id = gamepad.id; - if (idLength > 96) { - idLength = 96; - id = id.slice(0, 93) + '...'; - } - - let mappingLength = gamepad.mapping.length; - let mapping = gamepad.mapping; - if (mappingLength > 64) { - mappingLength = 61; - mapping = mapping.slice(0, 61) + '...'; - } - - wmi.storeInt(off(W, W), idLength); - wmi.storeInt(off(W, W), mappingLength); - wmi.storeString(off(96, 1), id); - wmi.storeString(off(64, 1), mapping); - - return true; - } - return false; - }, - - get_element_value_f64: (id_ptr, id_len) => { - let id = wasmMemoryInterface.loadString(id_ptr, id_len); - let element = getElement(id); - return element ? element.value : 0; - }, - get_element_value_string: (id_ptr, id_len, buf_ptr, buf_len) => { - let id = wasmMemoryInterface.loadString(id_ptr, id_len); - let element = getElement(id); - if (element) { - let str = element.value; - if (buf_len > 0 && buf_ptr) { - let n = Math.min(buf_len, str.length); - str = str.substring(0, n); - this.mem.loadBytes(buf_ptr, buf_len).set(new TextEncoder().encode(str)) - return n; - } - } - return 0; - }, - get_element_value_string_length: (id_ptr, id_len) => { - let id = wasmMemoryInterface.loadString(id_ptr, id_len); - let element = getElement(id); - if (element) { - return element.value.length; - } - return 0; - }, - get_element_min_max: (ptr_array2_f64, id_ptr, id_len) => { - let id = wasmMemoryInterface.loadString(id_ptr, id_len); - let element = getElement(id); - if (element) { - let values = wasmMemoryInterface.loadF64Array(ptr_array2_f64, 2); - values[0] = element.min; - values[1] = element.max; - } - }, - set_element_value_f64: (id_ptr, id_len, value) => { - let id = wasmMemoryInterface.loadString(id_ptr, id_len); - let element = getElement(id); - if (element) { - element.value = value; - } - }, - set_element_value_string: (id_ptr, id_len, value_ptr, value_len) => { - let id = wasmMemoryInterface.loadString(id_ptr, id_len); - let value = wasmMemoryInterface.loadString(value_ptr, value_len); - let element = getElement(id); - if (element) { - element.value = value; - } - }, - - set_element_style: (id_ptr, id_len, key_ptr, key_len, value_ptr, value_len) => { - let id = wasmMemoryInterface.loadString(id_ptr, id_len); - let key = wasmMemoryInterface.loadString(key_ptr, key_len); - let value = wasmMemoryInterface.loadString(value_ptr, value_len); - let element = getElement(id); - if (element) { - element.style[key] = value; - } - }, - - get_element_key_f64: (id_ptr, id_len, key_ptr, key_len) => { - let id = wasmMemoryInterface.loadString(id_ptr, id_len); - let key = wasmMemoryInterface.loadString(key_ptr, key_len); - let element = getElement(id); - return element ? element[key] : 0; - }, - get_element_key_string: (id_ptr, id_len, key_ptr, key_len, buf_ptr, buf_len) => { - let id = wasmMemoryInterface.loadString(id_ptr, id_len); - let key = wasmMemoryInterface.loadString(key_ptr, key_len); - let element = getElement(id); - if (element) { - let str = element[key]; - if (buf_len > 0 && buf_ptr) { - let n = Math.min(buf_len, str.length); - str = str.substring(0, n); - this.mem.loadBytes(buf_ptr, buf_len).set(new TextEncoder().encode(str)) - return n; - } - } - return 0; - }, - get_element_key_string_length: (id_ptr, id_len, key_ptr, key_len) => { - let id = wasmMemoryInterface.loadString(id_ptr, id_len); - let key = wasmMemoryInterface.loadString(key_ptr, key_len); - let element = getElement(id); - if (element && element[key]) { - return element[key].length; - } - return 0; - }, - - set_element_key_f64: (id_ptr, id_len, key_ptr, key_len, value) => { - let id = wasmMemoryInterface.loadString(id_ptr, id_len); - let key = wasmMemoryInterface.loadString(key_ptr, key_len); - let element = getElement(id); - if (element) { - element[key] = value; - } - }, - set_element_key_string: (id_ptr, id_len, key_ptr, key_len, value_ptr, value_len) => { - let id = wasmMemoryInterface.loadString(id_ptr, id_len); - let key = wasmMemoryInterface.loadString(key_ptr, key_len); - let value = wasmMemoryInterface.loadString(value_ptr, value_len); - let element = getElement(id); - if (element) { - element[key] = value; - } - }, - - - get_bounding_client_rect: (rect_ptr, id_ptr, id_len) => { - let id = wasmMemoryInterface.loadString(id_ptr, id_len); - let element = getElement(id); - if (element) { - let values = wasmMemoryInterface.loadF64Array(rect_ptr, 4); - let rect = element.getBoundingClientRect(); - values[0] = rect.left; - values[1] = rect.top; - values[2] = rect.right - rect.left; - values[3] = rect.bottom - rect.top; - } - }, - window_get_rect: (rect_ptr) => { - let values = wasmMemoryInterface.loadF64Array(rect_ptr, 4); - values[0] = window.screenX; - values[1] = window.screenY; - values[2] = window.screen.width; - values[3] = window.screen.height; - }, - - window_get_scroll: (pos_ptr) => { - let values = wasmMemoryInterface.loadF64Array(pos_ptr, 2); - values[0] = window.scrollX; - values[1] = window.scrollY; - }, - window_set_scroll: (x, y) => { - window.scroll(x, y); - }, - - device_pixel_ratio: () => { - return window.devicePixelRatio; - }, - - }, - - "webgl": webglContext.getWebGL1Interface(), - "webgl2": webglContext.getWebGL2Interface(), - }; -}; - -/** - * @param {string} wasmPath - Path to the WASM module to run - * @param {?HTMLPreElement} consoleElement - Optional console/pre element to append output to, in addition to the console - * @param {any} extraForeignImports - Imports, in addition to the default runtime to provide the module - * @param {?WasmMemoryInterface} wasmMemoryInterface - Optional memory to use instead of the defaults - * @param {?int} intSize - Size (in bytes) of the integer type, should be 4 on `js_wasm32` and 8 on `js_wasm64p32` - */ -async function runWasm(wasmPath, consoleElement, extraForeignImports, wasmMemoryInterface, intSize = 4) { - if (!wasmMemoryInterface) { - wasmMemoryInterface = new WasmMemoryInterface(); - } - wasmMemoryInterface.setIntSize(intSize); - - let imports = odinSetupDefaultImports(wasmMemoryInterface, consoleElement, wasmMemoryInterface.memory); - let exports = {}; - - if (extraForeignImports !== undefined) { - imports = { - ...imports, - ...extraForeignImports, - }; - } - - const response = await fetch(wasmPath); - const file = await response.arrayBuffer(); - const wasm = await WebAssembly.instantiate(file, imports); - exports = wasm.instance.exports; - wasmMemoryInterface.setExports(exports); - - if (exports.memory) { - if (wasmMemoryInterface.memory) { - console.warn("WASM module exports memory, but `runWasm` was given an interface with existing memory too"); - } - wasmMemoryInterface.setMemory(exports.memory); - } - - exports._start(); - - // Define a `@export step :: proc(delta_time: f64) -> (keep_going: bool) {` - // in your app and it will get called every frame. - // return `false` to stop the execution of the module. - if (exports.step) { - const odin_ctx = exports.default_context_ptr(); - - let prevTimeStamp = undefined; - function step(currTimeStamp) { - if (prevTimeStamp == undefined) { - prevTimeStamp = currTimeStamp; - } - - const dt = (currTimeStamp - prevTimeStamp)*0.001; - prevTimeStamp = currTimeStamp; - - if (!exports.step(dt, odin_ctx)) { - exports._end(); - return; - } - - window.requestAnimationFrame(step); - } - - window.requestAnimationFrame(step); - } else { - exports._end(); - } - - return; -}; - -window.odin = { - // Interface Types - WasmMemoryInterface: WasmMemoryInterface, - WebGLInterface: WebGLInterface, - - // Functions - setupDefaultImports: odinSetupDefaultImports, - runWasm: runWasm, -}; -})(); diff --git a/wgpu/sdl3/game-of-life/web/wgpu.js b/wgpu/sdl3/game-of-life/web/wgpu.js deleted file mode 100644 index 6104417..0000000 --- a/wgpu/sdl3/game-of-life/web/wgpu.js +++ /dev/null @@ -1,3311 +0,0 @@ -(function() { - -const STATUS_SUCCESS = 1; -const STATUS_ERROR = 2; - -const ENUMS = { - FeatureName: [undefined, "depth-clip-control", "depth32float-stencil8", "timestamp-query", "texture-compression-bc", "texture-compression-bc-sliced-3d", "texture-compression-etc2", "texture-compression-astc", "texture-compression-astc-sliced-3d", "indirect-first-instance", "shader-f16", "rg11b10ufloat-renderable", "bgra8unorm-storage", "float32-filterable", "float32-blendable", "clip-distances", "dual-source-blending" ], - StoreOp: [undefined, "store", "discard", ], - LoadOp: [undefined, "load", "clear", ], - BufferBindingType: [null, undefined, "uniform", "storage", "read-only-storage", ], - SamplerBindingType: [null, undefined, "filtering", "non-filtering", "comparison", ], - TextureSampleType: [null, undefined, "float", "unfilterable-float", "depth", "sint", "uint", ], - TextureViewDimension: [undefined, "1d", "2d", "2d-array", "cube", "cube-array", "3d", ], - StorageTextureAccess: [null, undefined, "write-only", "read-only", "read-write", ], - TextureFormat: [undefined, "r8unorm", "r8snorm", "r8uint", "r8sint", "r16uint", "r16sint", "r16float", "rg8unorm", "rg8snorm", "rg8uint", "rg8sint", "r32float", "r32uint", "r32sint", "rg16uint", "rg16sint", "rg16float", "rgba8unorm", "rgba8unorm-srgb", "rgba8snorm", "rgba8uint", "rgba8sint", "bgra8unorm", "bgra8unorm-srgb", "rgb10a2uint", "rgb10a2unorm", "rg11b10ufloat", "rgb9e5ufloat", "rg32float", "rg32uint", "rg32sint", "rgba16uint", "rgba16sint", "rgba16float", "rgba32float", "rgba32uint", "rgba32sint", "stencil8", "depth16unorm", "depth24plus", "depth24plus-stencil8", "depth32float", "depth32float-stencil8", "bc1-rgba-unorm", "bc1-rgba-unorm-srgb", "bc2-rgba-unorm", "bc2-rgba-unorm-srgb", "bc3-rgba-unorm", "bc3-rgba-unorm-srgb", "bc4-r-unorm", "bc4-r-snorm", "bc5-rg-unorm", "bc5-rg-snorm", "bc6h-rgb-ufloat", "bc6h-rgb-float", "bc7-rgba-unorm", "bc7-rgba-unorm-srgb", "etc2-rgb8unorm", "etc2-rgb8unorm-srgb", "etc2-rgb8a1unorm", "etc2-rgb8a1unorm-srgb", "etc2-rgba8unorm", "etc2-rgba8unorm-srgb", "eac-r11unorm", "eac-r11snorm", "eac-rg11unorm", "eac-rg11snorm", "astc-4x4-unorm", "astc-4x4-unorm-srgb", "astc-5x4-unorm", "astc-5x4-unorm-srgb", "astc-5x5-unorm", "astc-5x5-unorm-srgb", "astc-6x5-unorm", "astc-6x5-unorm-srgb", "astc-6x6-unorm", "astc-6x6-unorm-srgb", "astc-8x5-unorm", "astc-8x5-unorm-srgb", "astc-8x6-unorm", "astc-8x6-unorm-srgb", "astc-8x8-unorm", "astc-8x8-unorm-srgb", "astc-10x5-unorm", "astc-10x5-unorm-srgb", "astc-10x6-unorm", "astc-10x6-unorm-srgb", "astc-10x8-unorm", "astc-10x8-unorm-srgb", "astc-10x10-unorm", "astc-10x10-unorm-srgb", "astc-12x10-unorm", "astc-12x10-unorm-srgb", "astc-12x12-unorm", "astc-12x12-unorm-srgb", ], - QueryType: [undefined, "occlusion", "timestamp", ], - VertexStepMode: [null, undefined, "vertex", "instance", ], - VertexFormat: [undefined, "uint8", "uint8x2", "uint8x4", "sint8", "sint8x2", "sint8x4", "unorm8", "unorm8x2", "unorm8x4", "snorm8", "snorm8x2", "snorm8x4", "uint16", "uint16x2", "uint16x4", "sint16", "sint16x2", "sint16x4", "unorm16", "unorm16x2", "unorm16x4", "snorm16", "snorm16x2", "snorm16x4", "float16", "float16x2", "float16x4", "float32", "float32x2", "float32x3", "float32x4", "uint32", "uint32x2", "uint32x3", "uint32x4", "sint32", "sint32x2", "sint32x3", "sint32x4", "unorm10-10-2", "unorm8x4-bgra" ], - PrimitiveTopology: [undefined, "point-list", "line-list", "line-strip", "triangle-list", "triangle-strip", ], - IndexFormat: [undefined, "uint16", "uint32", ], - FrontFace: [undefined, "ccw", "cw", ], - CullMode: [undefined, "none", "front", "back", ], - AddressMode: [undefined, "clamp-to-edge", "repeat", "mirror-repeat", ], - FilterMode: [undefined, "nearest", "linear", ], - MipmapFilterMode: [undefined, "nearest", "linear", ], - CompareFunction: [undefined, "never", "less", "equal", "less-equal", "greater", "not-equal", "greater-equal", "always", ], - TextureDimension: [undefined, "1d", "2d", "3d", ], - ErrorType: [undefined, "no-error", "validation", "out-of-memory", "internal", "unknown", ], - WGSLLanguageFeatureName: [undefined, "readonly_and_readwrite_storage_textures", "packed_4x8_integer_dot_product", "unrestricted_pointer_parameters", "pointer_composite_access", ], - PowerPreference: [undefined, "low-power", "high-performance", ], - CompositeAlphaMode: ["auto", "opaque", "premultiplied", "unpremultiplied", "inherit", ], - StencilOperation: [undefined, "keep", "zero", "replace", "invert", "increment-clamp", "decrement-clamp", "increment-wrap", "decrement-wrap", ], - BlendOperation: ["add", "subtract", "reverse-subtract", "min", "max", ], - BlendFactor: [undefined, "zero", "one", "src", "one-minus-src", "src-alpha", "one-minus-src-alpha", "dst", "one-minus-dst", "dst-alpha", "one-minus-dst-alpha", "src-alpha-saturated", "constant", "one-minus-constant", "src1", "one-minus-src1", "src1-alpha", "one-minus-src1-alpha" ], - PresentMode: [undefined, "fifo", "fifo-relaxed", "immediate", "mailbox", ], - TextureAspect: [undefined, "all", "stencil-only", "depth-only"], - DeviceLostReason: [undefined, "unknown", "destroyed", "instance-dropped", "failed-creation"], - BufferMapState: [undefined, "unmapped", "pending", "mapped"], - OptionalBool: [false, true, undefined], - - // WARN: used with indexOf to pass to WASM, if we would pass to JS, this needs to use official naming convention (not like Odin enums) like the ones above. - BackendType: [undefined, null, "WebGPU", "D3D11", "D3D12", "Metal", "Vulkan", "OpenGL", "OpenGLES"], - AdapterType: [undefined, "DiscreteGPU", "IntegratedGPU", "CPU", "Unknown"], - RequestDeviceStatus: [undefined, "Success", "InstanceDropped", "Error", "Unknown"], - MapAsyncStatus: [undefined, "Success", "InstanceDropped", "Error", "Aborted", "Unknown"], - CreatePipelineAsyncStatus: [undefined, "Success", "InstanceDropped", "ValidationError", "InternalError", "Unknown"], - PopErrorScopeStatus: [undefined, "Success", "InstanceDropped", "EmptyStack"], - RequestAdapterStatus: [undefined, "Success", "InstanceDropped", "Unavailable", "Error", "Unknown"], - QueueWorkDoneStatus: [undefined, "Success", "InstanceDropped", "Error", "Unknown"], - CompilationInfoRequestStatus: [undefined, "Success", "InstanceDropped", "Error", "Unknown"], -}; - -/** - * Assumptions: - * - Ability to allocate memory, set the context to allocate with using the global `wgpu.g_context` - * - Exports a function table (for callbacks), added with `-extra-linker-flags:"--export-table"` - */ -class WebGPUInterface { - - /** - * @param {WasmMemoryInterface} mem - */ - constructor(mem) { - this.mem = mem; - - this.sizes = { - Color: [32, 8], - BufferBindingLayout: [24, 8], - SamplerBindingLayout: [8, 4], - TextureBindingLayout: [16, 4], - StorageTextureBindingLayout: [16, 4], - StringView: [2*this.mem.intSize, this.mem.intSize], - ConstantEntry: [this.mem.intSize === 8 ? 32 : 24, 8], - ProgrammableStageDescriptor: [8 + this.mem.intSize*4, this.mem.intSize], - VertexBufferLayout: [16 + this.mem.intSize*2, 8], - VertexAttribute: [24, 8], - VertexState: [8 + this.mem.intSize*6, this.mem.intSize], - PrimitiveState: [24, 4], - MultisampleState: [16, 4], - StencilFaceState: [16, 4], - ColorTargetState: [24, 8], - BlendComponent: [12, 4], - TexelCopyBufferLayout: [16, 8], - Origin3D: [12, 4], - QueueDescriptor: [this.mem.intSize*3, this.mem.intSize], - CallbackInfo: [20, 4], - UncapturedErrorCallbackInfo: [16, 4], - RenderPassColorAttachment: [56, 8], - BindGroupEntry: [40, 8], - BindGroupLayoutEntry: [80, 8], - Extent3D: [12, 4], - CompilationMessage: [this.mem.intSize == 8 ? 64 : 48, 8], - }; - - /** @type {WebGPUObjectManager<{}>} */ - this.instances = new WebGPUObjectManager("Instance", this.mem); - - /** @type {WebGPUObjectManager} */ - this.adapters = new WebGPUObjectManager("Adapter", this.mem); - - /** @type {WebGPUObjectManager} */ - this.bindGroups = new WebGPUObjectManager("BindGroup", this.mem); - - /** @type {WebGPUObjectManager} */ - this.bindGroupLayouts = new WebGPUObjectManager("BindGroupLayout", this.mem); - - /** @type {WebGPUObjectManager<{ buffer: GPUBuffer, mapping: ?{ range: ArrayBuffer, ptr: number, size: number } }>} */ - this.buffers = new WebGPUObjectManager("Buffer", this.mem); - - /** @type {WebGPUObjectManager} */ - this.devices = new WebGPUObjectManager("Device", this.mem); - - /** @type {WebGPUObjectManager} */ - this.commandBuffers = new WebGPUObjectManager("CommandBuffer", this.mem); - - /** @type {WebGPUObjectManager} */ - this.commandEncoders = new WebGPUObjectManager("CommandEncoder", this.mem); - - /** @type {WebGPUObjectManager} */ - this.computePassEncoders = new WebGPUObjectManager("ComputePassEncoder", this.mem); - - /** @type {WebGPUObjectManager} */ - this.renderPassEncoders = new WebGPUObjectManager("RenderPassEncoder", this.mem); - - /** @type {WebGPUObjectManager} */ - this.querySets = new WebGPUObjectManager("QuerySet", this.mem); - - /** @type {WebGPUObjectManager} */ - this.computePipelines = new WebGPUObjectManager("ComputePipeline", this.mem); - - /** @type {WebGPUObjectManager} */ - this.pipelineLayouts = new WebGPUObjectManager("PipelineLayout", this.mem); - - /** @type {WebGPUObjectManager} */ - this.queues = new WebGPUObjectManager("Queue", this.mem); - - /** @type {WebGPUObjectManager} */ - this.renderBundles = new WebGPUObjectManager("RenderBundle", this.mem); - - /** @type {WebGPUObjectManager} */ - this.renderBundleEncoders = new WebGPUObjectManager("RenderBundleEncoder", this.mem); - - /** @type {WebGPUObjectManager} */ - this.renderPipelines = new WebGPUObjectManager("RenderPipeline", this.mem); - - /** @type {WebGPUObjectManager} */ - this.samplers = new WebGPUObjectManager("Sampler", this.mem); - - /** @type {WebGPUObjectManager} */ - this.shaderModules = new WebGPUObjectManager("ShaderModule", this.mem); - - /** @type {WebGPUObjectManager} */ - this.surfaces = new WebGPUObjectManager("Surface", this.mem); - - /** @type {WebGPUObjectManager} */ - this.textures = new WebGPUObjectManager("Texture", this.mem); - - /** @type {WebGPUObjectManager} */ - this.textureViews = new WebGPUObjectManager("TextureView", this.mem); - - this.zeroMessageAddr = 0; - } - - struct(start) { - let offset = start; - - return (size, alignment = null) => { - if (alignment === null) { - if (Array.isArray(size)) { - [size, alignment] = size; - } else { - alignment = size; - } - } - - // Align the offset to the required boundary - offset = Math.ceil(offset / alignment) * alignment; - let currentOffset = offset; - offset += size; - - return currentOffset; - }; - } - - /** - * @param {number|BigInt} src - * @returns {number|BigInt} - */ - uint(src) { - if (this.mem.intSize == 8) { - return BigInt(src); - } else if (this.mem.intSize == 4) { - return src; - } else { - throw new Error("unreachable"); - } - } - - /** - * @param {number|BigInt} src - * @returns {number} - */ - unwrapBigInt(src) { - if (typeof src == "number") { - return src; - } - - const MAX_SAFE_INTEGER = 9007199254740991n; - if (typeof src != "bigint") { - throw new TypeError(`unwrapBigInt got invalid param of type ${typeof src}`); - } - - if (src > MAX_SAFE_INTEGER) { - throw new Error(`unwrapBigInt precision would be lost converting ${src}`); - } - - return Number(src); - } - - /** - * @param {boolean} condition - * @param {string} message - */ - assert(condition, message = "assertion failure") { - if (!condition) { - throw new Error(message); - } - } - - /** - * @template T - * - * @param {number} count - * @param {number} start - * @param {function(number): T} decoder - * @param {number} stride - * @returns {Array} - */ - array(count, start, decoder, stride) { - if (count == 0) { - return []; - } - this.assert(start != 0); - - const out = []; - for (let i = 0; i < count; i += 1) { - out.push(decoder.call(this, start)); - start += stride; - } - return out; - } - - /** - * @param {string} name - * @param {number} ptr - * @returns {`GPU${name}`} - */ - enumeration(name, ptr) { - const int = this.mem.loadI32(ptr); - return ENUMS[name][int]; - } - - /** - * @param {GPUSupportedFeatures} features - * @param {number} ptr - */ - genericGetFeatures(features, ptr) { - this.assert(ptr != 0); - - const availableFeatures = []; - ENUMS.FeatureName.forEach((feature, value) => { - if (!feature) { - return; - } - - if (features.has(feature)) { - availableFeatures.push(value); - } - }); - - if (availableFeatures.length === 0) { - return; - } - - const featuresAddr = this.mem.exports.wgpu_alloc(availableFeatures.length * 4); - this.assert(featuresAddr != 0); - - let off = this.struct(ptr); - this.mem.storeUint(off(this.mem.intSize), availableFeatures.length); - this.mem.storeI32(off(4), featuresAddr); - - off = this.struct(featuresAddr); - for (let i = 0; i < availableFeatures.length; i += 1) { - this.mem.storeI32(off(4), availableFeatures[i]); - } - } - - /** - * @param {GPUSupportedLimits} limits - * @param {number} ptr - * @returns {number} - */ - genericGetLimits(limits, supportedLimitsPtr) { - this.assert(supportedLimitsPtr != 0); - - const off = this.struct(supportedLimitsPtr); - off(4); - - this.mem.storeU32(off(4), limits.maxTextureDimension1D); - this.mem.storeU32(off(4), limits.maxTextureDimension2D); - this.mem.storeU32(off(4), limits.maxTextureDimension3D); - this.mem.storeU32(off(4), limits.maxTextureArrayLayers); - this.mem.storeU32(off(4), limits.maxBindGroups); - this.mem.storeU32(off(4), limits.maxBindGroupsPlusVertexBuffers); - this.mem.storeU32(off(4), limits.maxBindingsPerBindGroup); - this.mem.storeU32(off(4), limits.maxDynamicUniformBuffersPerPipelineLayout); - this.mem.storeU32(off(4), limits.maxDynamicStorageBuffersPerPipelineLayout); - this.mem.storeU32(off(4), limits.maxSampledTexturesPerShaderStage); - this.mem.storeU32(off(4), limits.maxSamplersPerShaderStage); - this.mem.storeU32(off(4), limits.maxStorageBuffersPerShaderStage); - this.mem.storeU32(off(4), limits.maxStorageTexturesPerShaderStage); - this.mem.storeU32(off(4), limits.maxUniformBuffersPerShaderStage); - this.mem.storeU64(off(8), limits.maxUniformBufferBindingSize); - this.mem.storeU64(off(8), limits.maxStorageBufferBindingSize); - this.mem.storeU32(off(4), limits.minUniformBufferOffsetAlignment); - this.mem.storeU32(off(4), limits.minStorageBufferOffsetAlignment); - this.mem.storeU32(off(4), limits.maxVertexBuffers); - this.mem.storeU64(off(8), limits.maxBufferSize); - this.mem.storeU32(off(4), limits.maxVertexAttributes); - this.mem.storeU32(off(4), limits.maxVertexBufferArrayStride); - this.mem.storeU32(off(4), limits.maxInterStageShaderVariables); - this.mem.storeU32(off(4), limits.maxColorAttachments); - this.mem.storeU32(off(4), limits.maxColorAttachmentBytesPerSample); - this.mem.storeU32(off(4), limits.maxComputeWorkgroupStorageSize); - this.mem.storeU32(off(4), limits.maxComputeInvocationsPerWorkgroup); - this.mem.storeU32(off(4), limits.maxComputeWorkgroupSizeX); - this.mem.storeU32(off(4), limits.maxComputeWorkgroupSizeY); - this.mem.storeU32(off(4), limits.maxComputeWorkgroupSizeZ); - this.mem.storeU32(off(4), limits.maxComputeWorkgroupsPerDimension); - - return STATUS_SUCCESS; - } - - genericGetAdapterInfo(infoPtr) { - this.assert(infoPtr != 0); - - const off = this.struct(infoPtr); - off(4); // nextInChain - off(this.sizes.StringView); // vendor - off(this.sizes.StringView); // architecture - off(this.sizes.StringView); // device - off(this.sizes.StringView); // description - - this.mem.storeI32(off(4), ENUMS.BackendType.indexOf("WebGPU")); - this.mem.storeI32(off(4), ENUMS.AdapterType.indexOf("Unknown")); - - // NOTE: I don't think getting the other fields in this struct is possible. - // `adapter.requestAdapterInfo` is deprecated. - - return STATUS_SUCCESS; - } - - /** - * @param {number} ptr - * @returns {GPUFeatureName} - */ - FeatureNamePtr(ptr) { - return this.FeatureName(this.mem.loadI32(ptr)); - } - - /** - * @param {number} featureInt - * @returns {GPUFeatureName} - */ - FeatureName(featureInt) { - return ENUMS.FeatureName[featureInt]; - } - - /** - * @param {number} ptr - * @returns {GPUSupportedLimits} - */ - RequiredLimitsPtr(ptr) { - const start = this.mem.loadPtr(ptr); - if (start == 0) { - return undefined; - } - - return this.Limits(start + 8); - } - - /** - * @param {number} start - * @return {GPUSupportedLimits} - */ - Limits(start) { - const limitU32 = (ptr) => { - const value = this.mem.loadU32(ptr); - if (value == 0xFFFFFFFF) { // LIMIT_32_UNDEFINED. - return undefined; - } - return value; - }; - - const limitU64 = (ptr) => { - const part1 = this.mem.loadU32(ptr); - const part2 = this.mem.loadU32(ptr + 4); - if (part1 != 0xFFFFFFFF || part2 != 0xFFFFFFFF) { // LIMIT_64_UNDEFINED. - return this.mem.loadU64(ptr); - } - return undefined; - }; - - const off = this.struct(start); - off(4); - - return { - maxTextureDimension1D: limitU32(off(4)), - maxTextureDimension2D: limitU32(off(4)), - maxTextureDimension3D: limitU32(off(4)), - maxTextureArrayLayers: limitU32(off(4)), - maxBindGroups: limitU32(off(4)), - maxBindGroupsPlusVertexBuffers: limitU32(off(4)), - maxBindingsPerBindGroup: limitU32(off(4)), - maxDynamicUniformBuffersPerPipelineLayout: limitU32(off(4)), - maxDynamicStorageBuffersPerPipelineLayout: limitU32(off(4)), - maxSampledTexturesPerShaderStage: limitU32(off(4)), - maxSamplersPerShaderStage: limitU32(off(4)), - maxStorageBuffersPerShaderStage: limitU32(off(4)), - maxStorageTexturesPerShaderStage: limitU32(off(4)), - maxUniformBuffersPerShaderStage: limitU32(off(4)), - maxUniformBufferBindingSize: limitU64(off(8)), - maxStorageBufferBindingSize: limitU64(off(8)), - minUniformBufferOffsetAlignment: limitU32(off(4)), - minStorageBufferOffsetAlignment: limitU32(off(4)), - maxVertexBuffers: limitU32(off(4)), - maxBufferSize: limitU64(off(8)), - maxVertexAttributes: limitU32(off(4)), - maxVertexBufferArrayStride: limitU32(off(4)), - maxInterStageShaderVariables: limitU32(off(4)), - maxColorAttachments: limitU32(off(4)), - maxColorAttachmentBytesPerSample: limitU32(off(4)), - maxComputeWorkgroupStorageSize: limitU32(off(4)), - maxComputeInvocationsPerWorkgroup: limitU32(off(4)), - maxComputeWorkgroupSizeX: limitU32(off(4)), - maxComputeWorkgroupSizeY: limitU32(off(4)), - maxComputeWorkgroupSizeZ: limitU32(off(4)), - maxComputeWorkgroupsPerDimension: limitU32(off(4)), - }; - } - - /** - * @param {number} start - * @returns {GPUQueueDescriptor} - */ - QueueDescriptor(start) { - return { - label: this.StringView(start + 4), - }; - } - - /** - * @param {number} ptr - * @returns {GPUComputePassTimestampWrites} - */ - ComputePassTimestampWritesPtr(ptr) { - const start = this.mem.loadPtr(ptr); - if (start == 0) { - return undefined; - } - - const off = this.struct(start); - return { - querySet: this.querySets.get(this.mem.loadPtr(off(4))), - beginningOfPassWriteIndex: this.mem.loadU32(off(4)), - endOfPassWriteIndex: this.mem.loadU32(off(4)), - }; - } - - /** - * @param {number} start - * @returns {GPURenderPassColorAttachment} - */ - RenderPassColorAttachment(start) { - const off = this.struct(start); - off(4); - - const viewIdx = this.mem.loadPtr(off(4)); - - let depthSlice = this.mem.loadU32(off(4)); - if (depthSlice == 0xFFFFFFFF) { // DEPTH_SLICE_UNDEFINED. - depthSlice = undefined; - } - - const resolveTargetIdx = this.mem.loadPtr(off(4)); - - return { - view: viewIdx > 0 ? this.textureViews.get(viewIdx) : undefined, - resolveTarget: resolveTargetIdx > 0 ? this.textureViews.get(resolveTargetIdx) : undefined, - depthSlice: depthSlice, - loadOp: this.enumeration("LoadOp", off(4)), - storeOp: this.enumeration("StoreOp", off(4)), - clearValue: this.Color(off(this.sizes.Color)), - }; - } - - /** - * @param {number} start - * @returns {GPUColor} - */ - Color(start) { - const off = this.struct(start); - return { - r: this.mem.loadF64(off(8)), - g: this.mem.loadF64(off(8)), - b: this.mem.loadF64(off(8)), - a: this.mem.loadF64(off(8)), - }; - } - - /** - * @param {number} ptr - * @returns {GPURenderPassDepthStencilAttachment} - */ - RenderPassDepthStencilAttachmentPtr(ptr) { - const start = this.mem.loadPtr(ptr); - if (start == 0) { - return undefined; - } - - const off = this.struct(start); - - return { - view: this.textureViews.get(this.mem.loadPtr(off(4))), - depthLoadOp: this.enumeration("LoadOp", off(4)), - depthStoreOp: this.enumeration("StoreOp", off(4)), - depthClearValue: this.mem.loadF32(off(4)), - depthReadOnly: this.mem.loadB32(off(4)), - stencilLoadOp: this.enumeration("LoadOp", off(4)), - stencilStoreOp: this.enumeration("StoreOp", off(4)), - stencilClearValue: this.mem.loadF32(off(4)), - stencilReadOnly: this.mem.loadB32(off(4)), - }; - } - - /** - * @param {number} ptr - * @returns {undefined|GPUQuerySet} - */ - QuerySet(ptr) { - ptr = this.mem.loadPtr(ptr); - if (ptr == 0) { - return undefined; - } - - return this.querySets.get(ptr); - } - - /** - * @param {number} ptr - * @returns {GPURenderPassTimestampWrites} - */ - RenderPassTimestampWritesPtr(ptr) { - return this.ComputePassTimestampWritesPtr(ptr); - } - - /** - * @param {number} start - * @returns {GPUOrigin3D} - */ - Origin3D(start) { - return { - x: this.mem.loadU32(start + 0), - y: this.mem.loadU32(start + 4), - z: this.mem.loadU32(start + 8), - }; - } - - /** - * @param {number} start - * @returns {GPUExtent3D} - */ - Extent3D(start) { - return { - width: this.mem.loadU32(start + 0), - height: this.mem.loadU32(start + 4), - depthOrArrayLayers: this.mem.loadU32(start + 8), - }; - } - - /** - * @param {number} start - * @returns {GPUBindGroupEntry} - */ - BindGroupEntry(start) { - const buffer = this.mem.loadPtr(start + 8); - const sampler = this.mem.loadPtr(start + 32); - const textureView = this.mem.loadPtr(start + 36); - - /** @type {GPUBindingResource} */ - let resource; - if (buffer > 0) { - resource = { - buffer: this.buffers.get(buffer).buffer, - offset: this.mem.loadU64(start + 16), - size: this.mem.loadU64(start + 24), - } - } else if (sampler > 0) { - resource = this.samplers.get(sampler); - } else if (textureView > 0) { - resource = this.textureViews.get(textureView); - } - - return { - binding: this.mem.loadU32(start + 4), - resource: resource, - }; - } - - /** - * @param {number} start - * @returns {GPUBindGroupLayoutEntry} - */ - BindGroupLayoutEntry(start) { - const off = this.struct(start); - off(4); - - const entry = { - binding: this.mem.loadU32(off(4)), - visibility: this.mem.loadU64(off(8)), - buffer: this.BufferBindingLayout(off(this.sizes.BufferBindingLayout)), - sampler: this.SamplerBindingLayout(off(this.sizes.SamplerBindingLayout)), - texture: this.TextureBindingLayout(off(this.sizes.TextureBindingLayout)), - storageTexture: this.StorageTextureBindingLayout(off(this.sizes.StorageTextureBindingLayout)), - }; - if (!entry.buffer.type) { - entry.buffer = undefined; - } - if (!entry.sampler.type) { - entry.sampler = undefined; - } - if (!entry.texture.sampleType) { - entry.texture = undefined; - } - if (!entry.storageTexture.access) { - entry.storageTexture = undefined; - } - return entry; - } - - /** - * @param {number} start - * @returns {GPUBufferBindingLayout} - */ - BufferBindingLayout(start) { - return { - type: this.enumeration("BufferBindingType", start + 4), - hasDynamicOffset: this.mem.loadB32(start + 8), - minBindingSize: this.mem.loadU64(start + 16), - }; - } - - /** - * @param {number} start - * @returns {GPUSamplerBindingLayout} - */ - SamplerBindingLayout(start) { - return { - type: this.enumeration("SamplerBindingType", start + 4), - }; - } - - /** - * @param {number} start - * @returns {GPUTextureBindingLayout} - */ - TextureBindingLayout(start) { - return { - sampleType: this.enumeration("TextureSampleType", start + 4), - viewDimension: this.enumeration("TextureViewDimension", start + 8), - multisampled: this.mem.loadB32(start + 12), - }; - } - - /** - * @param {number} start - * @returns {GPUStorageTextureBindingLayout} - */ - StorageTextureBindingLayout(start) { - return { - access: this.enumeration("StorageTextureAccess", start + 4), - format: this.enumeration("TextureFormat", start + 8), - viewDimension: this.enumeration("TextureViewDimension", start + 12), - }; - } - - /** - * @param {number} start - * @returns {GPUProgrammableStage} - */ - ProgrammableStageDescriptor(start) { - const off = this.struct(start); - off(4); - - const shaderModule = this.shaderModules.get(this.mem.loadPtr(off(4))); - const entryPoint = this.StringView(off(this.sizes.StringView)); - - const constantsArray = this.array( - this.mem.loadUint(off(this.mem.intSize)), - this.mem.loadPtr(off(4)), - this.ConstantEntry, - this.sizes.ConstantEntry[0], - ); - - return { - module: shaderModule, - entryPoint: entryPoint, - constants: constantsArray.reduce((prev, curr) => { - prev[curr.key] = curr.value; - return prev; - }, {}), - }; - } - - /** - * @param {number} start - * @returns {{ key: string, value: number }} - */ - ConstantEntry(start) { - const off = this.struct(start); - off(4); - - return { - key: this.StringView(off(this.sizes.StringView)), - value: this.mem.loadF64(off(8)), - }; - } - - /** - * @param {number} start - * @returns {GPUComputePipelineDescriptor} - */ - ComputePipelineDescriptor(start) { - const off = this.struct(start); - off(4); - - const label = this.StringView(off(this.sizes.StringView)); - const layoutIdx = this.mem.loadPtr(off(4)); - return { - label: label, - layout: layoutIdx > 0 ? this.pipelineLayouts.get(layoutIdx) : "auto", - compute: this.ProgrammableStageDescriptor(off(this.sizes.ProgrammableStageDescriptor)), - }; - } - - /** - * @param {number} start - * @returns {GPUVertexState} - */ - VertexState(start) { - const off = this.struct(start); - off(4); - - const shaderModuleIdx = this.mem.loadPtr(off(4)); - const entryPoint = this.StringView(off(this.sizes.StringView)); - - const constantsArray = this.array( - this.mem.loadUint(off(this.mem.intSize)), - this.mem.loadPtr(off(4)), - this.ConstantEntry, - this.sizes.ConstantEntry[0], - ); - - return { - module: this.shaderModules.get(shaderModuleIdx), - entryPoint: entryPoint, - constants: constantsArray.reduce((prev, curr) => { - prev[curr.key] = curr.value; - return prev; - }, {}), - buffers: this.array( - this.mem.loadUint(off(this.mem.intSize)), - this.mem.loadPtr(off(4)), - this.VertexBufferLayout, - this.sizes.VertexBufferLayout[0], - ), - }; - } - - /** - * @param {number} start - * @returns {?GPUVertexBufferLayout} - */ - VertexBufferLayout(start) { - const off = this.struct(start); - - const stepMode = this.enumeration("VertexStepMode", off(4)); - if (stepMode == null) { - return null; - } - - return { - arrayStride: this.mem.loadU64(off(8)), - stepMode: stepMode, - attributes: this.array( - this.mem.loadUint(off(this.mem.intSize)), - this.mem.loadPtr(off(4)), - this.VertexAttribute, - this.sizes.VertexAttribute[0], - ), - }; - } - - /** - * @param {number} start - * @returns {GPUVertexAttribute} - */ - VertexAttribute(start) { - const off = this.struct(start); - return { - format: this.enumeration("VertexFormat", off(4)), - offset: this.mem.loadU64(off(8)), - shaderLocation: this.mem.loadU32(off(4)), - }; - } - - /** - * @param {number} start - * @returns {GPUPrimitiveState} - */ - PrimitiveState(start) { - const off = this.struct(start); - off(4); - - return { - topology: this.enumeration("PrimitiveTopology", off(4)), - stripIndexFormat: this.enumeration("IndexFormat", off(4)), - frontFace: this.enumeration("FrontFace", off(4)), - cullMode: this.enumeration("CullMode", off(4)), - unclippedDepth: this.mem.loadB32(off(4)), - }; - } - - /** - * @param {number} start - * @returns {GPURenderPipelineDescriptor} - */ - RenderPipelineDescriptor(start) { - const off = this.struct(start); - off(4); - - const label = this.StringView(off(this.sizes.StringView)); - const layoutIdx = this.mem.loadPtr(off(4)); - return { - label: label, - layout: layoutIdx > 0 ? this.pipelineLayouts.get(layoutIdx) : "auto", - vertex: this.VertexState(off(this.sizes.VertexState)), - primitive: this.PrimitiveState(off(this.sizes.PrimitiveState)), - depthStencil: this.DepthStencilStatePtr(off(4)), - multisample: this.MultisampleState(off(this.sizes.MultisampleState)), - fragment: this.FragmentStatePtr(off(4)), - }; - } - - /** - * @param {number} ptr - * @returns {?GPUDepthStencilState} - */ - DepthStencilStatePtr(ptr) { - const start = this.mem.loadPtr(ptr); - if (start == 0) { - return undefined; - } - - const off = this.struct(start); - off(4); - - return { - format: this.enumeration("TextureFormat", off(4)), - depthWriteEnabled: this.enumeration("OptionalBool", off(4)), - depthCompare: this.enumeration("CompareFunction", off(4)), - stencilFront: this.StencilFaceState(off(this.sizes.StencilFaceState)), - stencilBack: this.StencilFaceState(off(this.sizes.StencilFaceState)), - stencilReadMask: this.mem.loadU32(off(4)), - stencilWriteMask: this.mem.loadU32(off(4)), - depthBias: this.mem.loadI32(off(4)), - depthBiasSlopeScale: this.mem.loadF32(off(4)), - depthBiasClamp: this.mem.loadF32(off(4)), - }; - } - - /** - * @param {number} start - * @returns {GPUStencilFaceState} - */ - StencilFaceState(start) { - return { - compare: this.enumeration("CompareFunction", start + 0), - failOp: this.enumeration("StencilOperation", start + 4), - depthFailOp: this.enumeration("StencilOperation", start + 8), - passOp: this.enumeration("StencilOperation", start + 12), - }; - } - - /** - * @param {number} start - * @returns {GPUMultisampleState} - */ - MultisampleState(start) { - return { - count: this.mem.loadU32(start + 4), - mask: this.mem.loadU32(start + 8), - alphaToCoverageEnabled: this.mem.loadB32(start + 12), - }; - } - - /** - * @param {number} ptr - * @returns {?GPUFragmentState} - */ - FragmentStatePtr(ptr) { - const start = this.mem.loadPtr(ptr); - if (start == 0) { - return undefined; - } - - const off = this.struct(start); - off(4); - - const shaderModule = this.shaderModules.get(this.mem.loadPtr(off(4))); - const entryPoint = this.StringView(off(this.sizes.StringView)); - - const constantsArray = this.array( - this.mem.loadUint(off(this.mem.intSize)), - this.mem.loadPtr(off(4)), - this.ConstantEntry, - this.sizes.ConstantEntry[0], - ); - - return { - module: shaderModule, - entryPoint: entryPoint, - constants: constantsArray.reduce((prev, curr) => { - prev[curr.key] = curr.value; - return prev; - }, {}), - targets: this.array( - this.mem.loadUint(off(this.mem.intSize)), - this.mem.loadPtr(off(4)), - this.ColorTargetState, - this.sizes.ColorTargetState[0], - ), - }; - } - - /** - * @param {number} start - * @returns {GPUColorTargetState} - */ - ColorTargetState(start) { - const off = this.struct(start); - off(4); - return { - format: this.enumeration("TextureFormat", off(4)), - blend: this.BlendStatePtr(off(4)), - writeMask: this.mem.loadU64(off(8)), - }; - } - - /** - * @param {number} ptr - * @returns {?GPUBlendState} - */ - BlendStatePtr(ptr) { - const start = this.mem.loadPtr(ptr); - if (start == 0) { - return undefined; - } - - const off = this.struct(start); - - return { - color: this.BlendComponent(off(this.sizes.BlendComponent)), - alpha: this.BlendComponent(off(this.sizes.BlendComponent)), - }; - } - - /** - * @param {number} start - * @returns {?GPUBlendComponent} - */ - BlendComponent(start) { - return { - operation: this.enumeration("BlendOperation", start + 0), - srcFactor: this.enumeration("BlendFactor", start + 4), - dstFactor: this.enumeration("BlendFactor", start + 8), - }; - } - - TexelCopyBufferInfo(start) { - const off = this.struct(start); - const layout = this.TexelCopyBufferLayout(off(this.sizes.TexelCopyBufferLayout)); - const bufferIdx = this.mem.loadPtr(off(4)); - return { - buffer: this.buffers.get(bufferIdx).buffer, - offset: layout.offset, - bytesPerRow: layout.bytesPerRow, - rowsPerImage: layout.rowsPerImage, - }; - } - - TexelCopyBufferLayout(start) { - const off = this.struct(start); - return { - offset: this.mem.loadU64(off(8)), - bytesPerRow: this.mem.loadU32(off(4)), - rowsPerImage: this.mem.loadU32(off(4)), - }; - } - - TexelCopyTextureInfo(start) { - const off = this.struct(start); - return { - texture: this.textures.get(this.mem.loadPtr(off(4))), - mipLevel: this.mem.loadU32(off(4)), - origin: this.Origin3D(off(this.sizes.Origin3D)), - aspect: this.enumeration("TextureAspect", off(4)), - }; - } - - StringView(start) { - const data = this.mem.loadPtr(start); - return this.mem.loadString(data, this.mem.loadUint(start + this.mem.intSize)); - } - - CallbackInfoPtr(ptr) { - const start = this.mem.loadPtr(ptr); - if (start === 0) { - return null; - } - - return CallbackInfo(start); - } - - CallbackInfo(start) { - const off = this.struct(start); - off(4); - // TODO: callback mode? - off(4); - return { - callback: this.mem.exports.__indirect_function_table.get(this.mem.loadPtr(off(4))), - userdata1: this.mem.loadPtr(off(4)), - userdata2: this.mem.loadPtr(off(4)), - }; - } - - UncapturedErrorCallbackInfo(start) { - const off = this.struct(start); - off(4); - return { - callback: this.mem.exports.__indirect_function_table.get(this.mem.loadPtr(off(4))), - userdata1: this.mem.loadPtr(off(4)), - userdata2: this.mem.loadPtr(off(4)), - }; - } - - callCallback(callback, args) { - args.push(callback.userdata1); - args.push(callback.userdata2); - callback.callback(...args); - } - - zeroMessageArg() { - if (this.zeroMessageAddr > 0) { - return this.zeroMessageAddr; - } - - this.zeroMessageAddr = this.mem.exports.wgpu_alloc(this.sizes.StringView[0]); - return this.zeroMessageAddr; - } - - makeMessageArg(message) { - if (message.length == 0) { - return this.zeroMessageArg(); - } - - const messageLength = new TextEncoder().encode(message).length; - const stringSize = this.sizes.StringView[0]; - - const addr = this.mem.exports.wgpu_alloc(stringSize + messageLength); - - const messageAddr = addr + stringSize; - - this.mem.storeI32(addr, messageAddr); - this.mem.storeUint(addr + this.mem.intSize, messageLength); - - this.mem.storeString(messageAddr, message); - - return addr; - } - - getInterface() { - return { - /** - * @param {0|number} descriptorPtr - * @returns {number} - */ - wgpuCreateInstance: (descriptorPtr) => { - if (!navigator.gpu) { - console.error("WebGPU is not supported by this browser"); - return 0; - } - - // TODO: instance capabilities for futures? - - return this.instances.create({}); - }, - - /** - * @param {number} capabilitiesPtr - * @returns {number} - */ - wgpuGetInstanceCapabilities: (capabilitiesPtr) => { - // TODO: implement (futures). - return STATUS_ERROR; - }, - - /** - * @param {number} procNamePtr - * @param {number} procNameLen - * @returns {number} - */ - wgpuGetProcAddress: (procNamePtr, procNameLen) => { - console.error(`unimplemented: wgpuGetProcAddress`); - return 0; - }, - - /* ---------------------- Adapter ---------------------- */ - - /** - * @param {number} adapterIdx - * @param {number} featuresPtr - */ - wgpuAdapterGetFeatures: (adapterIdx, featuresPtr) => { - const adapter = this.adapters.get(adapterIdx); - this.genericGetFeatures(adapter.features, featuresPtr); - }, - - /** - * @param {number} adapterIdx - * @param {number} infoPtr - * @returns {number} - */ - wgpuAdapterGetInfo: (adapterIdx, infoPtr) => { - return this.genericGetAdapterInfo(infoPtr); - }, - - /** - * @param {number} adapterIdx - * @param {number} limitsPtr - * @returns {number} - */ - wgpuAdapterGetLimits: (adapterIdx, limitsPtr) => { - const adapter = this.adapters.get(adapterIdx); - return this.genericGetLimits(adapter.limits, limitsPtr); - }, - - /** - * @param {number} adapterIdx - * @param {number} featureInt - * @returns {boolean} - */ - wgpuAdapterHasFeature: (adapterIdx, featureInt) => { - const adapter = this.adapters.get(adapterIdx); - return adapter.features.has(ENUMS.FeatureName[featureInt]); - }, - - /** - * @param {number} adapterIdx - * @param {0|number} descriptorPtr - * @param {number} callbackInfoPtr - * @return {number} - */ - wgpuAdapterRequestDevice: (adapterIdx, descriptorPtr, callbackInfoPtr) => { - const adapter = this.adapters.get(adapterIdx); - - const off = this.struct(descriptorPtr); - off(4); - - /** @type {GPUDeviceDescriptor} */ - let descriptor; - if (descriptorPtr != 0) { - descriptor = { - label: this.StringView(off(this.sizes.StringView)), - requiredFeatures: this.array( - this.mem.loadUint(off(this.mem.intSize)), - this.mem.loadPtr(off(4)), - this.FeatureNamePtr, - 4, - ), - requiredLimits: this.RequiredLimitsPtr(off(4)), - defaultQueue: this.QueueDescriptor(off(this.sizes.QueueDescriptor)), - }; - } - - const callbackInfo = this.CallbackInfo(callbackInfoPtr); - - const deviceLostCallbackInfo = this.CallbackInfo(off(this.sizes.CallbackInfo)); - const uncapturedErrorCallbackInfo = this.UncapturedErrorCallbackInfo(off(this.sizes.UncapturedErrorCallbackInfo)); - - adapter.requestDevice(descriptor) - .catch((e) => { - const messageAddr = this.makeMessageArg(e.message); - this.callCallback(callbackInfo, [ENUMS.RequestDeviceStatus.indexOf("Error"), messageAddr]); - this.mem.exports.wgpu_free(messageAddr); - }) - .then((device) => { - const deviceIdx = this.devices.create(device); - - if (deviceLostCallbackInfo.callback !== null) { - device.lost.then((info) => { - const reason = ENUMS.DeviceLostReason.indexOf(info.reason); - - const devicePtr = this.mem.exports.wgpu_alloc(4); - this.mem.storeI32(devicePtr, deviceIdx); - - const messageAddr = this.makeMessageArg(info.message); - this.callCallback(deviceLostCallbackInfo, [devicePtr, reason, messageAddr]); - - this.mem.exports.wgpu_free(devicePtr); - this.mem.exports.wgpu_free(messageAddr); - }); - } - - if (uncapturedErrorCallbackInfo.callback !== null) { - device.onuncapturederror = (ev) => { - let status; - if (ev.error instanceof GPUValidationError) { - status = ENUMS.ErrorType.indexOf("validation"); - } else if (ev.error instanceof GPUOutOfMemoryError) { - status = ENUMS.ErrorType.indexOf("out-of-memory"); - } else if (ev.error instanceof GPUInternalError) { - status = ENUMS.ErrorType.indexOf("internal"); - } else { - status = ENUMS.ErrorType.indexOf("unknown"); - } - - const messageAddr = this.makeMessageArg(ev.error.message); - this.callCallback(uncapturedErrorCallbackInfo, [deviceIdx, status, messageAddr]); - this.mem.exports.wgpu_free(messageAddr); - }; - } - - this.callCallback(callbackInfo, [ENUMS.ErrorType.indexOf("no-error"), deviceIdx, this.zeroMessageArg()]); - }); - - // TODO: returning a future? WARN that requires refactor removing await - return BigInt(0); - }, - - ...this.adapters.interface(), - - /** - * @param {number} infoPtr - */ - wgpuAdapterInfoFreeMembers: (infoPtr) => { - // NOTE: nothing to free. - }, - - /* ---------------------- BindGroup ---------------------- */ - - ...this.bindGroups.interface(true), - - /* ---------------------- BindGroupLayout ---------------------- */ - - ...this.bindGroupLayouts.interface(true), - - /* ---------------------- Buffer ---------------------- */ - - /** @param {number} bufferIdx */ - wgpuBufferDestroy: (bufferIdx) => { - const buffer = this.buffers.get(bufferIdx); - buffer.buffer.destroy(); - }, - - /** - * @param {number} bufferIdx - * @param {number|BigInt} offset - * @param {number|BigInt} size - * @returns {number} - */ - wgpuBufferGetConstMappedRange: (bufferIdx, offset, size) => { - const buffer = this.buffers.get(bufferIdx); - offset = this.unwrapBigInt(offset); - size = this.unwrapBigInt(size); - - // TODO: does constMappedRange need to do something else? - - this.assert(!buffer.mapping, "buffer already mapped"); - - const range = buffer.buffer.getMappedRange(offset, size); - - const ptr = this.mem.exports.wgpu_alloc(range.byteLength); - - const mapping = new Uint8Array(this.mem.memory.buffer, ptr, size); - mapping.set(new Uint8Array(range)); - - buffer.mapping = { range: range, ptr: ptr, size: range.byteLength }; - return ptr; - }, - - /** - * @param {number} bufferIdx - * @return {number} - */ - wgpuBufferGetMapState: (bufferIdx) => { - const buffer = this.buffers.get(bufferIdx); - return ENUMS.BufferMapState.indexOf(buffer.mapState); - }, - - /** - * @param {number} bufferIdx - * @param {number|BigInt} offset - * @param {number|BigInt} size - * @returns {number} - */ - wgpuBufferGetMappedRange: (bufferIdx, offset, size) => { - const buffer = this.buffers.get(bufferIdx); - offset = this.unwrapBigInt(offset); - size = this.unwrapBigInt(size); - - this.assert(!buffer.mapping, "buffer already mapped"); - - const range = buffer.buffer.getMappedRange(offset, size); - - const ptr = this.mem.exports.wgpu_alloc(range.byteLength); - - const mapping = new Uint8Array(this.mem.memory.buffer, ptr, size); - mapping.set(new Uint8Array(range)); - - buffer.mapping = { range: range, ptr: ptr, size: range.byteLength }; - return ptr; - }, - - /** - * @param {number} bufferIdx - * @returns {BigInt} - */ - wgpuBufferGetSize: (bufferIdx) => { - const buffer = this.buffers.get(bufferIdx); - return BigInt(buffer.buffer.size); - }, - - /** - * @param {number} bufferIdx - * @returns {number} - */ - wgpuBufferGetUsage: (bufferIdx) => { - const buffer = this.buffers.get(bufferIdx); - return buffer.buffer.usage; - }, - - /** - * @param {number} bufferIdx - * @param {number} mode - * @param {number|BigInt} offset - * @param {number|BigInt} size - * @param {number} callbackInfo - * @return {number} - */ - wgpuBufferMapAsync: (bufferIdx, mode, offset, size, callbackInfoPtr) => { - const buffer = this.buffers.get(bufferIdx); - mode = this.unwrapBigInt(mode); - offset = this.unwrapBigInt(offset); - size = this.unwrapBigInt(size); - - const callbackInfo = this.CallbackInfo(callbackInfoPtr); - buffer.buffer.mapAsync(mode, offset, size) - .catch((e) => { - const messageAddr = this.makeMessageArg(e.message); - this.callCallback(callbackInfo, [ENUMS.MapAsyncStatus.indexOf("Error"), messageAddr]); - this.mem.exports.wgpu_free(messageAddr); - }) - .then(() => { - this.callCallback(callbackInfo, [ENUMS.MapAsyncStatus.indexOf("Success"), this.zeroMessageArg()]); - }); - - // TODO: returning a future? WARN that requires refactor removing await - return BigInt(0); - }, - - /** - * @param {number} bufferIdx - * @param {number} labelPtr - * @param {number} labelLen - */ - wgpuBufferSetLabel: (bufferIdx, labelPtr, labelLen) => { - const buffer = this.buffers.get(bufferIdx); - buffer.buffer.label = this.mem.loadString(labelPtr, labelLen); - }, - - /** - * @param {number} bufferIdx - */ - wgpuBufferUnmap: (bufferIdx) => { - const buffer = this.buffers.get(bufferIdx); - this.assert(buffer.mapping, "buffer not mapped"); - - const mapping = new Uint8Array(this.mem.memory.buffer, buffer.mapping.ptr, buffer.mapping.size); - (new Uint8Array(buffer.mapping.range)).set(mapping); - - buffer.buffer.unmap(); - - this.mem.exports.wgpu_free(buffer.mapping.ptr); - buffer.mapping = null; - }, - - ...this.buffers.interface(), - - /* ---------------------- CommandBuffer ---------------------- */ - - ...this.commandBuffers.interface(true), - - /* ---------------------- CommandEncoder ---------------------- */ - - /** - * @param {number} commandEncoderIdx - * @param {0|number} descriptorPtr - * @return {number} The compute pass encoder - */ - wgpuCommandEncoderBeginComputePass: (commandEncoderIdx, descriptorPtr) => { - const commandEncoder = this.commandEncoders.get(commandEncoderIdx); - - /** @type {?GPUComputePassDescriptor} */ - let descriptor; - if (descriptorPtr != 0) { - const off = this.struct(descriptorPtr); - off(4); - descriptor = { - label: this.StringView(off(this.sizes.StringView)), - timestampWrites: this.ComputePassTimestampWritesPtr(off(4)), - }; - } - - const computePassEncoder = commandEncoder.beginComputePass(descriptor); - return this.computePassEncoders.create(computePassEncoder); - }, - - /** - * @param {number} commandEncoderIdx - * @param {number} descriptorPtr - * @return {number} The render pass encoder - */ - wgpuCommandEncoderBeginRenderPass: (commandEncoderIdx, descriptorPtr) => { - const commandEncoder = this.commandEncoders.get(commandEncoderIdx); - this.assert(descriptorPtr != 0); - - const off = this.struct(descriptorPtr); - - let maxDrawCount = undefined; - const nextInChain = this.mem.loadPtr(off(4)); - if (nextInChain != 0) { - const nextInChainType = this.mem.loadI32(nextInChain + 4); - // RenderPassMaxDrawCount = 0x00000003, - if (nextInChainType == 0x00000003) { - maxDrawCount = this.mem.loadU64(nextInChain + 8); - } - } - - /** @type {GPURenderPassDescriptor} */ - const descriptor = { - label: this.StringView(off(this.sizes.StringView)), - colorAttachments: this.array( - this.mem.loadUint(off(this.mem.intSize)), - this.mem.loadPtr(off(4)), - this.RenderPassColorAttachment, - this.sizes.RenderPassColorAttachment[0], - ), - depthStencilAttachment: this.RenderPassDepthStencilAttachmentPtr(off(4)), - occlusionQuerySet: this.QuerySet(off(4)), - timestampWrites: this.RenderPassTimestampWritesPtr(off(4)), - maxDrawCount: maxDrawCount, - }; - - const renderPassEncoder = commandEncoder.beginRenderPass(descriptor); - return this.renderPassEncoders.create(renderPassEncoder); - }, - - /** - * @param {number} commandEncoderIdx - * @param {number} bufferIdx - * @param {BigInt} offset - * @param {BigInt} size - */ - wgpuCommandEncoderClearBuffer: (commandEncoderIdx, bufferIdx, offset, size) => { - const commandEncoder = this.commandEncoders.get(commandEncoderIdx); - const buffer = this.buffers.get(bufferIdx); - offset = this.unwrapBigInt(offset); - size = this.unwrapBigInt(size); - commandEncoder.clearBuffer(buffer.buffer, offset, size); - }, - - /** - * @param {number} commandEncoderIdx - * @param {number} sourceIdx - * @param {BigInt} sourceOffset - * @param {number} destinationIdx - * @param {BigInt} destinationOffset - * @param {BigInt} size - */ - wgpuCommandEncoderCopyBufferToBuffer: (commandEncoderIdx, sourceIdx, sourceOffset, destinationIdx, destinationOffset, size) => { - const commandEncoder = this.commandEncoders.get(commandEncoderIdx); - const source = this.buffers.get(sourceIdx); - const destination = this.buffers.get(destinationIdx); - sourceOffset = this.unwrapBigInt(sourceOffset); - destinationOffset = this.unwrapBigInt(destinationOffset); - size = this.unwrapBigInt(size); - commandEncoder.copyBufferToBuffer(source.buffer, sourceOffset, destination.buffer, destinationOffset, size); - }, - - /** - * @param {number} commandEncoderIdx - * @param {number} sourcePtr - * @param {number} destinationPtr - * @param {number} copySizePtr - */ - wgpuCommandEncoderCopyBufferToTexture: (commandEncoderIdx, sourcePtr, destinationPtr, copySizePtr) => { - const commandEncoder = this.commandEncoders.get(commandEncoderIdx); - commandEncoder.copyBufferToTexture( - this.TexelCopyBufferInfo(sourcePtr), - this.TexelCopyTextureInfo(destinationPtr), - this.Extent3D(copySizePtr), - ); - }, - - /** - * @param {number} commandEncoderIdx - * @param {number} sourcePtr - * @param {number} destinationPtr - * @param {number} copySizePtr - */ - wgpuCommandEncoderCopyTextureToBuffer: (commandEncoderIdx, sourcePtr, destinationPtr, copySizePtr) => { - const commandEncoder = this.commandEncoders.get(commandEncoderIdx); - commandEncoder.copyTextureToBuffer( - this.TexelCopyTextureInfo(sourcePtr), - this.TexelCopyBufferInfo(destinationPtr), - this.Extent3D(copySizePtr), - ); - }, - - /** - * @param {number} commandEncoderIdx - * @param {number} sourcePtr - * @param {number} destinationPtr - * @param {number} copySizePtr - */ - wgpuCommandEncoderCopyTextureToTexture: (commandEncoderIdx, sourcePtr, destinationPtr, copySizePtr) => { - const commandEncoder = this.commandEncoders.get(commandEncoderIdx); - commandEncoder.copyTextureToTexture( - this.TexelCopyTextureInfo(sourcePtr), - this.TexelCopyTextureInfo(destinationPtr), - this.Extent3D(copySizePtr), - ); - }, - - /** - * @param {number} commandEncoderIdx - * @param {0|number} descriptorPtr - * @returns {number} The command buffer. - */ - wgpuCommandEncoderFinish: (commandEncoderIdx, descriptorPtr) => { - const commandEncoder = this.commandEncoders.get(commandEncoderIdx); - - /** @type {undefined|GPUCommandBufferDescriptor} */ - let descriptor; - if (descriptorPtr != 0) { - descriptor = { - label: this.StringView(descriptorPtr + 4), - }; - } - - const commandBuffer = commandEncoder.finish(descriptor); - return this.commandBuffers.create(commandBuffer); - }, - - /** - * @param {number} commandEncoderIdx - * @param {number} markerLabelPtr - * @param {number} markerLabelLen - */ - wgpuCommandEncoderInsertDebugMarker: (commandEncoderIdx, markerLabelPtr, markerLabelLen) => { - const commandEncoder = this.commandEncoders.get(commandEncoderIdx); - commandEncoder.insertDebugMarker(this.mem.loadString(markerLabelPtr, markerLabelLen)); - }, - - /** - * @param {number} commandEncoderIdx - */ - wgpuCommandEncoderPopDebugGroup: (commandEncoderIdx) => { - const commandEncoder = this.commandEncoders.get(commandEncoderIdx); - commandEncoder.popDebugGroup(); - }, - - /** - * @param {number} commandEncoderIdx - * @param {number} groupLabelPtr - * @param {number} groupLabelLen - */ - wgpuCommandEncoderPushDebugGroup: (commandEncoderIdx, groupLabelPtr, groupLabelLen) => { - const commandEncoder = this.commandEncoders.get(commandEncoderIdx); - commandEncoder.pushDebugGroup(this.mem.loadString(groupLabelPtr, groupLabelLen)); - }, - - /** - * @param {number} commandEncoderIdx - * @param {number} querySetIdx - * @param {number} firstQuery - * @param {number} queryCount - * @param {number} destinationIdx - * @param {BigInt} destinationOffset - */ - wgpuCommandEncoderResolveQuerySet: (commandEncoderIdx, querySetIdx, firstQuery, queryCount, destinationIdx, destinationOffset) => { - const commandEncoder = this.commandEncoders.get(commandEncoderIdx); - const querySet = this.querySets.get(querySetIdx); - const destination = this.buffers.get(destinationIdx); - destinationOffset = this.unwrapBigInt(destinationOffset); - commandEncoder.resolveQuerySet(querySet, firstQuery, queryCount, destination.buffer, destinationOffset); - }, - - /** - * @param {number} commandEncoderIdx - * @param {number} querySetIdx - * @param {number} queryIndex - */ - wgpuCommandEncoderWriteTimestamp: (commandEncoderIdx, querySetIdx, queryIndex) => { - const commandEncoder = this.commandEncoders.get(commandEncoderIdx); - const querySet = this.querySets.get(querySetIdx); - commandEncoder.writeTimestamp(querySet, queryIndex); - }, - - ...this.commandEncoders.interface(true), - - /* ---------------------- ComputePassEncoder ---------------------- */ - - - /** - * @param {number} computePassEncoderIdx - * @param {number} workgroupCountX - * @param {number} workgroupCountY - * @param {number} workgroupCountZ - */ - wgpuComputePassEncoderDispatchWorkgroups: (computePassEncoderIdx, workgroupCountX, workgroupCountY, workgroupCountZ) => { - const computePassEncoder = this.computePassEncoders.get(computePassEncoderIdx); - computePassEncoder.dispatchWorkgroups(workgroupCountX, workgroupCountY, workgroupCountZ); - }, - - /** - * @param {number} computePassEncoderIdx - * @param {number} indirectBufferIdx - * @param {BigInt} indirectOffset - */ - wgpuComputePassEncoderDispatchWorkgroupsIndirect: (computePassEncoderIdx, indirectBufferIdx, indirectOffset) => { - const computePassEncoder = this.computePassEncoders.get(computePassEncoderIdx); - const indirectBuffer = this.buffers.get(indirectBufferIdx); - indirectOffset = this.unwrapBigInt(indirectOffset); - computePassEncoder.dispatchWorkgroupsIndirect(indirectBuffer.buffer, indirectOffset); - }, - - /** - * @param {number} computePassEncoderIdx - */ - wgpuComputePassEncoderEnd: (computePassEncoderIdx) => { - const computePassEncoder = this.computePassEncoders.get(computePassEncoderIdx); - computePassEncoder.end(); - }, - - /** - * @param {number} computePassEncoderIdx - * @param {number} markerLabelPtr - * @param {number} markerLabelLen - */ - wgpuComputePassEncoderInsertDebugMarker: (computePassEncoderIdx, markerLabelPtr, markerLabelLen) => { - const computePassEncoder = this.computePassEncoders.get(computePassEncoderIdx); - computePassEncoder.insertDebugMarker(this.mem.loadString(markerLabelPtr, markerLabelLen)); - }, - - /** - * @param {number} computePassEncoderIdx - */ - wgpuComputePassEncoderPopDebugGroup: (computePassEncoderIdx) => { - const computePassEncoder = this.computePassEncoders.get(computePassEncoderIdx); - computePassEncoder.popDebugGroup(); - }, - - /** - * @param {number} computePassEncoderIdx - * @param {number} groupLabelPtr - * @param {number} groupLabelLen - */ - wgpuComputePassEncoderPushDebugGroup: (computePassEncoderIdx, groupLabelPtr, groupLabelLen) => { - const computePassEncoder = this.computePassEncoders.get(computePassEncoderIdx); - computePassEncoder.pushDebugGroup(this.mem.loadString(groupLabelPtr, groupLabelLen)); - }, - - /** - * @param {number} computePassEncoderIdx - * @param {number} groupIndex - * @param {0|number} groupIdx - * @param {number|BigInt} dynamicOffsetCount - * @param {number} dynamicOffsetsPtr - */ - wgpuComputePassEncoderSetBindGroup: (computePassEncoderIdx, groupIndex, groupIdx, dynamicOffsetCount, dynamicOffsetsPtr) => { - const computePassEncoder = this.computePassEncoders.get(computePassEncoderIdx); - dynamicOffsetCount = this.unwrapBigInt(dynamicOffsetCount); - - let bindGroup; - if (groupIdx != 0) { - bindGroup = this.bindGroups.get(groupIdx); - } - - const dynamicOffsets = []; - for (let i = 0; i < dynamicOffsetCount; i += 1) { - dynamicOffsets.push(this.mem.loadU32(dynamicOffsetsPtr)); - dynamicOffsetsPtr += 4; - } - - computePassEncoder.setBindGroup(groupIndex, bindGroup, dynamicOffsets); - }, - - /** - * @param {number} computePassEncoderIdx - * @param {number} pipelineIdx - */ - wgpuComputePassEncoderSetPipeline: (computePassEncoderIdx, pipelineIdx) => { - const computePassEncoder = this.computePassEncoders.get(computePassEncoderIdx); - const pipeline = this.computePipelines.get(pipelineIdx); - computePassEncoder.setPipeline(pipeline); - }, - - ...this.computePassEncoders.interface(true), - - /* ---------------------- ComputePipeline ---------------------- */ - - /** - * @param {number} computePipelineIdx - * @param {number} groupIndex - * @returns {number} - */ - wgpuComputePipelineGetBindGroupLayout: (computePipelineIdx, groupIndex) => { - const computePipeline = this.computePipelines.get(computePipelineIdx); - const bindGroupLayout = computePipeline.getBindGroupLayout(groupIndex); - return this.bindGroupLayouts.create(bindGroupLayout); - }, - - ...this.computePipelines.interface(true), - - /* ---------------------- Device ---------------------- */ - - /** - * @param {number} deviceIdx - * @param {number} descriptorPtr - * @returns {number} The bind group. - */ - wgpuDeviceCreateBindGroup: (deviceIdx, descriptorPtr) => { - const device = this.devices.get(deviceIdx); - this.assert(descriptorPtr != 0); - - const off = this.struct(descriptorPtr); - off(4); - - /** @type {GPUBindGroupDescriptor} */ - const descriptor = { - label: this.StringView(off(this.sizes.StringView)), - layout: this.bindGroupLayouts.get(this.mem.loadPtr(off(4))), - entries: this.array( - this.mem.loadUint(off(this.mem.intSize)), - this.mem.loadPtr(off(4)), - this.BindGroupEntry, - this.sizes.BindGroupEntry[0], - ), - }; - - const bindGroup = device.createBindGroup(descriptor); - return this.bindGroups.create(bindGroup); - }, - - /** - * @param {number} deviceIdx - * @param {number} descriptorPtr - * @returns {number} The bind group layout. - */ - wgpuDeviceCreateBindGroupLayout: (deviceIdx, descriptorPtr) => { - const device = this.devices.get(deviceIdx); - this.assert(descriptorPtr != 0); - - const off = this.struct(descriptorPtr); - off(4); - - /** @type {GPUBindGroupLayoutDescriptor} */ - const descriptor = { - label: this.StringView(off(this.sizes.StringView)), - entries: this.array( - this.mem.loadUint(off(this.mem.intSize)), - this.mem.loadPtr(off(4)), - this.BindGroupLayoutEntry, - this.sizes.BindGroupLayoutEntry[0], - ), - }; - - const bindGroupLayout = device.createBindGroupLayout(descriptor); - return this.bindGroupLayouts.create(bindGroupLayout); - }, - - /** - * @param {number} deviceIdx - * @param {number} descriptorPtr - * @returns {number} The buffer. - */ - wgpuDeviceCreateBuffer: (deviceIdx, descriptorPtr) => { - const device = this.devices.get(deviceIdx); - this.assert(descriptorPtr != 0); - - const off = this.struct(descriptorPtr); - off(4); - - /** @type {GPUBufferDescriptor} */ - const descriptor = { - label: this.StringView(off(this.sizes.StringView)), - usage: this.mem.loadU64(off(8)), - size: this.mem.loadU64(off(8)), - mappedAtCreation: this.mem.loadB32(off(4)), - }; - - const buffer = device.createBuffer(descriptor); - return this.buffers.create({buffer: buffer, mapping: null}); - }, - - /** - * @param {number} deviceIdx - * @param {0|number} descriptorPtr - * @returns {number} The command encoder. - */ - wgpuDeviceCreateCommandEncoder: (deviceIdx, descriptorPtr) => { - const device = this.devices.get(deviceIdx); - - /** @type {GPUCommandEncoderDescriptor} */ - let descriptor; - if (descriptor != 0) { - descriptor = { - label: this.StringView(descriptorPtr + 4), - }; - } - - const commandEncoder = device.createCommandEncoder(descriptor); - return this.commandEncoders.create(commandEncoder); - }, - - /** - * @param {number} deviceIdx - * @param {number} descriptorPtr - * @returns {number} The compute pipeline. - */ - wgpuDeviceCreateComputePipeline: (deviceIdx, descriptorPtr) => { - const device = this.devices.get(deviceIdx); - this.assert(descriptorPtr != 0); - const computePipeline = device.createComputePipeline(this.ComputePipelineDescriptor(descriptorPtr)); - return this.computePipelines.create(computePipeline); - }, - - /** - * @param {number} deviceIdx - * @param {number} descriptorPtr - * @param {number} callbackInfo - */ - wgpuDeviceCreateComputePipelineAsync: (deviceIdx, descriptorPtr, callbackInfoPtr) => { - const device = this.devices.get(deviceIdx); - - this.assert(descriptorPtr != 0); - - const callbackInfo = this.CallbackInfo(callbackInfoPtr); - device.createComputePipelineAsync(this.ComputePipelineDescriptor(descriptorPtr)) - .catch((e) => { - const messageAddr = this.makeMessageArg(e.message); - this.callCallback(callbackInfo, [ENUMS.CreatePipelineAsyncStatus.indexOf("Unknown"), 0, messageAddr]); - this.mem.exports.wgpu_free(messageAddr); - }) - .then((computePipeline) => { - const pipelineIdx = this.computePipelines.create(computePipeline); - this.callCallback(callbackInfo, [ENUMS.CreatePipelineAsyncStatus.indexOf("Success"), pipelineIdx, this.zeroMessageArg()]); - }); - - // TODO: returning futures? - return BigInt(0); - }, - - /** - * @param {number} deviceIdx - * @param {number} descriptorPtr - * @returns {number} The pipeline layout. - */ - wgpuDeviceCreatePipelineLayout: (deviceIdx, descriptorPtr) => { - const device = this.devices.get(deviceIdx); - this.assert(descriptorPtr != 0); - - const off = this.struct(descriptorPtr); - off(4); - - /** @type {GPUPipelineLayoutDescriptor} */ - const descriptor = { - label: this.StringView(off(this.sizes.StringView)), - bindGroupLayouts: this.array( - this.mem.loadUint(off(this.mem.intSize)), - this.mem.loadPtr(off(4)), - (ptr) => this.bindGroupLayouts.get(this.mem.loadPtr(ptr)), - 4, - ), - }; - - const pipelineLayout = device.createPipelineLayout(descriptor); - return this.pipelineLayouts.create(pipelineLayout); - }, - - /** - * @param {number} deviceIdx - * @param {number} descriptorPtr - * @returns {number} The query set. - */ - wgpuDeviceCreateQuerySet: (deviceIdx, descriptorPtr) => { - const device = this.devices.get(deviceIdx); - this.assert(descriptorPtr != 0); - - const off = this.struct(descriptorPtr); - off(4); - - /** @type {GPUQuerySetDescriptor} */ - const descriptor = { - label: this.StringView(off(this.sizes.StringView)), - type: this.enumeration("QueryType", off(4)), - count: this.mem.loadU32(off(4)), - }; - - const querySet = device.createQuerySet(descriptor); - return this.querySets.create(querySet); - }, - - /** - * @param {number} deviceIdx - * @param {number} descriptorPtr - * @returns {number} The query set. - */ - wgpuDeviceCreateRenderBundleEncoder: (deviceIdx, descriptorPtr) => { - const device = this.devices.get(deviceIdx); - this.assert(descriptorPtr != 0); - - const off = this.struct(descriptorPtr); - off(4); - - /** @type {GPURenderBundleEncoderDescriptor} */ - const descriptor = { - label: this.StringView(off(this.sizes.StringView)), - colorFormats: this.array( - this.mem.loadUint(off(this.mem.intSize)), - this.mem.loadPtr(off(4)), - this.TextureFormat, - 4, - ), - depthStencilFormat: this.enumeration("TextureFormat", off(4)), - sampleCount: this.mem.loadU32(off(4)), - depthReadOnly: this.mem.loadB32(off(4)), - stencilReadOnly: this.mem.loadB32(off(4)), - }; - - const renderBundleEncoder = device.createRenderBundleEncoder(descriptor); - return this.renderBundleEncoders.create(renderBundleEncoder); - }, - - /** - * @param {number} deviceIdx - * @param {number} descriptorPtr - * @returns {number} The render pipeline. - */ - wgpuDeviceCreateRenderPipeline: (deviceIdx, descriptorPtr) => { - const device = this.devices.get(deviceIdx); - this.assert(descriptorPtr != 0); - - const descriptor = this.RenderPipelineDescriptor(descriptorPtr); - const renderPipeline = device.createRenderPipeline(descriptor); - return this.renderPipelines.create(renderPipeline); - }, - - /** - * @param {number} deviceIdx - * @param {number} descriptorPtr - * @param {number} callbackInfo - */ - wgpuDeviceCreateRenderPipelineAsync: (deviceIdx, descriptorPtr, callbackInfoPtr) => { - const device = this.devices.get(deviceIdx); - this.assert(descriptorPtr != 0); - - const callbackInfo = this.CallbackInfo(callbackInfoPtr); - device.createRenderPipelineAsync(this.RenderPipelineDescriptor(descriptorPtr)) - .catch((e) => { - const messageAddr = this.makeMessageArg(e.message); - this.callCallback(callbackInfo, [ENUMS.CreatePipelineAsyncStatus.indexOf("Unknown"), 0, messageAddr]); - this.mem.exports.wgpu_free(messageAddr); - }) - .then((renderPipeline) => { - const renderPipelineIdx = this.renderPipelines.create(renderPipeline); - this.callCallback(callbackInfo, [ENUMS.CreatePipelineAsyncStatus.indexOf("Success"), renderPipelineIdx, this.zeroMessageArg()]); - }); - - // TODO: returning futures? - return BigInt(0); - }, - - /** - * @param {number} deviceIdx - * @param {0|number} descriptorPtr - * @returns {number} The sampler. - */ - wgpuDeviceCreateSampler: (deviceIdx, descriptorPtr) => { - const device = this.devices.get(deviceIdx); - - /** @type {?GPUSamplerDescriptor} */ - let descriptor; - if (descriptorPtr != 0) { - const off = this.struct(descriptorPtr); - off(4); - descriptor = { - label: this.StringView(off(this.sizes.StringView)), - addressModeU: this.enumeration("AddressMode", off(4)), - addressModeV: this.enumeration("AddressMode", off(4)), - addressModeW: this.enumeration("AddressMode", off(4)), - magFilter: this.enumeration("FilterMode", off(4)), - minFilter: this.enumeration("FilterMode", off(4)), - mipmapFilter: this.enumeration("MipmapFilterMode", off(4)), - lodMinClamp: this.mem.loadF32(off(4)), - lodMaxClamp: this.mem.loadF32(off(4)), - compare: this.enumeration("CompareFunction", off(4)), - maxAnisotropy: this.mem.loadU16(off(2)), - }; - } - - const sampler = device.createSampler(descriptor); - return this.samplers.create(sampler); - }, - - /** - * @param {number} deviceIdx - * @param {number} descriptorPtr - * @returns {number} The shader module. - */ - wgpuDeviceCreateShaderModule: (deviceIdx, descriptorPtr) => { - const device = this.devices.get(deviceIdx); - this.assert(descriptorPtr != 0); - - const off = this.struct(descriptorPtr); - - const nextInChain = this.mem.loadPtr(off(4)); - - const chainOff = this.struct(nextInChain); - chainOff(4); - - const nextInChainType = this.mem.loadI32(chainOff(4)); - - // ShaderSourceWGSL = 0x00000002, - if (nextInChainType != 2) { - throw new TypeError(`Descriptor type should be 'ShaderSourceWGSL', got ${nextInChainType}`); - } - - /** @type {GPUShaderModuleDescriptor} */ - const descriptor = { - label: this.StringView(off(this.sizes.StringView)), - code: this.StringView(chainOff(this.sizes.StringView)), - }; - - const shaderModule = device.createShaderModule(descriptor); - return this.shaderModules.create(shaderModule); - }, - - /** - * @param {number} deviceIdx - * @param {number} descriptorPtr - * @returns {number} The texture. - */ - wgpuDeviceCreateTexture: (deviceIdx, descriptorPtr) => { - const device = this.devices.get(deviceIdx); - this.assert(descriptorPtr != 0); - - const off = this.struct(descriptorPtr); - off(4); - - /** @type {GPUTextureDescriptor} */ - const descriptor = { - label: this.StringView(off(this.sizes.StringView)), - usage: this.mem.loadU64(off(8)), - dimension: this.enumeration("TextureDimension", off(4)), - size: this.Extent3D(off(this.sizes.Extent3D)), - format: this.enumeration("TextureFormat", off(4)), - mipLevelCount: this.mem.loadU32(off(4)), - sampleCount: this.mem.loadU32(off(4)), - viewFormats: this.array( - this.mem.loadUint(off(this.mem.intSize)), - this.mem.loadPtr(off(4)), - (ptr) => this.enumeration("TextureFormat", ptr), - 4, - ), - }; - - const texture = device.createTexture(descriptor); - return this.textures.create(texture); - }, - - /** - * @param {number} deviceIdx - */ - wgpuDeviceDestroy: (deviceIdx) => { - const device = this.devices.get(deviceIdx); - device.destroy(); - }, - - /** - * @param {number} deviceIdx - * @param {number} infoPtr - * @returns {number} - */ - wgpuDeviceGetAdapterInfo: (deviceIdx, infoPtr) => { - return this.genericGetAdapterInfo(infoPtr); - }, - - /** - * @param {number} deviceIdx - * @param {number} featuresPtr - */ - wgpuDeviceGetFeatures: (deviceIdx, featuresPtr) => { - const device = this.devices.get(deviceIdx); - return this.genericGetFeatures(device.features, featuresPtr); - }, - - /** - * @param {number} deviceIdx - * @param {number} limitsPtr - * @returns {number} - */ - wgpuDeviceGetLimits: (deviceIdx, limitsPtr) => { - const device = this.devices.get(deviceIdx); - return this.genericGetLimits(device.limits, limitsPtr); - }, - - /** - * @param {number} deviceIdx - * @returns {number} - */ - wgpuDeviceGetLostFuture: (deviceIdx) => { - // TODO: futures? - return BigInt(0); - }, - - /** - * @param {number} deviceIdx - * @returns {number} - */ - wgpuDeviceGetQueue: (deviceIdx) => { - const device = this.devices.get(deviceIdx); - return this.queues.create(device.queue); - }, - - /** - * @param {number} deviceIdx - * @param {number} featureInt - * @returns {boolean} - */ - wgpuDeviceHasFeature: (deviceIdx, featureInt) => { - const device = this.devices.get(deviceIdx); - return device.features.has(ENUMS.FeatureName[featureInt]); - }, - - /** - * @param {number} deviceIdx - * @param {number} callbackInfo - * @returns {number} - */ - wgpuDevicePopErrorScope: (deviceIdx, callbackInfoPtr) => { - const device = this.devices.get(deviceIdx); - - const callbackInfo = this.CallbackInfo(callbackInfoPtr); - device.popErrorScope() - .then((error) => { - if (!error) { - this.callCallback(callbackInfo, [ENUMS.PopErrorScopeStatus.indexOf("Success"), ENUMS.ErrorType.indexOf("no-error"), this.zeroMessageArg()]); - return; - } - - let status; - if (error instanceof GPUValidationError) { - status = ENUMS.ErrorType.indexOf("validation"); - } else if (error instanceof GPUOutOfMemoryError) { - status = ENUMS.ErrorType.indexOf("out-of-memory"); - } else if (error instanceof GPUInternalError) { - status = ENUMS.ErrorType.indexOf("internal"); - } else { - status = ENUMS.ErrorType.indexOf("unknown"); - } - - const messageAddr = error.message; - this.callCallback(callbackInfo, [ENUMS.PopErrorScopeStatus.indexOf("Success"), status, messageAddr]); - this.mem.exports.wgpu_free(messageAddr); - }); - - // TODO: futures? - return BigInt(0); - }, - - /** - * @param {number} deviceIdx - * @param {number} filterInt - */ - wgpuDevicePushErrorScope: (deviceIdx, filterInt) => { - const device = this.devices.get(deviceIdx); - device.pushErrorScope(ENUMS.ErrorFilter[filterInt]); - }, - - ...this.devices.interface(true), - - /* ---------------------- Instance ---------------------- */ - - /** - * @param {number} instanceIdx - * @param {number} descriptorPtr - */ - wgpuInstanceCreateSurface: (instanceIdx, descriptorPtr) => { - this.assert(instanceIdx > 0); - this.assert(descriptorPtr != 0); - - const off = this.struct(descriptorPtr); - - const nextInChain = this.mem.loadPtr(off(4)); - - const chainOff = this.struct(nextInChain); - chainOff(4); - - const nextInChainType = this.mem.loadI32(chainOff(4)); - - // SurfaceSourceCanvasHTMLSelector = 0x00040001, - if (nextInChainType != 0x00040001) { - throw new TypeError(`Descriptor type should be 'SurfaceSourceCanvasHTMLSelector', got ${nextInChainType}`); - } - - const selector = this.StringView(chainOff(this.sizes.StringView)); - const surface = document.querySelector(selector); - if (!surface) { - throw new Error(`Selector '${selector}' did not match any element`); - } - if (!(surface instanceof HTMLCanvasElement)) { - throw new Error('Selector matches an element that is not a canvas'); - } - - return this.surfaces.create(surface); - }, - - /** - * @param {number} instanceIdx - * @param {number} featurePtr - * @returns {number} - */ - wgpuInstanceGetWGSLLanguageFeatures: (instanceIdx, featuresPtr) => { - this.assert(featuresPtr != 0); - - const availableFeatures = []; - ENUMS.WGSLLanguageFeatureName.forEach((feature, value) => { - if (!feature) { - return; - } - - if (navigator.gpu.wgslLanguageFeatures.has(feature)) { - availableFeatures.push(value); - } - }); - - if (availableFeatures.length === 0) { - return; - } - - const featuresAddr = this.mem.exports.wgpu_alloc(availableFeatures.length * 4); - this.assert(featuresAddr != 0); - - let off = this.struct(featuresPtr); - this.mem.storeUint(off(this.mem.intSize), availableFeatures.length); - this.mem.storeI32(off(4), featuresAddr); - - off = this.struct(featuresAddr); - for (let i = 0; i < availableFeatures.length; i += 1) { - this.mem.storeI32(off(4), availableFeatures[i]); - } - - return STATUS_SUCCESS; - }, - - /** - * @param {number} instanceIdx - * @param {number} featureInt - * @returns {boolean} - */ - wgpuInstanceHasWGSLLanguageFeature: (instanceIdx, featureInt) => { - return navigator.gpu.wgslLanguageFeatures.has(ENUMS.WGSLLanguageFeatureName[featureInt]); - }, - - /** - * @param {number} instanceIdx - */ - wgpuInstanceProcessEvents: (instanceIdx) => { - console.warn("unimplemented: wgpuInstanceProcessEvents"); - }, - - /** - * @param {number} instanceIdx - * @param {0|number} optionsPtr - * @param {number} callbackInfo - * @returns {number} - */ - wgpuInstanceRequestAdapter: (instanceIdx, optionsPtr, callbackInfoPtr) => { - this.assert(instanceIdx > 0); - - /** @type {GPURequestAdapterOptions} */ - let options; - if (optionsPtr != 0) { - const off = this.struct(optionsPtr); - off(4); // nextInChain - off(4); // featureLevel - options = { - powerPreference: this.enumeration("PowerPreference", off(4)), - forceFallbackAdapter: this.mem.loadB32(off(4)), - }; - } - - const callbackInfo = this.CallbackInfo(callbackInfoPtr); - navigator.gpu.requestAdapter(options) - .catch((e) => { - const messageAddr = this.makeMessageArg(e.message); - this.callCallback(callbackInfo, [ENUMS.RequestAdapterStatus.indexOf("Error"), null, messageAddr]); - this.mem.exports.wgpu_free(messageAddr); - }) - .then((adapter) => { - const adapterIdx = this.adapters.create(adapter); - - this.callCallback(callbackInfo, [ENUMS.RequestAdapterStatus.indexOf("Success"), adapterIdx, this.zeroMessageArg()]); - }); - - // TODO: futures? - return BigInt(0); - }, - - wgpuInstanceWaitAny: (instanceIdx, futureCount, futuresPtr, timeoutNS) => { - // TODO: futures? - console.warn("unimplemented: wgpuInstanceProcessEvents"); - return BigInt(0); - }, - - ...this.instances.interface(false), - - /* ---------------------- PipelineLayout ---------------------- */ - - ...this.pipelineLayouts.interface(true), - - /* ---------------------- QuerySet ---------------------- */ - - /** - * @param {number} querySetIdx - */ - wgpuQuerySetDestroy: (querySetIdx) => { - const querySet = this.querySets.get(querySetIdx); - querySet.destroy(); - }, - - /** - * @param {number} querySetIdx - * @returns {number} - */ - wgpuQuerySetGetCount: (querySetIdx) => { - const querySet = this.querySets.get(querySetIdx); - return querySet.count; - }, - - /** - * @param {number} querySetIdx - * @returns {number} - */ - wgpuQuerySetGetType: (querySetIdx) => { - const querySet = this.querySets.get(querySetIdx); - return ENUMS.QueryType.indexOf(querySet.type); - }, - - ...this.querySets.interface(true), - - /* ---------------------- Queue ---------------------- */ - - /** - * @param {number} queueIdx - * @param {number} callbackInfo - */ - wgpuQueueOnSubmittedWorkDone: (queueIdx, callbackInfoPtr) => { - const queue = this.queues.get(queueIdx); - - const callbackInfo = this.CallbackInfo(callbackInfoPtr); - queue.onSubmittedWorkDone() - .catch((e) => { - console.warn(e); - this.callCallback(callbackInfo, [ENUMS.QueueWorkDoneStatus.indexOf("Error")]); - }) - .then(() => { - this.callCallback(callbackInfo, [ENUMS.QueueWorkDoneStatus.indexOf("Success")]); - }); - - // TODO: futures? - return BigInt(0); - }, - - /** - * @param {number} queueIdx - * @param {BigInt|number} commandCount - * @param {number} commandsPtr - */ - wgpuQueueSubmit: (queueIdx, commandCount, commandsPtr) => { - const queue = this.queues.get(queueIdx); - const commands = this.array( - this.unwrapBigInt(commandCount), - commandsPtr, - (ptr) => this.commandBuffers.get(this.mem.loadPtr(ptr)), - 4, - ); - queue.submit(commands); - }, - - /** - * @param {number} queueIdx - * @param {number} bufferIdx - * @param {BigInt} bufferOffset - * @param {number} dataPtr - * @param {number|BigInt} size - */ - wgpuQueueWriteBuffer: (queueIdx, bufferIdx, bufferOffset, dataPtr, size) => { - const queue = this.queues.get(queueIdx); - const buffer = this.buffers.get(bufferIdx); - bufferOffset = this.unwrapBigInt(bufferOffset); - size = this.unwrapBigInt(size); - queue.writeBuffer(buffer.buffer, bufferOffset, this.mem.loadBytes(dataPtr, size), 0, size); - }, - - /** - * @param {number} queueIdx - * @param {number} destinationPtr - * @param {number} dataPtr - * @param {number|BigInt} dataSize - * @param {number} dataLayoutPtr - * @param {number} writeSizePtr - */ - wgpuQueueWriteTexture: (queueIdx, destinationPtr, dataPtr, dataSize, dataLayoutPtr, writeSizePtr) => { - const queue = this.queues.get(queueIdx); - const destination = this.TexelCopyTextureInfo(destinationPtr); - dataSize = this.unwrapBigInt(dataSize); - const dataLayout = this.TexelCopyBufferLayout(dataLayoutPtr); - const writeSize = this.Extent3D(writeSizePtr); - queue.writeTexture(destination, this.mem.loadBytes(dataPtr, dataSize), dataLayout, writeSize); - }, - - ...this.queues.interface(true), - - /* ---------------------- RenderBundle ---------------------- */ - - ...this.renderBundles.interface(true), - - /* ---------------------- RenderBundleEncoder ---------------------- */ - - /** - * @param {number} renderBundleEncoderIdx - * @param {number} vertexCount - * @param {number} instanceCount - * @param {number} firstVertex - * @param {number} firstInstance - */ - wgpuRenderBundleEncoderDraw: (renderBundleEncoderIdx, vertexCount, instanceCount, firstVertex, firstInstance) => { - const renderBundleEncoder = this.renderBundleEncoders.get(renderBundleEncoderIdx); - renderBundleEncoder.draw(vertexCount, instanceCount, firstVertex, firstInstance); - }, - - /** - * @param {number} renderBundleEncoderIdx - * @param {number} indexCount - * @param {number} instanceCount - * @param {number} firstIndex - * @param {number} baseVertex - * @param {number} firstInstance - */ - wgpuRenderBundleEncoderDrawIndexed: (renderBundleEncoderIdx, indexCount, instanceCount, firstIndex, baseVertex, firstInstance) => { - const renderBundleEncoder = this.renderBundleEncoders.get(renderBundleEncoderIdx); - renderBundleEncoder.drawIndexed(indexCount, instanceCount, firstIndex, baseVertex, firstInstance); - }, - - /** - * @param {number} renderBundleEncoderIdx - * @param {number} indirectBufferIdx - * @param {BigInt} indirectOffset - */ - wgpuRenderBundleEncoderDrawIndexedIndirect: (renderBundleEncoderIdx, indirectBufferIdx, indirectOffset) => { - const renderBundleEncoder = this.renderBundleEncoders.get(renderBundleEncoderIdx); - indirectOffset = this.unwrapBigInt(indirectOffset); - const buffer = this.buffers.get(indirectBufferIdx); - renderBundleEncoder.drawIndexedIndirect(buffer.buffer, indirectOffset); - }, - - /** - * @param {number} renderBundleEncoderIdx - * @param {number} indirectBufferIdx - * @param {BigInt} indirectOffset - */ - wgpuRenderBundleEncoderDrawIndirect: (renderBundleEncoderIdx, indirectBufferIdx, indirectOffset) => { - const renderBundleEncoder = this.renderBundleEncoders.get(renderBundleEncoderIdx); - indirectOffset = this.unwrapBigInt(indirectOffset); - const buffer = this.buffers.get(indirectBufferIdx); - renderBundleEncoder.drawIndirect(buffer.buffer, indirectOffset); - }, - - /** - * @param {number} renderBundleEncoderIdx - * @param {0|number} descriptorPtr - * @returns {number} - */ - wgpuRenderBundleEncoderFinish: (renderBundleEncoderIdx, descriptorPtr) => { - const renderBundleEncoder = this.renderBundleEncoders.get(renderBundleEncoderIdx); - - /** @type {?GPURenderBundleDescriptor} */ - let descriptor; - if (descriptorPtr != 0) { - descriptor = { - label: this.StringView(descriptorPtr + 4), - }; - } - - const renderBundle = renderBundleEncoder.finish(descriptor); - return this.renderBundles.create(renderBundle); - }, - - /** - * @param {number} renderBundleEncoderIdx - * @param {number} markerLabelPtr - * @param {number} markerLabelLen - */ - wgpuRenderBundleEncoderInsertDebugMarker: (renderBundleEncoderIdx, markerLabelPtr, markerLabelLen) => { - const renderBundleEncoder = this.renderBundleEncoders.get(renderBundleEncoderIdx); - this.assert(markerLabelPtr != 0); - const markerLabel = this.mem.loadString(markerLabelPtr, markerLabelLen); - renderBundleEncoder.insertDebugMarker(markerLabel); - }, - - /** - * @param {number} renderBundleEncoderIdx - */ - wgpuRenderBundleEncoderPopDebugGroup: (renderBundleEncoderIdx) => { - const renderBundleEncoder = this.renderBundleEncoders.get(renderBundleEncoderIdx); - renderBundleEncoder.popDebugGroup(); - }, - - /** - * @param {number} renderBundleEncoderIdx - * @param {number} groupLabelPtr - * @param {number} grouplabelLen - */ - wgpuRenderBundleEncoderPushDebugGroup: (renderBundleEncoderIdx, groupLabelPtr, grouplabelLen) => { - const renderBundleEncoder = this.renderBundleEncoders.get(renderBundleEncoderIdx); - this.assert(groupLabelPtr!= 0); - const groupLabel = this.mem.loadString(groupLabelPtr, groupLabelLen); - renderBundleEncoder.pushDebugGroup(groupLabel); - }, - - /** - * @param {number} renderBundleEncoderIdx - * @param {number} groupIndex - * @param {0|number} groupIdx - * @param {number|BigInt} dynamicOffsetCount - * @param {number} dynamicOffsetsPtr - */ - wgpuRenderBundleEncoderSetBindGroup: (renderBundleEncoderIdx, groupIndex, groupIdx, dynamicOffsetCount, dynamicOffsetsPtr) => { - const renderBundleEncoder = this.renderBundleEncoders.get(renderBundleEncoderIdx); - - let group; - if (groupIdx > 0) { - group = this.bindGroups.get(groupIdx); - } - - dynamicOffsetCount = this.unwrapBigInt(dynamicOffsetCount); - const dynamicOffsets = this.array( - dynamicOffsetCount, - dynamicOffsetsPtr, - (ptr) => this.mem.loadU32(ptr), - 4 - ); - - renderBundleEncoder.setBindGroup(groupIndex, group, dynamicOffsets); - }, - - /** - * @param {number} renderBundleEncoderIdx - * @param {number} bufferIdx - * @param {number} formatInt - * @param {BigInt} offset - * @param {BigInt} size - */ - wgpuRenderBundleEncoderSetIndexBuffer: (renderBundleEncoderIdx, bufferIdx, formatInt, offset, size) => { - const renderBundleEncoder = this.renderBundleEncoders.get(renderBundleEncoderIdx); - const buffer = this.buffers.get(bufferIdx); - const format = ENUMS.IndexFormat[formatInt]; - offset = this.unwrapBigInt(offset); - size = this.unwrapBigInt(size); - renderBundleEncoder.setIndexBuffer(buffer.buffer, format, offset, size); - }, - - /** - * @param {number} renderBundleEncoderIdx - * @param {number} pipelineIdx - */ - wgpuRenderBundleEncoderSetPipeline: (renderBundleEncoderIdx, pipelineIdx) => { - const renderBundleEncoder = this.renderBundleEncoders.get(renderBundleEncoderIdx); - const pipeline = this.renderPipelines.get(pipelineIdx); - renderBundleEncoder.setPipeline(pipeline); - }, - - /** - * @param {number} renderBundleEncoderIdx - * @param {number} slot - * @param {0|number} bufferIdx - * @param {BigInt} offset - * @param {BigInt} size - */ - wgpuRenderBundleEncoderSetVertexBuffer: (renderBundleEncoderIdx, slot, bufferIdx, offset, size) => { - const renderBundleEncoder = this.renderBundleEncoders.get(renderBundleEncoderIdx); - - let buffer; - if (bufferIdx > 0) { - buffer = this.buffers.get(bufferIdx).buffer; - } - - offset = this.unwrapBigInt(offset); - size = this.unwrapBigInt(size); - renderBundleEncoder.setVertexBuffer(slot, buffer, offset, size); - }, - - ...this.renderBundleEncoders.interface(true), - - /* ---------------------- RenderPassEncoder ---------------------- */ - - /** - * @param {number} renderPassEncoderIdx - * @param {number} queryIndex - */ - wgpuRenderPassEncoderBeginOcclusionQuery: (renderPassEncoderIdx, queryIndex) => { - const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); - renderPassEncoder.beginOcclusionQuery(queryIndex); - }, - - /** - * @param {number} renderPassEncoderIdx - * @param {number} vertexCount - * @param {number} instanceCount - * @param {number} firstVertex - * @param {number} firstInstance - */ - wgpuRenderPassEncoderDraw: (renderPassEncoderIdx, vertexCount, instanceCount, firstVertex, firstInstance) => { - const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); - renderPassEncoder.draw(vertexCount, instanceCount, firstVertex, firstInstance); - }, - - /** - * @param {number} renderPassEncoderIdx - * @param {number} indexCount - * @param {number} instanceCount - * @param {number} firstIndex - * @param {number} baseVertex - * @param {number} firstInstance - */ - wgpuRenderPassEncoderDrawIndexed: (renderPassEncoderIdx, indexCount, instanceCount, firstIndex, baseVertex, firstInstance) => { - const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); - renderPassEncoder.drawIndexed(indexCount, instanceCount, firstIndex, baseVertex, firstInstance); - }, - - /** - * @param {number} renderPassEncoderIdx - * @param {number} indirectBufferIdx - * @param {BigInt} indirectOffset - */ - wgpuRenderPassEncoderDrawIndexedIndirect: (renderPassEncoderIdx, indirectBufferIdx, indirectOffset) => { - const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); - const buffer = this.buffers.get(indirectBufferIdx); - indirectOffset = this.unwrapBigInt(indirectOffset); - renderPassEncoder.drawIndexedIndirect(buffer.buffer, indirectOffset); - }, - - /** - * @param {number} renderPassEncoderIdx - * @param {number} indirectBufferIdx - * @param {BigInt} indirectOffset - */ - wgpuRenderPassEncoderDrawIndirect: (renderPassEncoderIdx, indirectBufferIdx, indirectOffset) => { - const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); - const buffer = this.buffers.get(indirectBufferIdx); - indirectOffset = this.unwrapBigInt(indirectOffset); - renderPassEncoder.drawIndirect(buffer.buffer, indirectOffset); - }, - - /** - * @param {number} renderPassEncoderIdx - */ - wgpuRenderPassEncoderEnd: (renderPassEncoderIdx) => { - const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); - renderPassEncoder.end(); - }, - - /** - * @param {number} renderPassEncoderIdx - */ - wgpuRenderPassEncoderEndOcclusionQuery: (renderPassEncoderIdx) => { - const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); - renderPassEncoder.endOcclusionQuery(); - }, - - /** - * @param {number} renderPassEncoderIdx - * @param {number|BigInt} bundleCount - * @param {number} bundlesPtr - */ - wgpuRenderPassEncoderExecuteBundles: (renderPassEncoderIdx, bundleCount, bundlesPtr) => { - const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); - bundleCount = this.unwrapBigInt(bundleCount); - const bundles = this.array( - bundleCount, - bundlesPtr, - (ptr) => this.renderBundles.get(this.mem.loadPtr(ptr)), - 4, - ); - renderPassEncoder.executeBundles(bundles); - }, - - /** - * @param {number} renderPassEncoderIdx - * @param {number} markerLabelPtr - * @param {number} markerLabelLen - */ - wgpuRenderPassEncoderInsertDebugMarker: (renderPassEncoderIdx, markerLabelPtr, markerLabelLen) => { - const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); - const markerLabel = this.mem.loadString(markerLabelPtr, markerLabelLen); - renderPassEncoder.insertDebugMarker(markerLabel); - }, - - /** - * @param {number} renderPassEncoderIdx - */ - wgpuRenderPassEncoderPopDebugGroup: (renderPassEncoderIdx) => { - const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); - renderPassEncoder.popDebugGroup(); - }, - - /** - * @param {number} renderPassEncoderIdx - * @param {number} groupLabelPtr - * @param {number} groupLabelLen - */ - wgpuRenderPassEncoderPushDebugGroup: (renderPassEncoderIdx, groupLabelPtr, groupLabelLen) => { - const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); - const groupLabel = this.mem.loadString(groupLabelPtr, groupLabelLen); - renderPassEncoder.pushDebugGroup(groupLabel); - }, - - /** - * @param {number} renderPassEncoderIdx - * @param {number} groupIndex - * @param {0|number} groupIdx - * @param {number|BigInt} dynamicOffsetCount - * @param {number} dynamicOffsetsPtr - */ - wgpuRenderPassEncoderSetBindGroup: (renderPassEncoderIdx, groupIndex, groupIdx, dynamicOffsetCount, dynamicOffsetsPtr) => { - const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); - - let group; - if (groupIdx > 0) { - group = this.bindGroups.get(groupIdx); - } - - dynamicOffsetCount = this.unwrapBigInt(dynamicOffsetCount); - const dynamicOffsets = this.array( - dynamicOffsetCount, - dynamicOffsetsPtr, - (ptr) => this.mem.loadU32(ptr), - 4 - ); - - renderPassEncoder.setBindGroup(groupIndex, group, dynamicOffsets); - }, - - /** - * @param {number} renderPassEncoderIdx - * @param {number} colorPtr - */ - wgpuRenderPassEncoderSetBlendConstant: (renderPassEncoderIdx, colorPtr) => { - const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); - this.assert(colorPtr != 0); - renderPassEncoder.setBlendConstant(this.Color(colorPtr)); - }, - - /** - * @param {number} renderPassEncoderIdx - * @param {number} bufferIdx - * @param {number} formatInt - * @param {BigInt} offset - * @param {BigInt} size - */ - wgpuRenderPassEncoderSetIndexBuffer: (renderPassEncoderIdx, bufferIdx, formatInt, offset, size) => { - const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); - const buffer = this.buffers.get(bufferIdx); - const format = ENUMS.IndexFormat[formatInt]; - offset = this.unwrapBigInt(offset); - size = this.unwrapBigInt(size); - renderPassEncoder.setIndexBuffer(buffer.buffer, format, offset, size); - }, - - /** - * @param {number} renderPassEncoderIdx - * @param {number} pipelineIdx - */ - wgpuRenderPassEncoderSetPipeline: (renderPassEncoderIdx, pipelineIdx) => { - const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); - const pipeline = this.renderPipelines.get(pipelineIdx); - renderPassEncoder.setPipeline(pipeline); - }, - - /** - * @param {number} renderPassEncoderIdx - * @param {number} x - * @param {number} y - * @param {number} width - * @param {number} height - */ - wgpuRenderPassEncoderSetScissorRect: (renderPassEncoderIdx, x, y, width, height) => { - const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); - renderPassEncoder.setScissorRect(x, y, width, height); - }, - - /** - * @param {number} renderPassEncoderIdx - * @param {number} reference - */ - wgpuRenderPassEncoderSetStencilReference: (renderPassEncoderIdx, reference) => { - const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); - renderPassEncoder.setStencilReference(reference); - }, - - /** - * @param {number} renderPassEncoderIdx - * @param {number} slot - * @param {0|number} bufferIdx - * @param {BigInt} offset - * @param {BigInt} size - */ - wgpuRenderPassEncoderSetVertexBuffer: (renderPassEncoderIdx, slot, bufferIdx, offset, size) => { - const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); - - let buffer; - if (bufferIdx > 0) { - buffer = this.buffers.get(bufferIdx).buffer; - } - - offset = this.unwrapBigInt(offset); - size = this.unwrapBigInt(size); - renderPassEncoder.setVertexBuffer(slot, buffer, offset, size); - }, - - /** - * @param {number} renderPassEncoderIdx - * @param {number} x - * @param {number} y - * @param {number} width - * @param {number} height - * @param {number} minDepth - * @param {number} maxDepth - */ - wgpuRenderPassEncoderSetViewport: (renderPassEncoderIdx, x, y, width, height, minDepth, maxDepth) => { - const renderPassEncoder = this.renderPassEncoders.get(renderPassEncoderIdx); - renderPassEncoder.setViewport(x, y, width, height, minDepth, maxDepth); - }, - - ...this.renderPassEncoders.interface(true), - - /* ---------------------- RenderPipeline ---------------------- */ - - /** - * @param {number} renderPipelineIdx - * @param {number} groupIndex - * @returns {number} - */ - wgpuRenderPipelineGetBindGroupLayout: (renderPipelineIdx, groupIndex) => { - const renderPipeline = this.renderPipelines.get(renderPipelineIdx); - const bindGroupLayout = renderPipeline.getBindGroupLayout(groupIndex); - return this.bindGroupLayouts.create(bindGroupLayout); - }, - - ...this.renderPipelines.interface(true), - - /* ---------------------- Sampler ---------------------- */ - - ...this.samplers.interface(true), - - /* ---------------------- ShaderModule ---------------------- */ - - /** - * @param {number} shaderModuleIdx - * @param {number} callbackInfo - */ - wgpuShaderModuleGetCompilationInfo: (shaderModuleIdx, callbackInfoPtr) => { - const shaderModule = this.shaderModules.get(shaderModuleIdx); - - const callbackInfo = this.CallbackInfo(callbackInfoPtr); - shaderModule.getCompilationInfo() - .catch((e) => { - console.warn(e); - this.callCallback(callbackInfo, [ENUMS.CompilationInfoRequestStatus.indexOf("Error"), null]); - }) - .then((compilationInfo) => { - const ptrsToFree = []; - - const compilationMessageSize = this.sizes.CompilationMessage[0]; - - const size = compilationInfo.messages.length * compilationMessageSize; - - const addr = this.mem.exports.wgpu_alloc(size); - ptrsToFree.push(addr); - - compilationInfo.messages.forEach((message, i) => { - const messageLength = new TextEncoder().encode(message.message).length; - const messageAddr = this.mem.exports.wgpu_alloc(messageLength); - ptrsToFree.push(messageAddr); - this.mem.storeString(messageAddr, message.message); - - const off = this.struct(addr + (i * compilationMessageSize)); - off(4); - - const messageStart = off(this.sizes.StringView); - this.mem.storeI32(messageStart, messageAddr); - this.mem.storeUint(messageStart + this.mem.intSize, messageLength); - - this.mem.storeI32(off(4), ENUMS.CompilationMessageType.indexOf(message.type)); - - this.mem.storeU64(off(8), message.lineNum); - this.mem.storeU64(off(8), message.linePos); - this.mem.storeU64(off(8), message.offset); - this.mem.storeU64(off(8), message.length); - }); - - const retAddr = this.mem.exports.wgpu_alloc(3*this.mem.intSize); - ptrsToFree.push(retAddr); - this.mem.storeUint(retAddr + this.mem.intSize, compilationInfo.messages.length); - this.mem.storeI32(retAddr + this.mem.intSize*2, addr); - - this.callCallback(callbackInfo, [ENUMS.CompilationInfoRequestStatus.indexOf("Success"), retAddr]); - - ptrsToFree.forEach(ptr => this.mem.exports.wgpu_free(ptr)); - }); - - // TODO: futures? - return BigInt(0); - }, - - ...this.shaderModules.interface(true), - - /* ---------------------- SupportedFeatures ---------------------- */ - - wgpuSupportedFeaturesFreeMembers: (supportedFeaturesCount, supportedFeaturesPtr) => { - this.mem.exports.wgpu_free(supportedFeaturesPtr); - }, - - /* ---------------------- SupportedWGSLLanguageFeatures ---------------------- */ - - wgpuSupportedWGSLLanguageFeaturesFreeMembers: (supportedFeaturesCount, supportedFeaturesPtr) => { - this.mem.exports.wgpu_free(supportedFeaturesPtr); - }, - - /* ---------------------- Surface ---------------------- */ - - /** - * @param {number} surfaceIdx - * @param {number} configPtr - */ - wgpuSurfaceConfigure: (surfaceIdx, configPtr) => { - const surface = this.surfaces.get(surfaceIdx); - const context = surface.getContext("webgpu"); - - const off = this.struct(configPtr); - off(4); - const device = this.devices.get(this.mem.loadPtr(off(4))); - const format = this.enumeration("TextureFormat", off(4)); - const usage = this.mem.loadU64(off(8)); - const width = this.mem.loadU32(off(4)); - const height = this.mem.loadU32(off(4)); - const viewFormats = this.array( - this.mem.loadUint(off(this.mem.intSize)), - this.mem.loadPtr(off(4)), - (ptr) => this.enumeration("TextureFormat", ptr), - 4, - ); - const alphaMode = this.enumeration("CompositeAlphaMode", off(4)); - // NOTE: present mode seems unused. - const presentMode = this.enumeration("PresentMode", off(4)); - - surface.width = width; - surface.height = height; - - /** @type {GPUCanvasConfiguration} */ - const config = { - device: device, - format: format, - usage: usage, - viewFormats: viewFormats, - alphaMode: alphaMode, - presentMode: presentMode, - }; - - context.configure(config); - }, - - /** - * @param {number} surfaceIdx - * @param {number} adapterIdx - * @param {number} capabilitiesPtr - * @return {number} - */ - wgpuSurfaceGetCapabilities: (surfaceIdx, adapterIdx, capabilitiesPtr) => { - const off = this.struct(capabilitiesPtr); - off(4); // nextInChain - off(8); // usages TODO: can we pass this? - - const formatStr = navigator.gpu.getPreferredCanvasFormat(); - const format = ENUMS.TextureFormat.indexOf(formatStr); - - this.mem.storeUint(off(this.mem.intSize), 1); - const formatAddr = this.mem.exports.wgpu_alloc(4); - this.mem.storeI32(formatAddr, format); - this.mem.storeI32(off(4), formatAddr); - - // NOTE: present modes don't seem to actually do anything in JS, we can just give back a default FIFO though. - this.mem.storeUint(off(this.mem.intSize), 1); - const presentModesAddr = this.mem.exports.wgpu_alloc(4); - this.mem.storeI32(presentModesAddr, ENUMS.PresentMode.indexOf("fifo")); - this.mem.storeI32(off(4), presentModesAddr); - - // Browser seems to support opaque and premultiplied. - this.mem.storeUint(off(this.mem.intSize), 2); - const alphaModesAddr = this.mem.exports.wgpu_alloc(8); - this.mem.storeI32(alphaModesAddr + 0, ENUMS.CompositeAlphaMode.indexOf("opaque")); - this.mem.storeI32(alphaModesAddr + 4, ENUMS.CompositeAlphaMode.indexOf("premultiplied")); - this.mem.storeI32(off(4), alphaModesAddr); - - return STATUS_SUCCESS; - }, - - /** - * @param {number} surfaceIdx - * @param {number} texturePtr - */ - wgpuSurfaceGetCurrentTexture: (surfaceIdx, texturePtr) => { - const surface = this.surfaces.get(surfaceIdx); - const context = surface.getContext('webgpu'); - const texture = context.getCurrentTexture(); - - const textureIdx = this.textures.create(texture); - this.mem.storeI32(texturePtr + 4, textureIdx); - - // TODO: determine suboptimal and/or status. - }, - - /** - * @param {number} surfaceIdx - */ - wgpuSurfacePresent: (surfaceIdx) => { - // NOTE: Not really anything to do here. - }, - - /** - * @param {number} surfaceIdx - */ - wgpuSurfaceUnconfigure: (surfaceIdx) => { - const surface = this.surfaces.get(surfaceIdx); - surface.getContext('webgpu').unconfigure(); - }, - - ...this.surfaces.interface(true), - - /* ---------------------- SurfaceCapabilities ---------------------- */ - - /** - * @param {number} surfaceCapabilitiesPtr - */ - wgpuSurfaceCapabilitiesFreeMembers: (surfaceCapabilitiesPtr) => { - const off = this.struct(surfaceCapabilitiesPtr); - off(4); // nextInChain - off(8); // usages - off(this.mem.intSize); // formatCount - - const formatsAddr = this.mem.loadPtr(off(4)); - this.mem.exports.wgpu_free(formatsAddr); - - off(this.mem.intSize); // presentModeCount - - const presentModesAddr = this.mem.loadPtr(off(4)); - this.mem.exports.wgpu_free(presentModesAddr); - - off(this.mem.intSize); // alphaModeCount - - const alphaModesAddr = this.mem.loadPtr(off(4)); - this.mem.exports.wgpu_free(alphaModesAddr); - }, - - /* ---------------------- Texture ---------------------- */ - - /** - * @param {number} textureIdx - * @param {0|number} descriptorPtr - * @returns {number} - */ - wgpuTextureCreateView: (textureIdx, descriptorPtr) => { - const texture = this.textures.get(textureIdx); - - /** @type {?GPUTextureViewDescriptor} */ - let descriptor; - if (descriptorPtr != 0) { - const off = this.struct(descriptorPtr); - off(4); - descriptor = { - label: this.StringView(off(this.sizes.StringView)), - format: this.enumeration("TextureFormat", off(4)), - dimension: this.enumeration("TextureViewDimension", off(4)), - baseMipLevel: this.mem.loadU32(off(4)), - mipLevelCount: this.mem.loadU32(off(4)), - baseArrayLayer: this.mem.loadU32(off(4)), - arrayLayerCount: this.mem.loadU32(off(4)), - aspect: this.enumeration("TextureAspect", off(4)), - usage: this.mem.loadU64(off(8)), - }; - if (descriptor.arrayLayerCount == 0xFFFFFFFF) { - descriptor.arrayLayerCount = undefined; - } - if (descriptor.mipLevelCount == 0xFFFFFFFF) { - descriptor.mipLevelCount = undefined; - } - } - - const textureView = texture.createView(descriptor); - return this.textureViews.create(textureView); - }, - - /** - * @param {number} textureIdx - */ - wgpuTextureDestroy: (textureIdx) => { - const texture = this.textures.get(textureIdx); - texture.destroy(); - }, - - /** - * @param {number} textureIdx - * @returns {number} - */ - wgpuTextureDepthOrArrayLayers: (textureIdx) => { - const texture = this.textures.get(textureIdx); - return texture.depthOrArrayLayers; - }, - - /** - * @param {number} textureIdx - * @returns {number} - */ - wgpuTextureGetDimension: (textureIdx) => { - const texture = this.textures.get(textureIdx); - return ENUMS.TextureDimension.indexOf(texture.dimension); - }, - - /** - * @param {number} textureIdx - * @returns {number} - */ - wgpuTextureGetFormat: (textureIdx) => { - const texture = this.textures.get(textureIdx); - return ENUMS.TextureFormat.indexOf(texture.format); - }, - - /** - * @param {number} textureIdx - * @returns {number} - */ - wgpuTextureGetHeight: (textureIdx) => { - const texture = this.textures.get(textureIdx); - return texture.height; - }, - - /** - * @param {number} textureIdx - * @returns {number} - */ - wgpuTextureGetMipLevelCount: (textureIdx) => { - const texture = this.textures.get(textureIdx); - return texture.mipLevelCount; - }, - - /** - * @param {number} textureIdx - * @returns {number} - */ - wgpuTextureGetSampleCount: (textureIdx) => { - const texture = this.textures.get(textureIdx); - return texture.sampleCount; - }, - - /** - * @param {number} textureIdx - * @returns {number} - */ - wgpuTextureGetUsage: (textureIdx) => { - const texture = this.textures.get(textureIdx); - return texture.usage; - }, - - /** - * @param {number} textureIdx - * @returns {number} - */ - wgpuTextureGetWidth: (textureIdx) => { - const texture = this.textures.get(textureIdx); - return texture.width; - }, - - ...this.textures.interface(true), - - /* ---------------------- TextureView ---------------------- */ - - ...this.textureViews.interface(true), - }; - } -} - -/** @template T */ -class WebGPUObjectManager { - - /** - * @param {string} name - * @param {WasmMemoryInterface} mem - */ - constructor(name, mem) { - this.name = name; - this.mem = mem; - - this.idx = 0; - - /** @type {Record} */ - this.objects = {}; - } - - /** - * @param {T} object - * @returns {number} - */ - create(object) { - this.objects[this.idx] = { references: 1, object }; - this.idx += 1; - return this.idx; - } - - /** - * @param {?number} idx - * @returns {T} - */ - get(idx) { - return this.objects[idx-1]?.object; - } - - /** @param {number} idx */ - release(idx) { - this.objects[idx-1].references -= 1; - if (this.objects[idx-1].references == 0) { - delete this.objects[idx-1]; - } - } - - /** @param {number} idx */ - reference(idx) { - this.objects[idx-1].references += 1; - } - - interface(withLabelSetter = false) { - const inter = {}; - inter[`wgpu${this.name}AddRef`] = this.reference.bind(this); - inter[`wgpu${this.name}Release`] = this.release.bind(this); - if (withLabelSetter) { - inter[`wgpu${this.name}SetLabel`] = (idx, labelPtr, labelLen) => { - const obj = this.get(idx); - obj.label = this.mem.loadString(labelPtr, labelLen); - }; - } - return inter; - } -} - -window.odin = window.odin || {}; -window.odin.WebGPUInterface = WebGPUInterface; - -})();