refactor: move world concepts into engine
This commit is contained in:
parent
eeedcb8e48
commit
2d15c7ab8d
52 changed files with 3298 additions and 1558 deletions
|
|
@ -1,137 +0,0 @@
|
|||
# Jerklint Review 01
|
||||
|
||||
Reviewed 2026-08-04. Scope: handwritten source under `engine/`, `app/`,
|
||||
`server/`, `shared/`, and `scripts/`; root runtime, build, lint, editor, and
|
||||
project configuration; `README.md`. Generated assets, build output,
|
||||
dependencies, and lockfile were excluded.
|
||||
|
||||
**Jerklint Findings**
|
||||
|
||||
1. **High** `engine/render/Color.ts:37-43`,
|
||||
`engine/render/Texture.ts:46-56`, `engine/render/Rasterizer.ts:199-203`,
|
||||
`engine/render/RenderConfig.ts:104-114`, `scripts/gen-assets.ts:136-143`:
|
||||
`Color.lerp` promises to interpolate a `Color` but omits alpha, so `Color.rgb`
|
||||
silently supplies `255`. Every bilinear sample therefore becomes opaque.
|
||||
The `clean` preset enables linear filtering, while the NPC asset has a
|
||||
transparent background and alpha cutout happens after sampling. This is a
|
||||
false generic abstraction with a direct behavior change hidden behind an
|
||||
unrelated render knob. Smallest fix: interpolate alpha in `Color.lerp`; add
|
||||
an explicitly named RGB-only blend only if fog or sky ever needs one.
|
||||
|
||||
2. **High** `engine/render/RenderConfig.ts:16-58`,
|
||||
`engine/render/RenderConfig.ts:64-115`, `app/main.ts:19-29`,
|
||||
`engine/render/Framebuffer.ts:43-73`: live render configuration has no
|
||||
authoritative validity or application boundary. Public presets are mutable
|
||||
singleton objects, `useConfig` retains the supplied alias, dimensions are
|
||||
snapshotted into framebuffer resources, and documented numeric ranges are
|
||||
not enforced. A direct width change leaves resources stale; values such as
|
||||
`colorDepth = 0` make quantization divide by zero. This will become immediate
|
||||
pressure when planned live sliders arrive. Smallest fix: make presets
|
||||
readonly templates, create a fresh validated config for each update, and
|
||||
route updates through one app-side apply function that rebuilds resources
|
||||
when dimensions change.
|
||||
|
||||
|
||||
4. **Medium** `app/level.ts:18-27`, `app/level.ts:66-105`,
|
||||
`app/player.ts:2`, `app/player.ts:19-31`, `app/player.ts:61-97`,
|
||||
`app/main.ts:48-52`: collision policy has no single owner. `Player.update`
|
||||
accepts the render-heavy `Level`, then reconstructs gameplay facts using a
|
||||
hardcoded ground height and NPC radius; NPC visual size lives in `main.ts`.
|
||||
`buildLevel` also constructs geometry, collision, NPC placement, and sky in
|
||||
one body. NPC or floor changes therefore require cross-file agreement and
|
||||
isolated player tests need unrelated render data. Smallest fix: let `Level`
|
||||
own a compact NPC descriptor and a narrow collision world containing ground,
|
||||
boxes, and circles; pass only that collision value to `Player.update`.
|
||||
|
||||
5. **Medium** `app/main.ts:54-69`, `app/player.ts:23-55`: browser key codes are
|
||||
the player simulation API. Physics owns WASD and Space bindings, receives a
|
||||
mutable `Set<string>`, cannot represent a press edge, and therefore treats a
|
||||
held jump key as a fresh jump whenever landing. Lost `keyup` events also
|
||||
leave state stuck because no blur path clears the set. Rebinding, gamepad
|
||||
input, and deterministic tests all require physics edits. Smallest fix: map
|
||||
browser events in `main.ts` to a semantic `PlayerInput` value with movement
|
||||
axes and `jumpPressed`; clear raw input on blur.
|
||||
|
||||
6. **Medium** `engine/scene/Camera.ts:27-33`,
|
||||
`engine/math/Mat4.ts:23-34`, `engine/render/Rasterizer.ts:9-19`,
|
||||
`engine/render/Rasterizer.ts:58-68`, `engine/render/Rasterizer.ts:77-115`:
|
||||
projection limits and clipping have separate authorities. Camera projection
|
||||
declares near/far values of `0.05` and `100`, but `Rasterizer.project`
|
||||
discards clip-space `z` and clips only against unrelated `w >= 0.01`; far
|
||||
clipping is absent. `Rasterizer.draw` looks matrix-generic while relying on
|
||||
perspective-specific `w` semantics for clipping, depth, and fog. Smallest
|
||||
fix: retain clip-space `z` and clip against canonical near/far planes, or
|
||||
narrow and name the API so its perspective-matrix contract is explicit.
|
||||
|
||||
7. **Medium** `engine/render/RenderConfig.ts:48-54`,
|
||||
`engine/render/Texture.ts:36-38`, `engine/render/Rasterizer.ts:51-57`,
|
||||
`engine/render/Sky.ts:34-36`, `engine/render/Sky.ts:93-98`,
|
||||
`engine/render/Sky.ts:121-128`: closed option unions fail open. New texture
|
||||
filters silently become nearest, new lighting modes silently become unlit,
|
||||
and new cloud kinds silently become basic because each dispatcher uses a
|
||||
binary predicate plus fallback. `basicCumulus` accepts the full `CloudLayer`
|
||||
union, preventing TypeScript from exposing the missing branch. Smallest fix:
|
||||
use exhaustive switches with a `never` check and narrow cloud helpers to
|
||||
their concrete variant types.
|
||||
|
||||
11. **Low** `engine/scene/Sprite.ts:9-23`, `app/main.ts:48-52`,
|
||||
`app/main.ts:107-112`: `Sprite.texture` is assigned but never read; drawing
|
||||
separately reaches back to `textures.npc`. Two sources can diverge, and the
|
||||
type promises ownership the render path ignores. Smallest fix: draw with
|
||||
`npc.texture`, or remove the field if material binding is intentionally
|
||||
external.
|
||||
|
||||
12. **Low** `engine/scene/Camera.ts:4-13`, `engine/math/Mat4.ts:37-43`,
|
||||
`engine/render/Sky.ts:62-66`, `app/main.ts:73-79`: camera pitch validity is
|
||||
documented by `Camera` but enforced by one caller through the unexplained
|
||||
literal `1.4`. Both camera and sky basis construction rely on that ritual.
|
||||
Any second camera producer can create a degenerate basis. Smallest fix: own
|
||||
the pitch limit and look-delta/clamp operation in the `Camera` namespace.
|
||||
|
||||
14. **Low** `engine/math/Vec3.ts:3-4`, `engine/math/Vec3.ts:34-37`:
|
||||
`Vec3.normalize` violates its namespace-wide fresh-result guarantee only for
|
||||
zero vectors by returning the mutable input alias. Callers can safely mutate
|
||||
ordinary results but unexpectedly mutate source state at one edge. Smallest
|
||||
fix: return a fresh zero vector.
|
||||
|
||||
**Scorecard**
|
||||
|
||||
- DRY: concern - collision, release, and resource-update policy have split owners.
|
||||
- KISS: pass - core engine stays direct, data-oriented, and framework-free.
|
||||
- YAGNI: concern - two unused public placeholders remain.
|
||||
- SOC: concern - render config application and release artifacts mix concerns.
|
||||
- Cohesion: concern - player physics consumes unrelated level rendering data.
|
||||
- Coupling: fail - config/resources and projection/clipping rely on hidden cross-module rules.
|
||||
- Dependency Direction: pass - browser code points inward and engine remains DOM-free.
|
||||
- Law of Demeter: pass - shallow plain-data access; no traversal chains or service locators.
|
||||
- Immutability: concern - mutable preset aliases and zero-vector aliasing weaken boundaries.
|
||||
- Declarative Shape: concern - option unions and release phases are not exhaustively interpreted.
|
||||
- Implicit Contracts: fail - alpha, dimensions, pitch, clipping, checks, and publishing depend on rituals.
|
||||
- Abstraction Pressure: concern - important rules are under-centralized while stale exports remain.
|
||||
- Naming/API Clarity: fail - `Color.lerp`, `check`, `publish`, and `Sprite.texture` overpromise.
|
||||
- Locality: concern - NPC behavior and configuration changes require cross-file coordination.
|
||||
- Testability: fail - zero tests, false-green test command, and broad player fixtures.
|
||||
- File/API Shape: concern - type/namespace pattern is strong; ignored and stale exports weaken it.
|
||||
- Predicate Accuracy: concern - binary fallback dispatch silently accepts future union variants.
|
||||
- Construction Phase Separation: concern - level and release construction each hide multiple phases.
|
||||
|
||||
**Strengths**
|
||||
|
||||
- Engine/browser dependency boundary is clean and enforced by separate TypeScript libraries.
|
||||
- Domain types generally own behavior through matching namespaces and matching filenames.
|
||||
- Hot-loop mutation is local, explicit, and appropriate for a software rasterizer.
|
||||
- Rasterizer, sky, texture loading, and procedural mesh generation remain cohesive despite numeric code.
|
||||
- `main.ts` is still a reasonable composition root; splitting presentation or FPS code now would add ceremony.
|
||||
- No ECS, service locator, class hierarchy, utility bag, dependency cycle, or speculative plugin system appeared.
|
||||
|
||||
**Validation**
|
||||
|
||||
- `bun run build`: passed.
|
||||
- `bunx tsc --build --dry`: selected engine, app, and config only.
|
||||
- `bun run lint`: exited with one `typescript(array-type)` warning at `scripts/gen-assets.ts:162`.
|
||||
- `bun run test`: found zero tests and emitted Bun's tsconfig directory-mismatch internal error; `--pass-with-no-tests` kept the command green.
|
||||
|
||||
**Verdict**
|
||||
|
||||
Refactor soon. Keep the core architecture. Fix lying primitives and quality
|
||||
commands first, then centralize configuration, input, and collision contracts.
|
||||
714
.agents/plans/gpu.md
Normal file
714
.agents/plans/gpu.md
Normal file
|
|
@ -0,0 +1,714 @@
|
|||
# GPU renderer migration direction note
|
||||
|
||||
Status: **options open; migration seam agreed.** This note records the current
|
||||
problem, viable approaches, expected scaling, proposed architecture, migration
|
||||
order, and decisions still needed. It is not approval to delete the software
|
||||
renderer or to commit to WebGL2, WebGPU rasterization, or WebGPU compute.
|
||||
|
||||
## Goal
|
||||
|
||||
Move triangle transformation and pixel rasterization off the CPU so adding more
|
||||
world geometry and moving actors does not collapse frame rate.
|
||||
|
||||
The GPU path must preserve the configurable PS1 look rather than replace it with
|
||||
a fixed visual preset. `RenderConfig` remains the source of live settings for
|
||||
internal resolution, vertex snap, texture filtering, lighting, fog, color depth,
|
||||
dither, and LOD distance.
|
||||
|
||||
The current software renderer remains playable during migration. A GPU backend
|
||||
is added beside it and selected at startup or by a debug option until parity and
|
||||
performance are proven.
|
||||
|
||||
## Decisions already made
|
||||
|
||||
- Build a parallel backend instead of replacing the software renderer in place.
|
||||
- Keep engine world/content boundaries unchanged. GPU work must not reintroduce
|
||||
content kinds, registries, or renderer knowledge of frogs, trees, texture names,
|
||||
or other game content.
|
||||
- Keep `RenderScene` as the clone-safe, content-agnostic render projection of a
|
||||
live `Level`.
|
||||
- Keep main-thread frustum culling and scene-local render prototype indexes at
|
||||
first. GPU culling is not required to obtain the expected performance gain.
|
||||
- Optimize for scaling as content grows, not merely for improving the current
|
||||
benchmark by a small constant factor.
|
||||
- Preserve low-resolution rendering and the live PS1 controls.
|
||||
|
||||
## Decisions not made
|
||||
|
||||
- WebGPU versus WebGL2 as the first GPU backend.
|
||||
- Fixed-function GPU rasterization versus a custom WebGPU compute rasterizer.
|
||||
- Whether WebGL2 and WebGPU should both exist long-term.
|
||||
- Whether the software renderer remains a permanent fallback after a GPU backend
|
||||
becomes default.
|
||||
- Whether GPU output must be pixel-identical to software output or only visually
|
||||
equivalent under the same `RenderConfig`.
|
||||
- Required browser, OS, and device support.
|
||||
- Whether GPU timing statistics are required for the first usable backend.
|
||||
|
||||
## Current baseline
|
||||
|
||||
Current final browser benchmark at 384x216:
|
||||
|
||||
| Backend | Work median | Work p95 | Frame p95 |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Software, single thread | 30.07 ms | 32.19 ms | 32.60 ms |
|
||||
| Software, 3 workers | 11.16 ms | 13.35 ms | 16.88 ms |
|
||||
|
||||
These numbers are machine- and scene-specific. They are useful as the baseline
|
||||
for this repository, not as universal predictions.
|
||||
|
||||
The software cost grows with transformed triangles, covered pixels, and texture
|
||||
samples. Workers divide framebuffer rows, but every worker still loops over the
|
||||
same visible draw groups and transforms the same triangles for its band. More
|
||||
content therefore keeps increasing CPU work even when screen resolution stays
|
||||
fixed.
|
||||
|
||||
## Remaining implementation options
|
||||
|
||||
### Option A: standard WebGPU rasterization
|
||||
|
||||
Use WebGPU render pipelines, vertex/index buffers, textures, samplers, a depth
|
||||
texture, actor instancing, and WGSL shaders. Recreate PS1 traits in shaders and a
|
||||
post-process pass.
|
||||
|
||||
Strengths:
|
||||
|
||||
- Uses hardware raster units for the work they are designed to perform.
|
||||
- Best route to large geometry and actor-count headroom.
|
||||
- Explicit resource and command model fits immutable compiled `RenderScene` data.
|
||||
- Storage buffers and compute remain available later without changing API.
|
||||
- Device-loss handling can fall back to the software backend.
|
||||
|
||||
Costs:
|
||||
|
||||
- More setup and resource-lifecycle code than WebGL2.
|
||||
- Uniform/storage alignment and bind-group layout require care.
|
||||
- Texture upload row pitch must satisfy WebGPU alignment requirements.
|
||||
- Browser/device support must be checked against actual release targets.
|
||||
- GPU work timing needs optional timestamp queries or coarse CPU submission timing.
|
||||
|
||||
Expected scaling:
|
||||
|
||||
Current geometry should normally fit in low single-digit GPU milliseconds on a
|
||||
reasonable desktop GPU. At this low internal resolution, 50k triangles is small
|
||||
for fixed-function GPU rasterization. Hundreds of thousands of visible triangles
|
||||
should remain practical if draw count, state changes, and overdraw stay controlled.
|
||||
These are estimates, not acceptance results.
|
||||
|
||||
### Option B: standard WebGL2 rasterization
|
||||
|
||||
Use a WebGL2 context, VAOs, vertex/index buffers, textures, samplers, an offscreen
|
||||
framebuffer, a depth attachment, GLSL ES 3 shaders, and instanced actor draws.
|
||||
|
||||
Strengths:
|
||||
|
||||
- Simpler first standard-raster backend.
|
||||
- Broad and mature browser implementation history.
|
||||
- All current PS1 effects can be implemented without compute shaders.
|
||||
- Hardware triangle throughput should be enough for this engine by a wide margin.
|
||||
- Easier shader/program debugging in many browser tools.
|
||||
|
||||
Costs:
|
||||
|
||||
- More implicit global state than WebGPU.
|
||||
- Resource binding and synchronization are less explicit.
|
||||
- No general compute path if exact software rasterization becomes necessary.
|
||||
- Large dynamic instance data is less flexible than WebGPU storage buffers.
|
||||
- A later WebGPU backend would duplicate substantial platform code.
|
||||
|
||||
Expected scaling:
|
||||
|
||||
For this renderer, standard WebGL2 and standard WebGPU should have similar basic
|
||||
triangle throughput because both use hardware rasterization. JavaScript draw-call
|
||||
overhead, material changes, and actor submission are more likely to distinguish
|
||||
them than raw fill rate at 384x216.
|
||||
|
||||
### Option C: WebGPU compute software rasterizer
|
||||
|
||||
Port the custom rasterizer to WGSL compute rather than using render pipelines.
|
||||
This is the path with the best chance of preserving exact custom edge, depth, and
|
||||
pixel rules, but it does not automatically scale like hardware rasterization.
|
||||
|
||||
A serious implementation needs multiple stages:
|
||||
|
||||
1. Transform vertices and clip triangles.
|
||||
2. Compute screen bounds and assign triangles to 8x8 or 16x16 pixel tiles.
|
||||
3. Store per-tile triangle lists with overflow handling.
|
||||
4. Rasterize each tile in a workgroup, using workgroup memory where useful.
|
||||
5. Resolve depth, alpha cutout, texture sampling, lighting, and color.
|
||||
6. Run color quantization/dither and copy to the canvas.
|
||||
|
||||
Expected speed:
|
||||
|
||||
A properly tiled compute renderer could plausibly reduce the current scene to
|
||||
roughly 1-5 ms on a decent desktop GPU. A naive port can be slower than the current
|
||||
worker renderer. Exact performance depends on tile occupancy, atomics, overdraw,
|
||||
texture access, and hardware.
|
||||
|
||||
Scaling limitations:
|
||||
|
||||
- Vertex transformation and binning still grow with triangle count.
|
||||
- Dense tiles grow with local triangle count and overdraw.
|
||||
- Triangle-parallel writes contend on depth and color.
|
||||
- Pixel-parallel loops over every triangle are catastrophically expensive.
|
||||
- Correct alpha-cutout depth ordering complicates packed atomic updates.
|
||||
- Reproducing deterministic software ordering can serialize work.
|
||||
|
||||
Use this option when exact software-raster behavior is more important than maximum
|
||||
content headroom. It is not the recommended first answer to the current scaling
|
||||
problem, but it remains open.
|
||||
|
||||
### Option D: CPU rasterizer with GPU presentation
|
||||
|
||||
Keep software rendering and upload the completed CPU framebuffer into a GPU texture
|
||||
for presentation.
|
||||
|
||||
Strengths:
|
||||
|
||||
- Small migration step.
|
||||
- Preserves exact current pixels.
|
||||
- Can establish canvas, texture-upload, scaling, and backend-selection plumbing.
|
||||
- May simplify or replace Canvas2D presentation.
|
||||
|
||||
Limitations:
|
||||
|
||||
- Does not move triangle or pixel work off the CPU.
|
||||
- Does not solve frame-rate collapse as content grows.
|
||||
- Adds an upload every frame.
|
||||
|
||||
This remains useful as an intermediate bridge or diagnostic backend, not as the
|
||||
destination for the stated performance goal.
|
||||
|
||||
### Option E: both WebGPU and WebGL2
|
||||
|
||||
Implement a common backend contract, then supply WebGPU, WebGL2, and software
|
||||
implementations.
|
||||
|
||||
Strengths:
|
||||
|
||||
- Best runtime coverage while retaining modern WebGPU capabilities.
|
||||
- Allows direct performance comparison on the same machine and scene.
|
||||
- Software remains the reference renderer.
|
||||
|
||||
Costs:
|
||||
|
||||
- Three renderers, shader languages, resource systems, and failure paths.
|
||||
- PS1 feature fixes must be maintained in multiple implementations.
|
||||
- Highest test and debugging burden.
|
||||
|
||||
Do not start both GPU backends simultaneously unless supported-browser requirements
|
||||
make that necessary. The common seam should permit a second backend without making
|
||||
it mandatory.
|
||||
|
||||
## Current architecture that should survive
|
||||
|
||||
No game-content rewrite is needed.
|
||||
|
||||
- `engine/world/Level.ts` owns live actors, collision, terrain, and render projection.
|
||||
- `engine/render/RenderScene.ts` owns static groups, chunks, billboards, prototypes,
|
||||
sky configuration, culling, and scene-local instance data.
|
||||
- `engine/render/Chunk.ts` owns LOD choice.
|
||||
- `Mesh` already provides flat position/UV vertices and indexed triangles.
|
||||
- `Material` already binds a concrete `Texture` and cull setting.
|
||||
- `ChunkBuilder` already batches static geometry by material object.
|
||||
- Actor definitions already expose material-bound render prototypes.
|
||||
- Main-thread frustum culling already prevents invisible chunks and actors from
|
||||
reaching the renderer.
|
||||
|
||||
GPU compilation should consume those objects directly. Backend-local maps may
|
||||
deduplicate resources by `Mesh`, `Texture`, `Material`, or `RenderPrototype` object
|
||||
identity. Such maps are compiled-resource caches, not semantic content registries:
|
||||
they have no content names, stable IDs, or dispatch behavior.
|
||||
|
||||
## Proposed backend seam
|
||||
|
||||
Current `app/renderer.ts` exposes software-specific `fb` and `parallel` fields.
|
||||
Presentation also lives partly in `app/main.ts`. A GPU backend needs a backend-neutral
|
||||
surface.
|
||||
|
||||
Candidate frame input:
|
||||
|
||||
```ts
|
||||
export type RenderFrame = {
|
||||
camera: Camera
|
||||
viewProjection: Mat4
|
||||
visibleChunks: readonly number[]
|
||||
instances: readonly RenderInstance[]
|
||||
time: number
|
||||
}
|
||||
```
|
||||
|
||||
Candidate renderer contract:
|
||||
|
||||
```ts
|
||||
export type Renderer = {
|
||||
readonly width: number
|
||||
readonly height: number
|
||||
readonly backend: "software" | "webgl2" | "webgpu"
|
||||
reconfigure: (config: RenderConfig) => void
|
||||
dispatch: (frame: RenderFrame) => void
|
||||
done: () => boolean
|
||||
present: () => void
|
||||
workMs: () => number | null
|
||||
dispose: () => void
|
||||
}
|
||||
```
|
||||
|
||||
This is a candidate, not a final API. Important properties:
|
||||
|
||||
- Main loop no longer reads a software `Framebuffer` to discover dimensions.
|
||||
- Backend owns presentation. Software calls `putImageData`; WebGL/WebGPU finish a
|
||||
pass targeting the canvas.
|
||||
- Software `done()` polls workers; standard GPU backends can initially report true
|
||||
after command submission.
|
||||
- `workMs()` may be unavailable until GPU timing support exists.
|
||||
- Renderer owns teardown for workers, buffers, textures, and device/context loss.
|
||||
- Scene replacement or level reload recompiles backend resources explicitly.
|
||||
|
||||
## Module ownership options
|
||||
|
||||
Core engine must remain DOM-free and usable by the server.
|
||||
|
||||
Recommended initial split:
|
||||
|
||||
- `engine/render/RenderScene.ts` keeps backend-neutral scene and frame contracts.
|
||||
- `app/renderers/SoftwareRenderer.ts` wraps the current worker renderer.
|
||||
- `app/renderers/WebGpuRenderer.ts` or `app/renderers/WebGlRenderer.ts` owns browser
|
||||
API calls and canvas contexts.
|
||||
- `app/main.ts` selects a backend and uses only the common renderer contract.
|
||||
|
||||
If a GPU backend grows too large for `app/`, another viable option is a separate
|
||||
platform project such as `renderers/webgpu/` that depends on `engine/` but is not
|
||||
part of DOM-free engine core. Do not add DOM/WebGPU globals to `tsconfig.engine.json`
|
||||
merely to make placement convenient.
|
||||
|
||||
## Standard GPU resource compilation
|
||||
|
||||
Compile resources once per `RenderScene` or level load.
|
||||
|
||||
### Meshes
|
||||
|
||||
- Convert `Mesh.verts` into one `Float32Array` with stride 5: x, y, z, u, v.
|
||||
- Convert `Mesh.indices` into `Uint16Array` when safe or `Uint32Array` otherwise.
|
||||
- Create one backend mesh resource per distinct `Mesh` object.
|
||||
- Preserve separate chunk meshes; merging all chunks would defeat CPU culling.
|
||||
- Release resources when replacing the scene.
|
||||
|
||||
### Textures
|
||||
|
||||
- Upload existing packed RGBA bytes through a `Uint8Array` view.
|
||||
- Keep repeat wrapping.
|
||||
- Create nearest and linear samplers without mipmaps.
|
||||
- Handle WebGPU `bytesPerRow` alignment with padded staging rows when needed.
|
||||
- Deduplicate by `Texture` object identity.
|
||||
|
||||
### Materials
|
||||
|
||||
- Bind texture resources by `Material.texture` object identity.
|
||||
- Keep culling as finite renderer capability, not content identity.
|
||||
- Standard GPU paths will probably need two world pipeline variants: culled and
|
||||
double-sided.
|
||||
- Avoid one shader program or pipeline per game material.
|
||||
|
||||
### Prototypes and instances
|
||||
|
||||
- Compile each `RenderPrototype` once.
|
||||
- Group visible instances by prototype each frame.
|
||||
- Upload transform data once per frame.
|
||||
- Draw repeated actors with instancing rather than one command per actor.
|
||||
- Keep prototype indexes local to the compiled scene.
|
||||
|
||||
### Billboards
|
||||
|
||||
- Replace per-frame CPU quad construction with one static unit quad.
|
||||
- Supply position/size as instance data.
|
||||
- Derive camera-right direction in the vertex shader so billboards stay upright.
|
||||
|
||||
## Standard GPU frame structure
|
||||
|
||||
### Pass 1: scene color and depth
|
||||
|
||||
Render at `RenderConfig.internalWidth` x `RenderConfig.internalHeight` into an
|
||||
offscreen RGBA color texture plus depth attachment.
|
||||
|
||||
Suggested order:
|
||||
|
||||
1. Draw full-screen sky with depth writes disabled.
|
||||
2. Draw static groups.
|
||||
3. Draw visible chunks using CPU-selected near/far groups.
|
||||
4. Draw billboards with alpha cutout and no culling.
|
||||
5. Draw actor prototype instances.
|
||||
|
||||
### Pass 2: quantization and presentation
|
||||
|
||||
Sample the offscreen color texture and apply color-depth quantization plus Bayer
|
||||
dither in a full-screen pass. Write to the canvas texture. Keep CSS integer scaling
|
||||
and `image-rendering` behavior where useful.
|
||||
|
||||
This two-pass shape matches current behavior better than quantizing each material
|
||||
fragment independently because software quantization runs after the complete frame,
|
||||
including sky.
|
||||
|
||||
## Mapping current raster features to shaders
|
||||
|
||||
| Current feature | Standard GPU implementation | Parity risk |
|
||||
| --- | --- | --- |
|
||||
| Perspective-correct UV | Native interpolation | Low; this is already desired behavior |
|
||||
| Internal low resolution | Offscreen render target at configured size | Low |
|
||||
| Nearest/linear texture filter | Select sampler from `RenderConfig` | Low |
|
||||
| No mipmaps | Allocate only base level and use non-mipmap sampler | Low |
|
||||
| Vertex snap | Snap projected screen coordinates in vertex shader, then rebuild clip xy | Medium; edge rules differ |
|
||||
| Flat lighting | Fragment derivatives of world/view position, or baked face normals | Medium |
|
||||
| Distance fog | Fragment shader using view-space distance | Low |
|
||||
| Alpha cutout | Fragment `discard` below alpha threshold | Low |
|
||||
| Backface culling | Culled and double-sided pipeline variants | Low; front-face sign must be checked |
|
||||
| Color depth | Full-screen post-process quantization | Low |
|
||||
| Bayer dither | Full-screen integer pixel-coordinate lookup | Low |
|
||||
| 1/w depth behavior | Native perspective depth or explicit fragment depth | Medium; exact values differ |
|
||||
| Near clipping | Native clipping | Low visually, not pixel-identical |
|
||||
| Procedural sky | Full-screen sky shader port | Medium; noise parity must be tested |
|
||||
| Chunk frustum culling | Keep existing CPU `RenderScene.visibleChunks` | Low |
|
||||
| Actor culling | Keep existing `Level.visibleInstances` | Low |
|
||||
| Chunk LOD | Keep existing CPU `Chunk.isFar` selection | Low |
|
||||
|
||||
Vertex snap sketch:
|
||||
|
||||
```text
|
||||
clip = projection * view * model * position
|
||||
ndc = clip.xy / clip.w
|
||||
pixel = (ndc * 0.5 + 0.5) * internalResolution
|
||||
pixel = round(pixel / snap) * snap
|
||||
clip.xy = ((pixel / internalResolution) * 2.0 - 1.0) * clip.w
|
||||
```
|
||||
|
||||
Y orientation and half-pixel conventions differ between APIs and need screenshot
|
||||
tests. Do not guess front-face or snap signs from the software renderer.
|
||||
|
||||
## Flat-lighting options
|
||||
|
||||
### Fragment derivatives
|
||||
|
||||
Calculate a face normal from `cross(dpdx(position), dpdy(position))` in the fragment
|
||||
shader. This preserves flat faceted lighting without changing mesh data.
|
||||
|
||||
Pros:
|
||||
|
||||
- Smallest geometry change.
|
||||
- Naturally one normal per rasterized face.
|
||||
- Available in WebGL2 fragment shaders and WGSL fragment shaders.
|
||||
|
||||
Cons:
|
||||
|
||||
- Exact shade may differ from the CPU cross-product calculation.
|
||||
- Orientation and two-sided handling need validation.
|
||||
|
||||
### Baked face normals
|
||||
|
||||
Duplicate shared vertices per triangle and store one face normal per vertex.
|
||||
|
||||
Pros:
|
||||
|
||||
- Explicit and predictable.
|
||||
- Can reproduce CPU directional-light math closely.
|
||||
|
||||
Cons:
|
||||
|
||||
- Increases vertex memory.
|
||||
- Requires mesh compilation to expand indexed geometry.
|
||||
- Changes cache behavior.
|
||||
|
||||
Both options remain open. Derivatives are the recommended first implementation.
|
||||
|
||||
## WebGPU-specific notes
|
||||
|
||||
- Request adapter/device once and handle failed acquisition cleanly.
|
||||
- Configure the canvas using the preferred canvas format.
|
||||
- Use an offscreen `rgba8unorm` scene texture and `depth24plus` initially.
|
||||
- Keep frame uniforms in a uniform buffer.
|
||||
- Use dynamic uniform offsets, instance vertex attributes, or storage buffers for
|
||||
model transforms. Do not repeatedly overwrite one model uniform before submit.
|
||||
- Respect 256-byte dynamic-uniform alignment.
|
||||
- Group actors by prototype and use `instance_index` for transform lookup.
|
||||
- Keep bind-group layouts stable across materials.
|
||||
- Separate frame data, texture views, and samplers so changing texture filtering
|
||||
does not force rebuilding every texture resource.
|
||||
- Listen for device loss and switch to software rather than leaving a dead canvas.
|
||||
- Treat timestamp queries as optional capability.
|
||||
- Recreate size-dependent color/depth textures after `RenderConfig` resolution
|
||||
changes.
|
||||
|
||||
## WebGL2-specific notes
|
||||
|
||||
- Request `webgl2` with alpha and antialias settings chosen explicitly.
|
||||
- Use one VAO per compiled mesh.
|
||||
- Use an FBO with RGBA color texture and depth renderbuffer/texture.
|
||||
- Use GLSL ES 3 vertex/fragment programs for world, billboard, sky, and post passes.
|
||||
- Use `drawElementsInstanced` and per-instance matrix attributes for actors.
|
||||
- Use `dFdx`/`dFdy` fragment derivatives for flat lighting if selected.
|
||||
- Use `EXT_disjoint_timer_query_webgl2` only when available.
|
||||
- Handle context loss/restoration by releasing and recompiling scene resources.
|
||||
- Reset or centralize state changes; implicit stale state is a major WebGL failure
|
||||
mode.
|
||||
|
||||
## WebGPU compute-raster details
|
||||
|
||||
Avoid these naive designs:
|
||||
|
||||
- One compute invocation per pixel looping over every triangle.
|
||||
- One invocation per triangle writing non-atomic color and depth.
|
||||
- A global atomic lock per pixel around full shading.
|
||||
- Unbounded fixed-size tile lists with silent overflow.
|
||||
- Recreating CPU row-band splitting on the GPU.
|
||||
|
||||
Open design questions for compute:
|
||||
|
||||
- Tile size and maximum triangle references per tile.
|
||||
- Multi-pass prefix sums versus fixed tile capacity.
|
||||
- Depth encoding suitable for atomic comparison.
|
||||
- How alpha-cutout fragments update depth and color consistently.
|
||||
- Whether exact triangle order is required for equal-depth fragments.
|
||||
- Whether perspective-correct UV and texture filtering use native sampling.
|
||||
- How clipping-generated triangles enter tile lists.
|
||||
- Whether post-process quantization remains a separate render pass.
|
||||
|
||||
Do not implement compute first unless pixel parity is declared a hard requirement
|
||||
after testing a standard raster prototype.
|
||||
|
||||
## Draw-call and batching strategy
|
||||
|
||||
Current chunks are already grouped by material, which is suitable for GPU upload.
|
||||
Culling should remain more important than globally merging geometry.
|
||||
|
||||
Initial strategy:
|
||||
|
||||
- Draw each visible chunk group separately.
|
||||
- Skip invisible chunks on CPU.
|
||||
- Select near/far groups on CPU.
|
||||
- Sort or bucket visible groups by cull pipeline and material only if profiling
|
||||
shows draw-call overhead matters.
|
||||
- Instance actors by prototype.
|
||||
- Use one unit-quad mesh for billboards.
|
||||
|
||||
Possible later optimizations:
|
||||
|
||||
- Merge neighboring chunk groups only if visibility remains acceptably granular.
|
||||
- Multi-draw or indirect draws where API support and complexity justify them.
|
||||
- GPU frustum/occlusion culling only after CPU submission becomes measured cost.
|
||||
- Texture atlases or arrays only after material binding becomes measured cost.
|
||||
|
||||
Do not add batching abstractions before a profile identifies the bottleneck.
|
||||
|
||||
## Performance expectations and growth testing
|
||||
|
||||
Standard GPU rasterization is recommended for the content-scaling goal because
|
||||
hardware raster units remove the CPU per-triangle/per-pixel loop. However, GPU
|
||||
rendering can still lose frame rate through excessive draws, overdraw, huge texture
|
||||
uploads, per-frame allocation, or synchronization.
|
||||
|
||||
Benchmark growth levels, not only the current scene:
|
||||
|
||||
| Scenario | Purpose |
|
||||
| --- | --- |
|
||||
| 1x current content | Parity and baseline |
|
||||
| 2x static props | Early scaling slope |
|
||||
| 5x static props | Dense-world target |
|
||||
| 10x static props | Stress and culling behavior |
|
||||
| 2x/5x moving actors | Instance upload and draw scaling |
|
||||
| Worst forest camera | Fill, overdraw, and visible draw count |
|
||||
| Empty clearing | Fixed per-frame overhead |
|
||||
|
||||
Record:
|
||||
|
||||
- CPU simulation time.
|
||||
- CPU render submission time.
|
||||
- GPU time when available.
|
||||
- Median, p95, and max frame time.
|
||||
- Visible triangles, draw count, and visible actor count.
|
||||
- GPU memory and scene compilation time where practical.
|
||||
|
||||
Candidate budget, not yet agreed:
|
||||
|
||||
- Keep renderer p95 below roughly 10 ms at the chosen content target, leaving
|
||||
room inside a 16.67 ms frame for simulation, input, browser, and presentation.
|
||||
- Demonstrate a substantially flatter frame-time slope than software at 2x and 5x
|
||||
content.
|
||||
|
||||
## Migration sequence
|
||||
|
||||
### Phase 1: isolate backend contract
|
||||
|
||||
1. Introduce `RenderFrame` and backend-neutral renderer dimensions/lifecycle.
|
||||
2. Wrap current software renderer without changing output.
|
||||
3. Move Canvas2D presentation behind the software backend.
|
||||
4. Keep `?bench=st|mt` working.
|
||||
5. Add explicit backend query selection such as `?renderer=software|webgl2|webgpu`.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Software screenshots and benchmark remain unchanged.
|
||||
- Preset switching, resize, worker fallback, and presentation still work.
|
||||
|
||||
### Phase 2: GPU scene compiler
|
||||
|
||||
1. Traverse `RenderScene` once.
|
||||
2. Deduplicate meshes, textures, materials, and prototypes by object identity.
|
||||
3. Upload immutable resources.
|
||||
4. Record compile time and resource counts.
|
||||
5. Add disposal and level-reload behavior.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Compiling a scene does not mutate engine scene data.
|
||||
- No content names or registries enter backend code.
|
||||
- Repeated object references produce one GPU resource.
|
||||
|
||||
### Phase 3: minimum standard-raster world
|
||||
|
||||
1. Render static groups and visible chunk groups.
|
||||
2. Add depth testing and material culling.
|
||||
3. Add texture sampling.
|
||||
4. Keep CPU culling and LOD.
|
||||
5. Use a simple clear color before sky parity exists.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Geometry, UVs, winding, depth, and near/far LOD are visibly correct.
|
||||
- Current scene already shows major software-to-GPU work-time reduction.
|
||||
|
||||
### Phase 4: actors and billboards
|
||||
|
||||
1. Compile render prototypes.
|
||||
2. Group visible actors by prototype.
|
||||
3. Upload per-frame transforms.
|
||||
4. Draw actors with instancing.
|
||||
5. Draw billboards from a shared unit quad.
|
||||
6. Add alpha cutout.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- No actor content dispatch exists in backend.
|
||||
- Worker scene-local prototype semantics remain backend-neutral.
|
||||
- Dynamic actor count scaling is measured.
|
||||
|
||||
### Phase 5: PS1 look parity
|
||||
|
||||
1. Implement vertex snap.
|
||||
2. Implement nearest/linear sampler selection.
|
||||
3. Implement flat lighting.
|
||||
4. Implement fog.
|
||||
5. Implement color-depth quantization and Bayer dither in post-processing.
|
||||
6. Validate `standard`, `soft`, `clean`, and `ps1` presets.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Every current `RenderConfig` knob works live.
|
||||
- No PS1 trait is silently hardcoded into GPU shaders.
|
||||
|
||||
### Phase 6: sky parity
|
||||
|
||||
1. Port gradient and sun.
|
||||
2. Port skybox sampling.
|
||||
3. Port basic cumulus.
|
||||
4. Port fancy cumulus.
|
||||
5. Preserve reduced cloud sampling where still useful.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- Sky animation and panorama orientation match visually.
|
||||
- Cloud cost is profiled separately at each internal resolution.
|
||||
|
||||
### Phase 7: reliability and selection
|
||||
|
||||
1. Handle WebGPU device loss or WebGL context loss.
|
||||
2. Fall back to software when GPU initialization fails.
|
||||
3. Verify preset switching and resize during active rendering.
|
||||
4. Add screenshot comparison and growth benchmarks.
|
||||
5. Decide default backend only from support and measured results.
|
||||
|
||||
## Validation plan
|
||||
|
||||
### Visual scenes
|
||||
|
||||
Capture stable views that expose different failure modes:
|
||||
|
||||
- Room edge crossing near plane.
|
||||
- Terrain horizon with fog.
|
||||
- Dense trees and boulders at LOD transition.
|
||||
- Flowers and sprite alpha cutout.
|
||||
- Frog/bee/robin close and far.
|
||||
- Skybox poles and seam.
|
||||
- Basic and fancy clouds.
|
||||
- Backface-culling winding checks.
|
||||
|
||||
### Comparison policy
|
||||
|
||||
If standard GPU rasterization is selected, require visual equivalence rather than
|
||||
bit-identical frames unless exact parity is later declared. Native clipping, depth,
|
||||
edge fill, derivatives, and floating-point behavior will differ.
|
||||
|
||||
If compute rasterization is selected specifically for exactness, define which
|
||||
parts must match bit-for-bit before implementation. "Exact" cannot remain vague.
|
||||
|
||||
### Automated checks
|
||||
|
||||
- GPU scene compiler imports no game modules.
|
||||
- Renderer backends import no concrete game modules.
|
||||
- GPU compilation creates no stable content IDs.
|
||||
- Software tests continue passing.
|
||||
- Shader compilation errors fail backend initialization clearly.
|
||||
- Scene replacement releases old resources.
|
||||
- Config switch and resize recreate only size/config-dependent resources.
|
||||
- Device/context loss reaches a working fallback.
|
||||
|
||||
## Backend choice guide
|
||||
|
||||
Choose standard WebGPU first when:
|
||||
|
||||
- Target browsers/devices support WebGPU adequately.
|
||||
- Future compute or storage-buffer work matters.
|
||||
- Explicit resource management is worth the larger initial implementation.
|
||||
- Software fallback is acceptable where WebGPU is absent.
|
||||
|
||||
Choose WebGL2 first when:
|
||||
|
||||
- Broad browser/device coverage is the priority.
|
||||
- Fastest path to hardware rasterization is the priority.
|
||||
- Current feature set is enough and compute is not required.
|
||||
- Maintaining software fallback covers correctness/reference needs.
|
||||
|
||||
Choose WebGPU compute first only when:
|
||||
|
||||
- Exact custom raster behavior is a hard product requirement.
|
||||
- Standard GPU raster tests prove shader emulation insufficient.
|
||||
- The project accepts a tile-binning/atomic rasterizer as a major subsystem.
|
||||
- WebGPU-only support is acceptable.
|
||||
|
||||
Choose both standard GPU APIs only when:
|
||||
|
||||
- Browser support requirements cannot be met by one GPU API plus software fallback.
|
||||
- The maintenance cost is explicitly accepted.
|
||||
|
||||
## Current recommendation
|
||||
|
||||
Start with the backend seam, then prototype **standard WebGPU rasterization** while
|
||||
keeping software fully operational. Standard WebGL2 remains a valid first backend
|
||||
if target-browser research favors it. Measure one textured static world pass before
|
||||
porting every visual feature.
|
||||
|
||||
Do not begin with compute unless exact edge/depth behavior is declared mandatory.
|
||||
For the stated problem, standard hardware rasterization offers the strongest chance
|
||||
of adding substantially more content without frame time growing like the current
|
||||
CPU rasterizer.
|
||||
|
||||
## Questions required before implementation
|
||||
|
||||
1. Which browsers, OS versions, and device classes must run the GPU path?
|
||||
2. Is software-renderer pixel identity required, or is visual equivalence enough?
|
||||
3. What content-growth target must hold 60 fps: 2x, 5x, 10x, or a concrete level?
|
||||
4. Is the software backend a permanent supported mode or only migration/reference?
|
||||
5. Is GPU timing required in the HUD for the first version?
|
||||
6. Should backend selection be automatic, query-driven, or user-configurable?
|
||||
7. Is WebGPU compute still desired after a standard-raster visual prototype exists?
|
||||
|
||||
Until these are answered, keep all options above open and avoid API choices that
|
||||
make WebGL2, WebGPU rasterization, or WebGPU compute unnecessarily impossible.
|
||||
|
|
@ -1,9 +1,18 @@
|
|||
# Levels & the in-game editor — direction note
|
||||
|
||||
Status: **direction agreed, not yet built.** Captures a brainstorm so the next
|
||||
Status: **historical proposal, partially superseded.** Captures a brainstorm so the next
|
||||
session starts from the conclusion, not a cold read. Nothing here is committed
|
||||
code; it's the shape we want and *why*.
|
||||
|
||||
> **Architecture note (2026-08-20):** Naming and ownership sections below are
|
||||
> superseded by `docs/adr/0001-engine-owns-world-concepts.md` and `CONTEXT.md`.
|
||||
> Canonical terms are now engine `LevelDefinition` (live-level creation input),
|
||||
> engine `Level` (live runtime), and clone-safe engine `RenderScene`; game owns
|
||||
> concrete definitions and values. A future serializable editor document needs its
|
||||
> own name rather than overloading these runtime terms. References below to game-owned world contracts, content
|
||||
> `kind` fields/registries, `game/Terrain.ts`, or `game/renderScene.ts` describe the
|
||||
> old code only. Editor goals remain valid but must use the canonical boundary.
|
||||
|
||||
## The pivot
|
||||
|
||||
The game is heading toward a **multiplayer twitch shooter**. Each match loads a
|
||||
|
|
@ -20,7 +29,7 @@ The game is heading toward a **multiplayer twitch shooter**. Each match loads a
|
|||
- **Persistence and player carry-over are YAGNI** for now — but must not be walled
|
||||
off (see hedges).
|
||||
|
||||
## Naming: `Level` (data) vs `Scene` (runtime)
|
||||
## Historical naming proposal: `Level` (data) vs `Scene` (runtime)
|
||||
|
||||
Agreed vocabulary — reads as **"bake a `Level` into a `Scene`."** Fits the
|
||||
type + namespace convention.
|
||||
|
|
@ -210,7 +219,7 @@ field = mutate the document + re-bake.
|
|||
- **No hardcoded buffer sizes** tied to today's world — mob/framebuffer sizing already
|
||||
re-runs on `setup`; keep it so odd-shaped match maps just work.
|
||||
|
||||
## Current-state facts the next session will need
|
||||
## Historical current-state facts (before 2026-08-20)
|
||||
|
||||
- `buildLevel(textures): Level` (`game/level.ts:175`) is the single entry, called once
|
||||
in `app/main.ts`. Today's `Level` (`game/level.ts:69-81`) is a **bake result**
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
---
|
||||
description: Local vs sync mode rules for git and Forgejo — commit, push, and PR policy per workflow.
|
||||
---
|
||||
|
||||
# Forge Sync Rules
|
||||
|
||||
When a workflow uses explicit mode:
|
||||
|
||||
- `Mode: sync` or `--sync` means sync mode.
|
||||
- no sync marker means local mode.
|
||||
- Do not ask whether git workflow should be agent-managed.
|
||||
|
||||
Local mode:
|
||||
- No push.
|
||||
- No forge write tools.
|
||||
- No PR create/update/comment.
|
||||
|
||||
Sync mode:
|
||||
- Commit completed workflow units.
|
||||
- Push after each unit commit.
|
||||
- Create PR after first push if missing.
|
||||
- Reuse existing PR on later pushes.
|
||||
- Comment/update PR only when workflow asks for it.
|
||||
|
||||
Forbidden always:
|
||||
- No force push.
|
||||
- No branch deletion.
|
||||
- No hard reset.
|
||||
- No amending unless explicitly requested.
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
---
|
||||
description: Rules for OpenSpec propose and archive workflows.
|
||||
---
|
||||
|
||||
# OpenSpec Instructions
|
||||
|
||||
## OpenSpec Propose Workflow
|
||||
|
||||
When proposing changes via `/opsx-propose`:
|
||||
|
||||
- If the change introduces or modifies user-facing behavior (API changes, conventions), include a task section for updating `README.md` in the proposal's impact assessment.
|
||||
|
||||
## OpenSpec Archive Workflow
|
||||
|
||||
When archiving tasks via `/opsx-archive`:
|
||||
|
||||
1. Automatically sync specs, do not ask the user.
|
||||
2. If mode is explicit, use `forge-sync` for archive commit, push, PR, and Forgejo behavior.
|
||||
3. If no mode is explicit, default to local mode.
|
||||
Loading…
Add table
Add a link
Reference in a new issue