Skip to content

Render360/iphone baseline lowmem - #5

Open
matthewcodergamer wants to merge 214 commits into
weliveinhell:masterfrom
matthewcodergamer:render360/iphone-baseline-lowmem
Open

matthewcodergamer wants to merge 214 commits into
weliveinhell:masterfrom
matthewcodergamer:render360/iphone-baseline-lowmem

Conversation

@matthewcodergamer

Copy link
Copy Markdown

No description provided.

matthewcodergamer and others added 30 commits October 8, 2026 18:59
…ne-lowmem

Web: Portal-themed start page; glassier, brighter controller
Every control's position and size is measured from the reference layout
(iPhone 11 landscape, 896x350 CSS px below Safari's bar) and placed by its
centre as a fraction of the page, scaling with the screen. Same shapes:
pill shoulders, triggers and Pause/Back/Start, oval face buttons, a
rounded rectangular Move pad with chevrons; same dark tinted fills
(amber, gray, navy, green, red) with hairline borders and 9px labels. The
fills are translucent so the game shows through. The Move pad maps
direction against its own width and height since it is not round.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qy5DKdwTqGQqScWvJyNcft
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qy5DKdwTqGQqScWvJyNcft
…ne-lowmem

Web: on-screen controller matches the reference pad one-to-one
Audio: SDL's Emscripten driver runs the audio callback on the browser's
main thread, which never instantiates the engine side module, so Portal
was silent. On the web build snd_dev_sdl.cpp now skips SDL audio, keeps
mixing into its usual ring buffer, and posts the ring's address, size,
m_readPos and m_pauseCount to the page ('callHandler', since main-thread
EM_ASM cannot run side-module JS). An AudioWorklet reads the ring straight
from shared Wasm memory, resamples if the output rate differs, and
advances m_readPos as SDL's callback did. iOS: the first tap resumes the
AudioContext, navigator.audioSession is 'playback' so the ringer switch
does not mute it, and it suspends while the tab is hidden.
+snd_mute_losefocus 0, since the canvas rarely holds DOM focus.

Controller: positions are anchored to thumb zones and the safe area
(measured in the parent page, since env() is 0 inside the iframe):
Move pad bottom-left with LT above, Y/X/B/A level with it bottom-right
with RT above, Pause / Back+Start / LB+RB along the top, Back and Start
flanking the ••• pill. Mirrored margins; sizes and shapes unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qy5DKdwTqGQqScWvJyNcft
…ne-lowmem

Web: game audio through an AudioWorklet; thumb-zone controller layout
Text: system-font text (menu, console, HUD) still drew as solid boxes on
iPhone, and custom fonts such as the Portal title showed a black box
behind white letters: the font pages reach the screen without their
alpha. linuxfont.cpp filled every glyph pixel with white RGB and put the
shape only in alpha, so with alpha lost each glyph became a box. On the
web build it now writes coverage into RGB as well, as Win32Font does, so
letters stay readable and additive fonts blend correctly. Fallback fonts
are also looked up at the absolute /platform/resource/linux_fonts path
pre.js writes them to, independent of the mounted search paths.

Diagnostics keep a separate list of error/font/audio lines so the cause
of a text or sound problem is not pushed out of the 40-line tail.

Full screen: while the game runs the staging page can scroll behind the
fixed game layer, and every landscape rotation scrolls it so Safari
collapses its address bar.

Audio: +volume 0.7 by default; full scale is harsh on a phone speaker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qy5DKdwTqGQqScWvJyNcft
…ne-lowmem

iPhone: readable vgui text, auto-collapse Safari bars, softer audio
When a GLSL program failed to link, CGLMShaderPair::SetProgramPair called
glGetShaderiv(GL_INFO_LOG_LENGTH) on the *program* id. Emscripten serves
that through getShaderInfoLog(GL.shaders[id]), so WebGL received something
that is not a shader and threw - the iPhone crash "Argument 1 ('shader')
to WebGL2RenderingContext.getShaderInfoLog must be an instance of
WebGLShader". The program info log is now read with glGetProgramInfoLog
only, and both link and compile failures log one bounded line with the
shader names and the driver's message instead of dumping both full shader
sources through Msg.

Programs failing to link on iPhone also fits the menu text drawing as
boxes; the new lines land in the diagnostics' error list so the driver's
reason is visible on the next run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qy5DKdwTqGQqScWvJyNcft
New Game: the dialog lists chapters with FindFirst("cfg/chapter*.cfg").
Directory listings come from the main thread's shared FS, which only had
gameinfo and VPK placeholders; the user's loose cfg/scripts/resource/maps
files were linked on the Source worker only. The chapter list was empty
and "Start new game" had nothing to start. post.js now adds zero-byte
names for those loose files too; opens and stats are still intercepted by
filesystem_stdio and served from the real File.

Text: the font page reaches the screen with alpha read as opaque, so each
glyph drew on a black box. Glyph coverage is already in RGB on the web
build, so the procedural font materials now blend additively there; the
box disappears whether or not alpha survives. Bitmap fonts keep alpha
blending.

Audio: the AudioWorklet kept reading the ring even when the engine had not
painted new samples, replaying stale contents - the buzz. The engine now
publishes the sample time it has written (m_paintedEnd) and the reader
never plays past it, outputting silence instead. snd_mixahead 0.25 gives
slower phone frames more headroom inside the 0.37 s ring.

Rendering: mat_bumpmap/mat_specular stay on; turning them off made Portal
materials request a $bumpmap texture that was never loaded (red console
errors), and the crash they were meant to avoid was the togles link-log
bug.

Controller: works on the menus. Menu mode (background map, pause menu):
stick sends arrow keys with auto-repeat, A/Start/X Enter, B/Back Escape,
LB/RB left/right. Game mode once a real map loads; Pause toggles between
the two. It stays visible on the menus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qy5DKdwTqGQqScWvJyNcft
…pad fades

New Game now reaches testchmb_a_00, but ~75 s into loading the level the
engine thread died with an out-of-bounds access inside the main module's
low-numbered runtime functions (allocator range) - the signature of heap
memory overwritten earlier. Level load is where Source recurses deepest
(BSP and physics collision building), and under PROXY_TO_PTHREAD the
engine thread gets STACK_SIZE, which was 4 MB against Linux's 8 MB. A
silent stack overflow into the heap fits. STACK_SIZE is now 16 MB.

To make the next crash, if any, conclusive: modules and the main module
are linked with --profiling-funcs so Safari's Wasm stacks carry function
names (the CI ban on it is lifted while crashes are traced), and the
worker crash stack is kept whole (40 frames, 3000 chars) instead of being
cut at 300 characters.

Controller: fades to 16% opacity after 4 s without a touch and comes back
on any touch; never fades while a control is held.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qy5DKdwTqGQqScWvJyNcft
With function names in the build, the iPhone crash finally resolved to:
CInputSystem::FingerEvent <- TouchSDLWatcher <- SDL_PushEvent <-
SDL_SendTouch <- Emscripten_HandleTouch. The earlier "~30 s" out-of-bounds
crashes had the same frame indices: they were the player's taps.

FingerEvent used SDL's finger id directly as the index into
m_touchAccumX/Y and only checked the upper bound. SDL ids are arbitrary
64-bit values; Android gives 0,1,2..., but in a browser they are the DOM
Touch.identifier, which on iPhone Safari is large. Truncated to int it is
often negative, so every tap wrote outside the arrays.

TouchSDLWatcher now assigns each live finger a slot 0..9 on down/motion
and frees it on up; FingerEvent and GetTouchAccumulators reject anything
outside 0..TOUCH_FINGER_MAX_COUNT-1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qy5DKdwTqGQqScWvJyNcft
…olors)

