Quake UI rewrite to use gogpu/ui #17
Replies: 2 comments
|
Working on a v4 after doing some more research. Will be pushing to |
RESEARCH-0008 follow-up: the 4th rewrite (v4) — what we learned, and how it changes the pictureStatus: Follow-up reply to RESEARCH-0008 (the three-branch evaluation) 1. tl;drThe three-branch saga's central claim — "the embedding seam is the unsupported, But v4 also landed on a humbling conclusion: after four attempts, the engine 2. What v4 changed (the architecture)
The three structural decisions that made v4 different:
3. The new negative finding: GPU-direct composite is still brokenThis is the most important thing v4 learned, and it refines RESEARCH-0008's 3.1 v2's Vulkan presentation crashes — now root-causedRESEARCH-0008 §3.2 listed v2's "Vulkan command buffers referenced The gg GPU-direct flush ( cmdBuf, err := encoder.Finish() // (1) finishes THE SHARED GOGPU FRAME ENCODER
s.queue.Submit(cmdBuf) // (2) gg submits the encoder itself
s.prevCmdBufs = append(..., cmdBuf) // (3) retains it, frees at NEXT frame's BeginFrame
This is exactly v2's failure, and it's in the gg layer, not the app. The 3.2 The accelerator is a trap for the readback pathv4 also discovered the gg SDF accelerator and the CPU-readback composite are
So the accelerator import had to be removed entirely. The "single-pass GPU 4. What v4 confirms from the first three
5. What v4 adds to the recommendationsRESEARCH-0008 §5.3's list stands, with one addition and one reframing:
6. Where v4 stands now
The honest bottom line after four attempts: the widget toolkit is not the |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
RESEARCH-0008: Three gogpu/ui Rewrite Branches — Evaluation & Feedback
gogpu discussion #468 (https://github.com/orgs/gogpu/discussions/468)
1. Context
ironwail-go (a pure-Go,
CGO_ENABLED=0port of the Ironwail Quake engine ongogpu/WebGPU) ran three successive experimental branches rewriting its
hand-rolled UI (menu, dropdown console, HUD) on top of gogpu/ui. Each branch
took a different integration architecture, each had its own bugs and
tradeoffs, and each was superseded by the next. This document is the
deep-dive evaluation of all three approaches, framed as the validation-case
feedback the gogpu org explicitly invited in discussion #468.
The three branches:
experiment/ui-rewrite(v1)RenderFrameexperiment/ui-rewrite-v2(v2)desktop.Runowns the loop; the 3D world rendered into acore/gpuviewtexture the compositor blits under the UIexperiment/ui-rewrite-v3(v3, current)gg.Context, then blitted to the swapchain via a custom WGSL overlay-composite pass withLoadOpLoad2. The three approaches at a glance
RenderFramedesktop.Runowns the loop; world rendered into acore/gpuviewtexturegg.Context, blitted via a custom HAL composite passgfx/*.lmppicsgfx/*.lmppicsinternal/game(UIHost, gateway, CSQC fallback)HostadapterHostadaptergpucontext.EventSourcegateway shimapp.HandleEventKeyForwarderapp.HandleEventKeyForwarderdesktop.Runblocks)3. What each branch got right and wrong
v1 — the "in-loop bridge" (Architecture A+C)
Strengths that carried forward: the engine-owned loop (WASM/headless
compatible), the
ui_backendcvar gate for A/B comparison, the legacymenu.Manageraccessor surface, and the layeredStacksurface model. Theinput gateway (a full
gpucontext.EventSourceshim with pointer/scroll/IME)was the most complete input solution of the three.
Failures that killed it:
spike found
GlobalFontRegistrylives in an internal package and isunimportable; the public
plugin.AssetLoader.LoadFontregisterer cannotreach it either. gogpu/ui has no public bitmap-font path, so a retro
game either hand-rolls per-glyph draws or accepts non-native text.
manage
ggcanvas.Canvaslifecycle (BeginAcceleratorFrame,BeginGPUFrame,ResetFrameDamage,MarkDirty,Render) and resize —exactly the plumbing that breaks on the first framework change. The v2
ADR records it "failed: canvas sizing fought the framework, the composite
could clear the world, and input invalidation was fragile."
syncing, input raw sinks, and CSQC fallback all lived in game code.
v2 — the "desktop.Run + GPUView" (Architecture B)
Strengths: real visual fidelity (conchars + LMP pics), clean isolation via
a
Hostadapter (world texture exposed as agpucontext.TextureView), andthe C-lineage 320x200 menu transform math. This was the #468-endorsed BYO-kit
composition path exactly as the maintainer proposed it.
Failures that killed it:
desktop.Runinverted application control. The engine lost ownershipof the frame loop. The world had to render into an offscreen
gpuviewtexture, which required retargeting the entire scene-target pipeline
(world/entities/waterwarp/polyblend).
during window resize and widget invalidation.
presentation failures. The deepest technical failure of the three — a
GPU-lifetime bug, not a design preference.
desktop.Runblocks; therequestAnimationFramepath is incompatible), so v2 was native-desktop-only.
v3 — the "engine-owned overlay + HAL composite" (current)
The synthesis: it keeps v1's engine-owned loop (WASM/headless work again)
and v2's visual fidelity and clean adapter. The novel part: widgets draw into
a reusable CPU
gg.Context→render.NewCanvas→dc.Image(), and therenderer's
DrawRGBAuploads that as one texture and composites it with ahand-written WGSL overlay-composite pipeline using
LoadOpLoadon the currentswapchain view.
Difficulties it hit (the most fix-commits of any branch):
implemented. The spec/ADR claim
FlushGPUWithViewPreserveContentis themechanism, but the code never calls it — it does a CPU
gg.Contextrastera finding.
a helper uses reflection +
unsafeto poke gogpu's internalframeCleared/hasPendingClearfields to forceLoadOpLoad. That is amaintenance landmine — it breaks on any gogpu internal rename.
shader UV flipping.
gg.Contextproduces straight alpha but thecomposite pipeline's blend state had to be tuned (premultiplied vs
straight) to match.
to split one "any callback disables polling" gate into three separate
gates (key / mouse-button / mouse-move) because the old heuristic
double-delivered keys under the new path. A regression in the engine's
core input backend caused by the UI experiment.
to
event.Key, which changed menu-toggle behavior.imports
internal/renderer(itsDrawOverlaytakes a renderer type), andthe import-closure test only forbids
internal/game, silently dropping theinternal/renderercheck that v2's test enforced.4. The pattern across all three branches
Every branch converged on the same core widget layer (the menu/console/HUD
widgets are near-identical between v2 and v3 — v3 is v2's widget tree with
desktop.Run/WorldTexturedeleted and anOverlayRendereradded). Thechurn was entirely in the integration seam, and each iteration was a
reaction to the previous seam's failure:
The root cause: gogpu/ui's only first-class embedding path is
desktop.Run, which is a whole-application ownership model. There is nosupported "render my widget tree into a texture I already own, on my own
loop" path. Every branch had to hand-build that seam, and each hand-build
touched gogpu internals (reflection on internal frame state, the
externalTextureWidget/DrawGPUTexturecompositor path, the single-slotOnDraw/EventSource).5. What this means for discussion #468
5.1 Feedback the experiment validates (evidence-backed)
The BYO-kit path works. All three branches used
app+widget+custom
widget.WidgetBasewidgets with zerocore/*imports; Godead-code elimination kept the binary lean. The org's "full custom kit"
scenario is real and viable.
The Painter pattern is not what a game actually uses. All three
branches implemented custom widgets on
widget.WidgetBase, notpainters over stock widgets. Quake's UI is bespoke enough that there is no
stock widget to paint — the "custom look + behavior" row of the VEE table
is the one that matters, and it needs a good custom-widget authoring path
(sdk/), not more painters.
The Canvas gap table was validated precisely. The
image.RGBA.SubImageworkaround for sprite atlases (status bar faces/weapons, 9-patch boxes)
works exactly as predicted but forces callers to manage atlas lifecycle.
ImageRegionDrawer(orDrawImageSrcRect) is confirmed the single mostvaluable Canvas addition for game UIs. The conchars atlas is 128x128
with 256 sub-rects; the status bar alone does dozens of sub-rect draws per
frame.
The
AdvancedCanvas.GGContext()escape hatch is real and used. Thewhole overlay draw relies on the concrete canvas's
Context()— but it'sthe only escape hatch, and it bypasses Canvas state. A first-class
AdvancedCanvaswould be cleaner.The bitmap-font gap is the biggest unaddressed one.
GlobalFontRegistryis unimportable (internal package) and the public
plugin.AssetLoaderregisterer can't reach it. A public font-registration path (or a
bitmap-strike API) is a concrete, blocking ask — it's why v1 failed on
text and why v2/v3 had to hand-roll per-glyph atlas draws instead of using
the text system.
desktop.Runownership is the sharpest edge. The single-slotOnDraw/EventSourceconflict forced every branch into a bespoke seam.The org should document/standardize the standalone-engine path (render
the widget tree into a caller-owned texture on the caller's loop) — this is
what games and embedded apps hit first, and it's currently undocumented.
GPUView needs a non-desktop blit path. The
gpuviewwidget is onlycomposited by
desktop.Run's layer tree; a standalone engine must blitthe texture itself. v2 hit this directly.
5.2 The honest negative findings the org should hear
Every branch ended up scheduling an animation frame + forcing a redraw
every frame for the menu (animated cursor) and HUD (per-frame state) —
i.e. the damage/repaint optimization is bypassed entirely for a game. The
per-boundary
SceneCache/PictureLayer machinery (a #468 selling point) isdead weight for this workload.
ThemeExtensionMerge/Lerp/CopyWithcontract was implemented but isessentially unused by the widgets — Quake colors are palette indices read
directly, not semantic theme tokens. The semantic
ColorSchemeassumptionin the Painter pattern is a poor fit for palette-indexed games.
had to write its own shader + pipeline + bind-group to blit a UI texture
onto the swapchain (because the framework's own 2D path couldn't preserve
the world reliably) suggests the 2D-over-3D composite path needs
first-class support, not a per-app workaround.
5.3 Concrete recommendations for the org
retro or custom-font game. Without it, text is the first thing that
breaks.
ImageRegionDrawer/DrawImageSrcRect. Highest-value Canvasaddition; removes the SubImage boilerplate all three branches carried.
caller-owned texture, caller loop) and the GPUView non-desktop blit. This
is the #468 "kernel boundary" question from a game's perspective — the
answer is "the seam needs to be a supported contract, not app code."
AdvancedCanvas.GGContext()as a documented Tier-2 escape hatch(already proposed in #468) — confirmed needed.
semantic
ColorSchemeassumption doesn't map.Continuous/AlwaysDirtywidget mode so games don'thave to fight the retained-mode invalidation to get per-frame redraws.
6. Where the branches stand now
desktop.Run/gpuviewarchitecture isabandoned.
surfaces render with real art, tests green, WASM works), but it carries
real debt: the reflection/unsafe poke at gogpu internals, the renderer
import in the UI package (isolation boundary silently weakened), the
input-backend regression risk, and the doc/code drift on the "single-pass
GPU flush."
The single most useful thing the three-branch saga proves for #468: the
widget toolkit itself is fine (BYO-kit, custom widgets, atlas draws all work),
but the embedding seam — how a non-
desktop.Runhost renders a widgettree over its own 3D content — is the unsupported, undocumented,
repeatedly-reinvented part. That's where the org should invest before v1.0: a
supported standalone-render contract, a public font path, and the source-rect
Canvas API.
All reactions