GL frontend experiment: hardware-rendered xsofy + level-2 fall into first person - #140
GL frontend experiment: hardware-rendered xsofy + level-2 fall into first person#140nnunley wants to merge 16 commits into
Conversation
…nsition, first-person)
…cale/translation, look-at z sign; reinstate + strengthen tests
Implement grid.lg: the adapter that converts xsofy's rendered glyph grid (cells with character, fg/bg colors) into vertex batches for GL rendering. Public API: - cell-quad: Convert a glyph cell to textured vertex quad (54 floats) - cell-bg-quad: Convert cell background to untextured quad - grid-verts: Batch cells into bg and glyph vertex vectors - wall-extrude-verts: Extrude a cell into 4 CCW-wound wall faces (216 floats) Coordinate system: cell (col, row) at world x = col*cell-w, y = -row*cell-h, z = 0. Cell width defaults to 0.5, height to 1.0; walls extrude upward (+z). Colors normalized from [0-255] to [0,1]; glyphs fallback to "?" if missing. Tests (grid_test.lg): 15 tests covering float counts, vertex positions/UVs/colors, alpha-zero filtering, fallback logic, batch separation, and wall geometry.
… camera; fix wall height test axis
…able
GLYPH INVENTORY: collected from terrain, entities, items, runes (140 total glyphs).
- assets/gl/runes.txt: UTF-8 string of all glyphs (ASCII 0x20-0x7E + Unicode runes)
- Baked with lgatlas (Monaco.ttf): 128x256 texture, 16x16 grid (cell 8x16)
- assets/gl/atlas.{png,edn}: texture and UV lookup table
RENDER BRIDGE (xsofy/gl/bridge.lg):
- Reuses render.lg terrain/entity composition logic without modification
- Extracts grid cells as {:col :row :glyph :fg :bg} per cell
- Applies: lighting (light/apply-light), stains (fx/stain-tint-*), gases (fx/gas-tint)
- FOV/memory/unseen cells handled: visible cells lit, remembered cells dimmed, unseen black
MAIN LOOP (main_gl.lg):
- Init GL window via glplat, load atlas texture
- Build game state via build-game (skips title screen)
- Frame loop: PollInputEvents → parse glplat strings → dispatch actions → bridge → grid-verts → submit
- Input mapping: "key:w" → "w" → input/parse-key → :unknown | "key:up" → "[A" → :up
- Ortho projection: 0–39.5 (79*0.5) on x, 0–30 on z; draw bg (tex 0) then glyphs (atlas tex)
- Auto-exit via XSOFY_GL_AUTOEXIT env var (seconds); XSOFY_GL_DUMP flag logs first frame vertex counts
VERIFICATION:
- Test suite: 362 tests, 2880 assertions, all PASS ✓
- GL version: 2-second run, 2370 cells rendered, 14220 verts/batch, lighting visible ✓
- Batch output: bg=[0 0 0 1.0] (untextured), glyph=[uvs] in atlas
NOTES FOR NEXT WAVE (fall+first-person):
- Screenfx tint flash (if used on level 2) will be a full-screen quad post-submit
- no audio calls stubbed; sound system may need wrapping at loop level
- Wall extrusion ready in grid.lg; fall.lg morph parametrization ready
- Determinism preserved: no graphics side effects on seed/action-log
…lyph stub), read-string number parsing (env parsers silently failed -> infinite loop), os/exit after -main (GLFW keeps process alive), print instead of io flush! on *out*
Implements the centerpiece of GL frontend experiment: level-2 entry triggers
playfield-to-floor transition (2.5s morph), followed by first-person mode.
LEVEL-CHANGE DETECTION:
- Track depth (:depth in world state) before/after dispatch
- On depth increase (detected in advance-pres-state), trigger fall animation
- Presentation-side state machine: :top-down → :falling → :fp (game state untouched)
- XSOFY_GL_FORCE_FALL env hook: start animation N seconds after boot for testing
MORPH RENDERING (:falling mode, 2.5s duration):
- fall/morph provides: plane-tilt-rad, wall-height, camera {eye/target/up}, projection-blend
- Component-wise MVP lerp: ortho → perspective as projection-blend (t: 0→1)
- Plane tilt: rotate-x about player pivot, composed into model matrix
- Wall extrusion: wall-extrude-verts for all wall cells (tiles 8, 9) with dynamic height
FIRST-PERSON MODE (:fp):
- Perspective projection: ~70° FOV, camera 0.5 units above player, facing heading
- Dungeon-crawler input remapping: (up/k=forward, down/j=back, left/h=turn-left,
right/l=turn-right in place, turn changes presentation-side facing only)
- Billboards: entity glyphs as upright quads facing camera (using yaw from facing)
- Wall boxes: same extrusion as morph but fixed 1.0 unit height
- Geometry culling: only render visible cells (FOV + memory + non-black cells)
DUMP METRICS (:falling only for now; can extend to :fp):
- Per-frame at t≈0, t≈0.5, t≈1: mode, t, camera eye/target, batch vertex counts
- Sanity checks: wall-batch non-empty on maps with walls; camera eye y descends
- Exit message includes final mode
fpview.lg: new namespace for FP view logic:
- remap-fp-input: input → action + facing change (turn-only flag for presentation)
- facing-to-angle: yaw from cardinal direction (for billboards)
- fp-camera-pose: camera {eye/target/up} from player pos + heading
- entity-billboard: upright glyph quad at entity cell, colored from entity visual
- Wall tile predicate: tiles 8, 9 (can extend if needed)
Test suite: 362 tests, 2880 assertions, all pass. Game logic untouched.
…op double model-tilt, 70-degree fovy (was 7), fall.lg camera in world units, force-fall no longer re-arms t=0 every frame
…eal descend fall trigger - main_gl: advance-pres-state args were swapped (compared old>new depth) so a real stairs descent never triggered the fall; only XSOFY_GL_FORCE_FALL worked - main_gl: auto-modes (autoexplore/rest/run/auto-target) now step every 60ms between inputs, mirroring play.lg's tick loop; keypress interrupts - gl/vfx.lg: tick + overlay quads for :projectile/:bolt/:flash world vfx, lifted y=0.02 over the floor; wired into all three render modes - gl/hud.lg: screen-space HUD (status line, HP bar, last 3 log messages) drawn in a second pass with near=0 ortho so it lands at NDC depth -1 (always on top under LEQUAL); fixed nested-vector corruption in hud-verts solids batch - title.lg: show-descending-screen no-ops under XSOFY_GL_FRONTEND (set by main_gl) — it painted runes into the launching terminal mid-descent Tests: 409 tests, 2968 pass, 0 fail
…sigil overlap - assets/gl: atlas rebaked from Fairfax HD (the web shell's one true face, tools/fonts/.cache) — 159 glyphs, everything the sources emit incl. the box-drawing projectile glyphs (─│╱╲) whose absence made projectiles invisible, plus full runic coverage (Monaco lacked 33 glyphs; terminal only looked right via macOS font fallback) - tools/bake_atlas.lg: baking policy now lives in lg (layout, glyph list, EDN/PNG writes) over four minimal glplat primitives (FontLoad, FontHasGlyph, FontRasterizeCell, SaveGlyphAtlasPNG); assets/gl/fonts.edn is the font manifest; replaces let-go's cmd/lgatlas Go CLI - gl/hud.lg: spell-rune glyphs render 2 cells wide with 1-cell advance so consecutive runes overlap into a composite sigil — reproduces the native terminal look (halfwidth grid + wide fallback glyphs), which Fairfax (strictly halfwidth, measured) does not produce on its own - tools/xsofy-shell.html: regenerated by gen-terminal-font.py (font fetch) Tests: 410 tests, 2972 pass, 0 fail
- glplat/Screenshot (glReadPixels → PNG, pre-swap) + XSOFY_GL_SHOT / XSOFY_GL_SHOT_AT env hooks in main_gl so frames can be inspected headlessly - fonts.edn cell 8x16 → 12x24: Fairfax HD is a bitmap-style face with a native 12x24 grid; at 16px every stroke straddled pixels and antialiased to faint hairlines (screenshot-diagnosed unreadable HUD) - bake_atlas.lg: binarize alpha (>=96 → solid) for terminal-crisp glyphs; atlas now 256x512, zero partial-alpha pixels Verified by screenshots: top-down and mid-fall frames, HUD/messages legible
wall-extrude-verts takes :fg (0-255) and shades per face (south 1.0, north 0.8, x-faces 0.65); build-wall-batch feeds it the bridge cells' lit style colors, so extruded walls follow the palette and lighting instead of rendering fullbright white. Screenshot-verified mid-fall. Tests: 411 tests, 2980 pass, 0 fail
Unset XSOFY_GL_SHOT read back as "" (truthy), so 1s in the loop called
Screenshot("") which threw; with no catch, the process exited 0 silently
— looked like a clean quit. getenv-str helper (unset/empty → nil) now
guards all env reads; the frame loop prints exceptions instead of dying
silently; title.lg's XSOFY_GL_FRONTEND guard tests count, not nil (it was
always-on, wrongly suppressing the terminal descending screen too).
Verified: plain run alive 15s+; AUTOEXIT/SHOT/DUMP paths still work.
Tests: 411, 2980 pass, 0 fail
- main loop now drives xsofy.ui/run-loop: glplat-input-source ([:key ev], pumps GLFW) + ui/clock-source, pure gl-step fold, gl-render subscriber. Same tick-vs-input contract as the terminal: sources polled input-first so a keypress cancels auto-mode before the tick's auto-step; cosmetic ticks never enter the deterministic action stream. frame-ms 0 — vsync paces via EndFrame. - :title presentation mode (default entry): scattered scheme-tinted runes pulsing via screenfx/rune-brightness at the terminal's 120ms frame pace, title fade-in, 'Press any key', seed line; any key enters gameplay. XSOFY_GL_SKIP_TITLE=1 for scripted runs. - fall trigger also checked at key-dispatch time (depth increase), not just per-tick Screenshot-verified title; force-fall runs reach :fp and exit on time. Tests: 411, 2980 pass, 0 fail
let-go now ships poll!/offer!/alts! (nooga/let-go#194), so the chan-source adapter deferred in the #56 spec is buildable: producer goroutines push events into a channel; the source drains it non-blockingly via async/poll! (capped 16/tick like input-source). The loop contract is unchanged — sources still never block, so WASM's single thread never parks the frame loop; goroutines under Go-wasm park/wake through the JS event loop. Stale 'deferred until #194' notes updated. Tests cover empty/order/splice/ flood-cap and a real (go …) producer. Tests: 416, 2988 pass, 0 fail
|
We rebased this branch onto current The headline: culling cells that are neither in FOV nor remembered takes the top-down frame from ~400ms to ~18ms at seed 42, and the captured frame is byte-identical before and after. Also included are frame timing behind Independent of that PR: the test suite hangs on this branch's current base. It reaches |
Experiment branch (draft): the whole game rendered through a minimal OpenGL platform layer, with entering depth 2 tipping the playfield into the floor of a 3D world and continuing in first person.
Runs on the let-go
glplatbranch build (nooga/let-go#392; async parity in nooga/let-go#391):What's in the branch
main_gl.lgon the unified poll-based event loop (xsofy.ui, Responsive UI event loop: composable, replay-safe input + cosmetic animation #56): glplat input source + clock source, pure step fold, render subscriber — same tick-vs-input contract as the terminal, so cosmetic animation never touches the deterministic (seed, action-log) stream.xsofy/gl/: mat4, grid adapter (floor quads + extruded walls tinted from the lit tile palette with per-face shading), bridge (FOV/lighting/stains/gases), fall morph, fpview, vfx overlays (projectiles/bolts/flashes), HUD (status line, HP bar, log tail; spell-rune glyphs render 2-cells-wide with 1-cell advance so inscriptions overlap into composite sigils, matching the native-terminal look).tools/bake_atlas.lg+assets/gl/fonts.ednbake Fairfax HD (the web shell's single face) at its native 12x24 grid with binarized alpha;ui/chan-sourceadded for push-based producers now that let-go has poll!.Suite green throughout (416 tests, 2988 pass). Known gaps: no browser/WebGL backend yet, screenfx tint flash stub, no smooth FP camera lerp, no FOV culling of submitted geometry.