New Game now loads testchmb_a_00 and renders the chamber on iPhone, but
props drew with green/blue garbage and the frame crashed in
CMeshDX8::SetColorStreamState <- CMeshDX8::Draw <- R_StudioDrawGroupHWSkin
<- DrawModelStaticProp <- CStaticPropMgr::DrawStaticProps: the static
prop's color mesh pointer was invalid.

Static prop vertex lighting (.vhv) is read through the async filesystem
into DataCache-managed, pooled color meshes; in the browser build props
were drawn with color meshes that were not valid yet or already released.
On the web build r_proplightingfromdisk now defaults to 0, so no color
meshes are created and props use regular model lighting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qy5DKdwTqGQqScWvJyNcft
The game rendered the menu and the first chamber at 640x480 (the device log
said BackBufWidth 640) and the page stretched that 4:3 picture over the
2.16:1 iPhone screen: zoomed, horizontally squashed 3D and oversized menu
text, unlike real Portal's wide view.

FindVideoMode only honours a custom size if it equals the size requested
when the window was created and the game is windowed; anything else falls
back to the display mode list, and for a size not in that list, to mode 0
(640x480). A browser canvas has no mode list, so on the web build it now
always returns the requested size. ResetCurrentModeForNewResolution logs
the chosen mode ([Render360 video]) into the diagnostics error list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qy5DKdwTqGQqScWvJyNcft
…n web

