diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index 8921529a..a196b2b9 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -463,6 +463,7 @@ D644,FIXED,File-select royal crest drew on an opaque dark square (#146):,"FIXED D645,FIXED,"Original 1.1: F10 ""Invert look"" did not invert vertical look (#145 a):",FIXED 2026-10-12 (F10 row and watch row synced in Original; maintainer-verified) D646,FIXED,"F10 dropdown popup drew over the hint/tooltip panel (popup bounded by the screen edge, not the settings box) —",FIXED 2026-10-12 (ddGeom bounded to the box; maintainer-verified) D951,OPEN,"v0.6.0 release gate: Windows golden sweep flags are timing noise in the test, not rendering changes —",OPEN (test-side; 2026-10-13) +D952,FIXED,"macOS: black window, no frame, when the game starts while the display is asleep (SDL3 Cocoa VSync waits forever on a CVDisplayLink that never started) —","FIXED 2026-10-13 (port-only, gfx_sdl2.cpp; Mac A/B:" D607,OPEN,Address model unified into one header:,DONE 2026-10-07 (macos branch; Windows identity gate: D608,FIXED,D34 ANIM_DATA_x re-basing guard in assets/animationtable_data.h was __x86_64__-only:,"FIXED 2026-10-07 (macos branch; mechanical guard widening, arm64 link clean)" D609,FIXED,macOS arm64 SIGKILL at exec:,"FIXED 2026-10-07 (macos branch; probe-confirmed; arm64 keeps the 4 GiB pagezero, floor 11.0)" diff --git a/docs/dev/findings.md b/docs/dev/findings.md index e2e11e63..022201d4 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -874,6 +874,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D645 | **Original 1.1: F10 "Invert look" did not invert vertical look (#145 a): D605 flipped the move stick; the game's own Look up/down drives C-button/aim pitch** — full `## D645` entry at file tail | FIXED 2026-10-12 (F10 row and watch row synced in Original; maintainer-verified) | | D646 | **F10 dropdown popup drew over the hint/tooltip panel (popup bounded by the screen edge, not the settings box)** — full `## D646` entry at file tail | FIXED 2026-10-12 (`ddGeom` bounded to the box; maintainer-verified) | | D951 | **v0.6.0 release gate: Windows golden sweep flags are timing noise in the test, not rendering changes** — full `## D951` entry at file tail | OPEN (test-side; 2026-10-13) | +| D952 | **macOS: black window, no frame, when the game starts while the display is asleep (SDL3 Cocoa VSync waits forever on a CVDisplayLink that never started)** — full `## D952` entry at file tail | FIXED 2026-10-13 (port-only, `gfx_sdl2.cpp`; Mac A/B: unfixed 3/3 hang, fixed 0/5) | Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS threads, compiled GE's real `src/sched.c`, and brought in PD's fast3d software @@ -21999,3 +22000,17 @@ FpsCap = 60'`: 20/21 PASS; statue REGRESSION (4.66%, frame 900). **Owed (test side only).** (1) verify.sh should pin `FpsCap = 60` in its ini itself, since the goldens are 60 fps captures; (2) a flagged level should be re-run once before it counts as a regression, or the first run should be discarded as warm-up. No game-code change. **Status.** OPEN, test-side. + +## D952: macOS first-start black window = SDL3 VSync waiting on a CVDisplayLink that never started (display asleep) — FIXED 2026-10-13 + +**Symptom.** "If the window stays black on the very first start, close it and start it again" (v0.6.0 known issue): 4 hangs on 10-12 and 2 of 13 cold first launches on 10-13, always the first launch after the Mac had sat idle (which is when a new CI build arrives, so it looked like "first launch of a new binary"). + +**Evidence.** Hung run (`kernel heartbeat: no frame rendered`, frames=0) sampled with `sample`: the D481 render worker sits in `portRenderGfxTask -> Cocoa_GL_SwapWindow -> SDL_WaitConditionTimeoutNS -> __psynch_cvwait`; the scheduler, `amMain` (`src/audi.c` reply wait) and `bossMainloop` only wait behind it. The process has NO `CVDisplayLink` thread; a healthy run has one. SDL 3.4.14 (`SDL_cocoaopengl.m`): with a swap interval set, `Cocoa_GL_SwapWindow` waits on a condition that only the display-link callback signals, with no timeout; the link is created and started once, in `initWithFormat`, from the active displays. + +**Trigger (deterministic).** `pmset displaysleepnow`, then launch: unfixed download 3/3 hang. Re-signing the binary (new cdhash) with the display awake: 0/10. The "new binary" correlation was the idle gap before it. + +**Fix (`port/fast3d/gfx_sdl2.cpp`, `__APPLE__` only).** SDL's swap interval stays 0, so SDL's swap never waits; VSync is set on the context with `CGLSetParameter(kCGLCPSwapInterval)` (as SDL2 did on macOS), which blocks in `flushBuffer` on the driver's own refresh. `get_swap_interval` returns the CGL value, so D578/D600 pacing sees the same interval as before. Logs `SDL: swap interval N via CGL (read back N)`. + +**Verified (Mac M3, macOS 15.6.1, fix built with the CI recipe, run against the download's own SDL 3.4.14 dylibs).** Display asleep: fixed 0/5 hang. Display awake: 60.0 presents/s as before; VSync on/off read back 1/0 from CGL. 21-level sweep (mac_sweep.sh, fixed binary): 21/21 OK. Windows build clean; Windows/Linux code paths unchanged (`#else` = the old lines). + +**Status.** FIXED. diff --git a/docs/porting-notes.md b/docs/porting-notes.md index c64364ad..ff91d645 100644 --- a/docs/porting-notes.md +++ b/docs/porting-notes.md @@ -676,6 +676,8 @@ through a converter or a runtime bswap fixup reads scrambled. - **CPU-built RDP triangles need float vertices, not `Vtx` (D245).** Where GE builds RDP edge/texture coefficients on the CPU (sky/water), 32-bit S/T and positions are normal; squeezing them through s16 `tc`/`ob` costs visible precision on horizon-scale geometry. Use `G_FLOATVTX_EXT` (`port/include/floatvtx.h`). - **A first-run-only preset is skipped forever by any earlier launch that created the ini, and window keys are not an "untouched" signal (D283).** Seeding values *before* `configLoad()` only works when no `ge007.ini` exists; one launch in a different environment (SteamOS Desktop Mode has no `STEAMOS`) writes a plain ini and the preset never runs. Apply presets *after* the load, record a one-shot flag key, and gate on keys the user must change deliberately (`Video.Fullscreen`, `Window.Maximized`) — `Window.Width/Height` are rewritten from the live window on every clean exit (`videoSaveWindowState`), so "still at default" never holds for them. +**SDL3 on macOS: a GL swap with VSync can wait forever (D952).** SDL3's Cocoa backend implements the GL swap interval itself: `SDL_GL_SwapWindow` waits, with no timeout, for a tick from a `CVDisplayLink` that SDL creates and starts once, with the context. If that link never runs (seen when the game starts while the display is asleep), the first swap never returns: black window, zero frames, every other thread waiting behind the render thread. Tells: `sample` shows `Cocoa_GL_SwapWindow -> SDL_WaitCondition*` on the render thread and the process has no `CVDisplayLink` thread. Fix pattern: leave SDL's interval at 0 and set `kCGLCPSwapInterval` on the context with `CGLSetParameter`, which waits in the driver (SDL2's behaviour). Reproduce with `pmset displaysleepnow` before launching. + ## D. N64 hardware idioms fast3d does not emulate - **Appending a port-owned 2D overlay to the game frame: hook inside diff --git a/port/fast3d/gfx_sdl2.cpp b/port/fast3d/gfx_sdl2.cpp index a7c42b86..f56499fb 100644 --- a/port/fast3d/gfx_sdl2.cpp +++ b/port/fast3d/gfx_sdl2.cpp @@ -9,6 +9,9 @@ #include "gfx_screen_config.h" #include "input.h" #include "ge007_icon.h" +#if defined(__APPLE__) +#include /* D952: CGL swap interval */ +#endif extern "C" void videoRequestQuit(const char *why); // port/src/video.c (D344) extern "C" void inputRequestRescan(void); // port/src/input.c (D450) @@ -18,6 +21,34 @@ static SDL_GLContext ctx; static SDL_Renderer* renderer; static int sdl_to_lus_table[512]; static bool vsync_enabled = true; + +/* D952: macOS swap interval through CGL, not SDL. SDL3's Cocoa GL VSync makes + * SDL_GL_SwapWindow wait, with no timeout, for a tick from the CVDisplayLink it + * starts when the context is created. On some first launches of a never-seen + * binary that display link never starts (no CVDisplayLink thread in the hung + * process), so the first swap blocks forever: no frame, a black window. SDL's + * own interval stays 0 (its swap never waits) and CGL's kCGLCPSwapInterval does + * the VSync inside flushBuffer, as SDL2 did on macOS. Must be called with the + * context current (both callers are: init on the host thread, live config on + * the render thread). */ +#if defined(__APPLE__) +static int s_cglSwapInterval = 0; +static bool gfx_sdl_apply_swap_interval(int interval) { + SDL_GL_SetSwapInterval(0); + CGLContextObj cgl = CGLGetCurrentContext(); + GLint v = interval ? 1 : 0; + if (!cgl || CGLSetParameter(cgl, kCGLCPSwapInterval, &v) != kCGLNoError) { + return false; + } + s_cglSwapInterval = v; + { + GLint back = -1; + CGLGetParameter(cgl, kCGLCPSwapInterval, &back); + sysLogPrintf(LOG_INFO, "SDL: swap interval %d via CGL (read back %d)", (int)v, (int)back); + } + return true; +} +#endif // OTRTODO: These are redundant. Info can be queried from SDL. static int window_width = DESIRED_SCREEN_WIDTH; static int window_height = DESIRED_SCREEN_HEIGHT; @@ -267,7 +298,11 @@ static void gfx_sdl_init(const struct GfxWindowInitSettings *set) { } SDL_GL_MakeCurrent(wnd, ctx); +#if defined(__APPLE__) + gfx_sdl_apply_swap_interval(1); /* D952 */ +#else SDL_GL_SetSwapInterval(1); +#endif // D550: window / taskbar icon = the project mark (generated by // tools_pc/make_icon.py into ge007_icon.h; R,G,B,A byte order per @@ -631,11 +666,19 @@ static void gfx_sdl_set_window_title(const char *title) { } static int gfx_sdl_get_swap_interval(void) { +#if defined(__APPLE__) + return s_cglSwapInterval; /* D952 */ +#else return SDL_GL_GetSwapInterval(); +#endif } static bool gfx_sdl_set_swap_interval(int interval) { +#if defined(__APPLE__) + const bool success = gfx_sdl_apply_swap_interval(interval); /* D952 */ +#else const bool success = SDL_GL_SetSwapInterval(interval) >= 0; +#endif vsync_enabled = success && (interval != 0); if (!success) { sysLogPrintf(LOG_WARNING, "SDL: failed to set vsync %d: %s", interval, SDL_GetError());