WebGL can't map GPU buffers. Emscripten's emulation hands back fresh,
uninitialized malloc memory on every map and uploads all of it on unmap,
which broke three paths in the browser:

- Lightmap pages are dynamic textures that stream through a mapped pixel
  buffer. Every re-lock of a page re-uploaded the whole page from garbage,
  wiping the surfaces written earlier: the world rendered blown-out white
  with cyan/green noise. On web, dynamic textures now keep a persistent CPU
  backing store (zero-initialized) and upload only the locked region.
- Static vertex/index buffer locks got uninitialized memory, so any bytes
  the engine didn't rewrite became garbage. On web the locked range is
  staged in CPU memory holding the buffer's current contents (read back
  with glGetBufferSubData when needed) and uploaded on unlock.
- Static prop color meshes shared one pooled vertex buffer that is locked
  for N vertices and then written past that range. Pooling is off on web,
  so every prop gets its own color mesh (green/cyan garbage on props).

Also:
- DXT decoding for tiny mips (2x2, 1x1) used the real width as the row
  stride and mistook 2x2 DXT5 for uncompressed data; decode into whole
  blocks and upload with the padded row length.
- The requested -w/-h was clamped to a fixed 640x480 "display mode" in
  the browser, so the game still ran stretched 4:3. Keep the requested size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qy5DKdwTqGQqScWvJyNcft
…y option

After a deploy Safari can load the start page before the updated service
worker controls it, so the page has no COOP/COEP headers, no
SharedArrayBuffer, and the engine dies with "The object can not be cloned"
on a black screen. The start page used to reload only once per tab; it now
reloads whenever isolation is missing (at most once every 20 s, so browsers
that can't isolate don't loop), and the game page does the same instead of
starting without threads.

Graphics quality (start page segmented control, and "Graphics" in the
in-game ••• menu, applied on next launch):
- Standard: 540p, quarter-size textures (previous behaviour, default)
- HD: 720p, half-size textures
- Max: full screen resolution, full-size textures
The chosen quality is included in Copy diagnostics.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qy5DKdwTqGQqScWvJyNcft
With the engine now asking for the page-sized mode (1168x540 on an
iPhone 11), CShaderDeviceMgrDx8::ValidateMode compared it with the GL
layer's fixed 640x480 "display mode" and refused it. SetMode failed,
CreateGameWindow returned false, ModInit failed and the engine exited
right after "[Render360 video] mode 1168x540" with RUN_OK and nothing on
screen. A browser canvas can be any size, so skip the windowed size check
on web.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qy5DKdwTqGQqScWvJyNcft
…game)

WebGL 2 has no glDrawRangeElementsBaseVertex, so the browser build's
DrawIndexedPrimitive computed a base-vertex-adjusted attribute setup in
FlushVertexBindings, but that setup is only latched (m_drawVertexSetup is
unused). FlushDrawStates rebuilt the GL vertex attribs from the bound
streams with a base vertex of 0, so every indexed draw with a non-zero
BaseVertexIndex read the wrong vertices: triangles stretched across the
screen, stair-stepped smears, blown-out white surfaces and saturated
blue/green/cyan from mismatched positions, texcoords and colors.

On web, pass BaseVertexIndex through to FlushDrawStates and start every
vertex attribute that many vertices into its stream; the base vertex is
part of the attrib-cache key so it is re-applied whenever it changes.
Native builds are unchanged (they pass it to glDrawRangeElementsBaseVertex).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qy5DKdwTqGQqScWvJyNcft
…tics

In game, moving the camera leaves stacked, ghosted copies of geometry and
colours run to white/blue/cyan, while still frames (menu, pause) look
right: something from earlier frames survives into later ones.

- Run with gl_clear 1, so the client clears the colour buffer at the
  start of every view and nothing from a previous frame can carry over.
- Add a GL probe in the render worker: it shadows the state the engine
  sets through WebGL and, for a few frames (5, 30, 120, 300, 600, then
  every 900), logs draws, blended draws and their factors, draws without
  depth test or depth writes, every clear with the scissor/mask state it
  ran under, framebuffer switches and glGetError, as "[Render360 GL]"
  lines in Copy diagnostics. It needs no GPU queries per draw.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qy5DKdwTqGQqScWvJyNcft
The probe showed the in-game trails are gone (frames are cleared), but a
GL error (0x500, INVALID_ENUM) every frame, lit world surfaces rendering
black, and a freeze after the first chamber loads. Extend it so the next
diagnostics pin all three down:

- Error hunt: for three short windows (frames 6-8, 200-203, 450-453)
  every WebGL call is followed by glGetError and the rejected calls are
  logged with their arguments. The wrappers are installed only for the
  window and removed after, so normal frames pay nothing.
- Texture uploads: the first texSubImage2D/texImage2D calls are logged
  with size, format, UNPACK_ROW_LENGTH and a sample of their bytes (mean,
  share of non-zero), to see whether lightmap data reaches WebGL.
- Heartbeat: an "alive" line with frame count and fps about every 10 s,
  so a freeze shows as the moment the lines stop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qy5DKdwTqGQqScWvJyNcft
…robe

The GL probe named the calls WebGL rejects in game:

- texImage2D(GL_RGB8 internal format, GL_RGBA data) -> INVALID_OPERATION.
  D3DFMT_X8R8G8B8 is 4-byte BGRX data kept in a 3-channel GL_RGB8 texture,
  a pairing GLES 3 / WebGL 2 do not allow, so every such texture (normal
  maps, the engine's 1x1 white/grey/black fallback textures) had no image
  and sampled black. On web X8R8G8B8 is reported unsupported, so the
  material system converts those images to R8G8B8 when it loads them;
  convert_texture also promotes RGB8 to RGBA8 whenever 4-channel data
  arrives, so no texture is left without an image.
- texParameter(GL_TEXTURE_BORDER_COLOR), the GL_CLAMP_TO_BORDER wrap mode
  and enable/disable(GL_FRAMEBUFFER_SRGB) -> INVALID_ENUM every frame.
  WebGL has none of them: skip the border colour, clamp to edge instead,
  and leave the sRGB write to the shader (as the web build already does).

Freeze: frames stopped about 12 s into the first chamber. The probe now
logs frames slower than 1.5 s with their draws, uploads and GPU buffer
read-backs, counts read-backs in the heartbeat, adds the framebuffer
status to INVALID_FRAMEBUFFER_OPERATION errors, and the page logs when
the render thread stops presenting frames and when it resumes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qy5DKdwTqGQqScWvJyNcft
…ndows

The probe showed clears and draws failing with framebuffer status 0x8CD9
(FRAMEBUFFER_INCOMPLETE_DIMENSIONS): WebGL 2 still requires every
attachment of a framebuffer to have the same size, while Source renders
into smaller targets (1024x512, 292x135, ...) with the full-screen
1168x540 depth buffer bound, as D3D9 allows. Everything drawn into those
framebuffers was lost. On web such a framebuffer now gets a private depth
buffer of the colour target's size (created once per size); those passes
only touch the top-left of the big depth buffer and clear depth anyway.

The menu also ran at 6-12 s per frame in the last run. Add profile windows
(frames 11-13, 60-63, 240-243, or up to a slow frame) that count WebGL
calls by name and time spent inside them versus the whole frame, so the
next diagnostics show whether the time goes to WebGL or to the engine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qy5DKdwTqGQqScWvJyNcft
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants