Skip to content

launcher: stb-based launcher revamp (parked — needs Phase-2b GL loader) - #13

Closed
mstan wants to merge 16 commits into
masterfrom
feat/launcher-stb-revamp
Closed

launcher: stb-based launcher revamp (parked — needs Phase-2b GL loader)#13
mstan wants to merge 16 commits into
masterfrom
feat/launcher-stb-revamp

Conversation

@mstan

@mstan mstan commented Jul 10, 2026

Copy link
Copy Markdown
Owner

What

Replaces the RmlUi launcher with a self-contained stb-based UI (stb_truetype rendering, no RmlUi dependency). Authored by @NyperYuhgard — the three commits here are their original work, cherry-picked onto current master to preserve it as a reviewable branch after master was reverted to RmlUi.

Why parked

Two blockers before this can land (see revert f36b458 on master):

  1. GL loader ("Phase 2b"): the launcher calls modern GL 3.0+ entry points directly without going through an SDL_GL_GetProcAddress loader, so launcher-ON game builds fail to link on MinGW.
  2. Visual polish: the rendered UI didn't pass review against the RmlUi baseline (2026-07-10) — font rendering and layout need another pass before it can replace the shipping launcher.

Master stays on the RmlUi launcher (the configuration every recent release shipped with) until both are addressed. This PR keeps the revamp alive and reviewable instead of buried in pre-revert history.

Credits

All launcher code in this PR: @NyperYuhgard (original author, preserved on the commits).

🤖 Generated with Claude Code

mstan added a commit that referenced this pull request Aug 4, 2026
Salvaged from PR #13 (feat/launcher-stb-revamp, commits 2ba52e9/a40d587b)
by NyperYuhgard:

- COP2 condition branches (BC2F/BC2T) recognized end-to-end: decoder
  mnemonics, control-flow analyzer (is_control_flow/analyze_instruction),
  and branch-condition codegen. Previously these fell through to the
  'unknown branch: never taken' default.
- MOVN/MOVZ (SPECIAL 0x0A/0x0B): decoded, classified, and translated.
  Previously silently no-op'd as TODO comments.
- Branch-likely (0x14-0x17) and REGIMM-likely decoder mnemonics.

Corrections to the original:
- BC2F/BC2T encoding: both are rs=0x08 with rt bit 0 selecting T/F;
  the original treated rs=0x09 as BC2T (undefined encoding).
- BC2 condition: the original tested invented GTE FLAG bits via
  gte_ctrl[31]. The PS1 GTE does not drive the COP2 condition input
  (psx-spx), so BC2F emits always-taken and BC2T never-taken.
- MOVZ emitted the same condition as MOVN; corrected to rt == 0.

Dropped from the original:
- The function_analysis.cpp DISCOVERY validity-table widening (accepting
  MOVN/MOVZ, branch-likely, REGIMM-likely rt values, addi prologues as
  valid MIPS words). These opcodes raise Reserved Instruction on a real
  R3000A, so no genuine PS1 code contains them; accepting them only
  admits DATA into discovery — measured on Tomba 1 (SCUS-94236): +9
  spurious dispatch entries, all data-as-code (movn/movz writing $zero).
  With this dropped, generated output is byte-identical to master for
  both SCPH1001 and Tomba 1 — the codegen additions are reachable only
  via seed-forced regions, as belt-and-braces over the TODO no-op.
- Enumerated REGIMM branch-likely condition cases (master's every-rt
  hardware decode already covers them) and the post-delay-slot
  branch-and-link write (master already links all four regimm link
  forms before the delay slot).

Co-Authored-By: NyperYuhgard <noreply@github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mstan added a commit that referenced this pull request Aug 4, 2026
…nert)

Salvaged from PR #13 (feat/launcher-stb-revamp, commits 2ba52e9/a40d587b)
by NyperYuhgard. What remains after measurement (see below):

- generate_branch_condition: BC2F/BC2T (opcode 0x12, rs=0x08, rt bit 0
  selects T/F). The PS1 GTE does not drive the COP2 condition input
  (psx-spx), so BC2F emits always-taken, BC2T never-taken.
- translate_instruction: MOVN/MOVZ (SPECIAL 0x0A/0x0B) translated
  instead of silently no-op'd as TODO comments; COP2 BC sub-op emits an
  explicit no-op marker in-block.
- mips_decoder: mnemonics/classification for MOVN/MOVZ, BC2F/BC2T,
  REGIMM-likely and branch-likely forms (diagnostic output only).

Corrections to the original: BC2T was mis-encoded as rs=0x09 (both are
rs=0x08); the BC2 condition tested invented GTE FLAG bits; MOVZ emitted
MOVN's condition (corrected to rt == 0).

Dropped from the original, with measurement:
- function_analysis validity-table widening and control_flow BC2
  branch classification. MOVN/MOVZ/branch-likely raise Reserved
  Instruction on a real R3000A, and no PS1 compiler emits them, so
  widening discovery only admits DATA: measured +9 spurious dispatch
  entries on Tomba 1 (SCUS-94236), all data-as-code (movn writing
  $zero), each one replacing a fail-closed psx_unknown_dispatch with
  compiled garbage.
- REGIMM 0x12/0x13 branch-and-link: R3000A links iff (rt&0x1E)==0x10,
  so rt 0x12/0x13 must NOT link; the original emitted phantom $ra
  writes in swept data regions.

With those dropped, generated output is byte-identical to master for
SCPH1001, OpenBIOS, and Tomba 1 — no cg-hash roll, no reshard. The kept
codegen is reachable only for seed-forced or future-discovered regions,
as belt-and-braces over the TODO no-op.

Co-Authored-By: NyperYuhgard <noreply@github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mstan pushed a commit that referenced this pull request Aug 5, 2026
…dd CFC0

Salvaged from PR #13 (feat/launcher-stb-revamp, commit 9fd2876) by
NyperYuhgard. Adds to the dirty-RAM interpreter:

- LoadAddressError/StoreAddressError delivery for misaligned LH/LW/LHU/
  SH/SW and misaligned JR/JALR targets, via a new interp_exception()
  that performs the architectural COP0 sequence (BadVAddr, Cause ExcCode,
  SR mode-stack push, EPC, BEV-selected vector).
- COP0 Cause write protection on MTC0/CTC0.
- CFC0/CTC0 aliases (identical to MFC0/MTC0 on R3000A).

Two deviations from the original:
- The original masked Cause writes to bits [15:10] (backwards vs
  hardware); corrected to the writable software-interrupt bits [9:8]
  per psx-spx / PCSX-Redux, which also un-deadens the psxTestSWInts
  recheck below it.
- The original's post-RFE immediate psx_check_interrupts recheck is
  dropped: master already delivers post-RFE interrupts through
  psx_rfe_mark_escape() and re-checking inside the handler window is
  the documented dbg-build boot-wedge livelock.

Co-Authored-By: NyperYuhgard <noreply@github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mstan added a commit that referenced this pull request Aug 5, 2026
Salvaged from PR #13 (feat/launcher-stb-revamp, commit 2ba52e9) by
NyperYuhgard. Master fatal-halts on any DMA start for channel 5; this
moves words to/from RAM directly (device side unemulated: RAM-to-device
reads-and-discards, device-to-RAM zero-fills) and completes the
transfer with the normal IRQ path.

Note: zero-filling from an unemulated expansion device is weaker than
the fail-closed psx_fatal_halt default the other channels follow; kept
as submitted because the contributor exercised it on a real title.
Trimmed from the original hunk: unused DMA_PIO_CYCLES_PER_WORD macro
and mdec_finish_async_{in,out}_transfer wrappers nothing references.

Co-Authored-By: NyperYuhgard <noreply@github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mstan added a commit that referenced this pull request Aug 5, 2026
Salvaged from PR #13 (feat/launcher-stb-revamp, commit 2ba52e9) by
NyperYuhgard. Fills the gaps the file header listed as not modeled:

- PS1-style reverb delay lines processing the final mix
- noise generator (LFSR) with per-voice noise mode and the noise-level
  register pacing steps
- per-voice sweep volume envelopes (volume reg bit 15) alongside the
  existing direct mode
- pitch modulation (PMON) of a voice's step by the previous voice
- SPU IRQ (i_stat bit 9) when the ADPCM decoder crosses the IRQ address

Merged around master's later work: Gaussian interpolation stays the
sample source (noise replaces it only in noise mode), and the voice
loop keeps master's any_voice gate, voice-sum tap, and shadow-tap
instrumentation with the branch's sweep-volume selection swapped in.

Co-Authored-By: NyperYuhgard <noreply@github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mstan pushed a commit that referenced this pull request Aug 5, 2026
…dd CFC0

Salvaged from PR #13 (feat/launcher-stb-revamp, commit 9fd2876) by
NyperYuhgard. Adds to the dirty-RAM interpreter:

- LoadAddressError/StoreAddressError delivery for misaligned LH/LW/LHU/
  SH/SW and misaligned JR/JALR targets, via a new interp_exception()
  that performs the architectural COP0 sequence (BadVAddr, Cause ExcCode,
  SR mode-stack push, EPC, BEV-selected vector).
- COP0 Cause write protection on MTC0/CTC0.
- CFC0/CTC0 aliases (identical to MFC0/MTC0 on R3000A).

Two deviations from the original:
- The original masked Cause writes to bits [15:10] (backwards vs
  hardware); corrected to the writable software-interrupt bits [9:8]
  per psx-spx / PCSX-Redux, which also un-deadens the psxTestSWInts
  recheck below it.
- The original's post-RFE immediate psx_check_interrupts recheck is
  dropped: master already delivers post-RFE interrupts through
  psx_rfe_mark_escape() and re-checking inside the handler window is
  the documented dbg-build boot-wedge livelock.

Co-Authored-By: NyperYuhgard <noreply@github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mstan added a commit that referenced this pull request Aug 5, 2026
Salvaged from PR #13 (feat/launcher-stb-revamp, commit 2ba52e9) by
NyperYuhgard. Master fatal-halts on any DMA start for channel 5; this
moves words to/from RAM directly (device side unemulated: RAM-to-device
reads-and-discards, device-to-RAM zero-fills) and completes the
transfer with the normal IRQ path.

Note: zero-filling from an unemulated expansion device is weaker than
the fail-closed psx_fatal_halt default the other channels follow; kept
as submitted because the contributor exercised it on a real title.
Trimmed from the original hunk: unused DMA_PIO_CYCLES_PER_WORD macro
and mdec_finish_async_{in,out}_transfer wrappers nothing references.

Co-Authored-By: NyperYuhgard <noreply@github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mstan added a commit that referenced this pull request Aug 5, 2026
Salvaged from PR #13 (feat/launcher-stb-revamp, commit 2ba52e9) by
NyperYuhgard. Fills the gaps the file header listed as not modeled:

- PS1-style reverb delay lines processing the final mix
- noise generator (LFSR) with per-voice noise mode and the noise-level
  register pacing steps
- per-voice sweep volume envelopes (volume reg bit 15) alongside the
  existing direct mode
- pitch modulation (PMON) of a voice's step by the previous voice
- SPU IRQ (i_stat bit 9) when the ADPCM decoder crosses the IRQ address

Merged around master's later work: Gaussian interpolation stays the
sample source (noise replaces it only in noise mode), and the voice
loop keeps master's any_voice gate, voice-sum tap, and shadow-tap
instrumentation with the branch's sweep-volume selection swapped in.

Co-Authored-By: NyperYuhgard <noreply@github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mstan pushed a commit that referenced this pull request Aug 5, 2026
…dd CFC0

Salvaged from PR #13 (feat/launcher-stb-revamp, commit 9fd2876) by
NyperYuhgard. Adds to the dirty-RAM interpreter:

- LoadAddressError/StoreAddressError delivery for misaligned LH/LW/LHU/
  SH/SW and misaligned JR/JALR targets, via a new interp_exception()
  that performs the architectural COP0 sequence (BadVAddr, Cause ExcCode,
  SR mode-stack push, EPC, BEV-selected vector).
- COP0 Cause write protection on MTC0/CTC0.
- CFC0/CTC0 aliases (identical to MFC0/MTC0 on R3000A).

Two deviations from the original:
- The original masked Cause writes to bits [15:10] (backwards vs
  hardware); corrected to the writable software-interrupt bits [9:8]
  per psx-spx / PCSX-Redux, which also un-deadens the psxTestSWInts
  recheck below it.
- The original's post-RFE immediate psx_check_interrupts recheck is
  dropped: master already delivers post-RFE interrupts through
  psx_rfe_mark_escape() and re-checking inside the handler window is
  the documented dbg-build boot-wedge livelock.

Co-Authored-By: NyperYuhgard <noreply@github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mstan added a commit that referenced this pull request Aug 5, 2026
Salvaged from PR #13 (feat/launcher-stb-revamp, commit 2ba52e9) by
NyperYuhgard. Master fatal-halts on any DMA start for channel 5; this
moves words to/from RAM directly (device side unemulated: RAM-to-device
reads-and-discards, device-to-RAM zero-fills) and completes the
transfer with the normal IRQ path.

Note: zero-filling from an unemulated expansion device is weaker than
the fail-closed psx_fatal_halt default the other channels follow; kept
as submitted because the contributor exercised it on a real title.
Trimmed from the original hunk: unused DMA_PIO_CYCLES_PER_WORD macro
and mdec_finish_async_{in,out}_transfer wrappers nothing references.

Co-Authored-By: NyperYuhgard <noreply@github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mstan

mstan commented Aug 5, 2026

Copy link
Copy Markdown
Owner Author

Closing this out — everything worth keeping has landed on master, credited to @NyperYuhgard.

Merged from this PR

Harvested earlier (July), each with attribution:

Merged today, after validating on Tomba 1, Tomba 2, Ape Escape and Mega Man X6:

  • 95669105 interp: deliver alignment exceptions, protect Cause read-only bits, add CFC0
  • e629d622 dma: implement channel 5 (PIO / expansion port) transfers

Three bugs in those two were fixed in-port rather than carried forward: the COP0 Cause write mask was inverted (it made the read-only hardware-IP bits writable and locked out the writable software-interrupt bits, which also dead-coded its own SW-interrupt recheck), the post-RFE interrupt recheck collided with master's psx_rfe_mark_escape livelock fix, and a duplicate $ra write duplicated work master already did before the delay slot.

Not merged, with reasons

SPU reverb / noise / sweep / PMON. Rejected. The gap is real — master still says reverb is not modelled — but this implementation's parameter mapping is invented: it reads dAPF2/vIIR/vCOMB1 as feedback/wet/early-reflection level, which is not what those registers are, and it gated the whole thing on bit 15 of dAPF1 (an all-pass offset register) rather than SPUCNT bit 7. It produces a reverb, not the console's, and nothing checks it against hardware. Under this project's no-fakes rule that has to be a fresh psx-spx-faithful implementation, not a port.

Recompiler changes (BC2F/BC2T, MOVN/MOVZ, branch-likely). Dropped after measurement. MOVN/MOVZ and the branch-likely forms raise Reserved Instruction on a real R3000A, so widening the discovery validity tables only admits data-as-code: measured on Tomba 1, it added 9 spurious dispatch entries, every one of them data (movn writing $zero), each replacing a fail-closed psx_unknown_dispatch with compiled garbage. Also corrected along the way: BC2T was encoded as rs=0x09 (both BC2F/BC2T are rs=0x08, rt bit 0 selects), the BC2 condition tested invented GTE FLAG bits when the PS1 GTE does not drive the COP2 condition input at all, and MOVZ was emitted with MOVN's condition.

The stb launcher. Obsolete — the engine no longer has its own launcher UI; the shared recomp-ui/ImGui launcher replaced it, and the RmlUi remnants were stripped in #76.

Also came out of this

The overlay: use platform-native shared-library suffixes commit above prompted a check of whether the packagers actually shipped those shards. They did not — the Linux packager never staged a cache, and the Windows one computed a codegen tag that had drifted from the runtime's, so its match silently never hit. Both are fixed and now refuse to build a cache-less package. Thanks for the original work, and for the pieces that ended up mattering in places nobody expected.

@mstan mstan closed this Aug 5, 2026
ClaudioVarandas pushed a commit to ClaudioVarandas/psxrecomp that referenced this pull request Aug 18, 2026
Extract the useful diagnostic from PR mstan#13 into an opt-in, title-agnostic boot
write-range trace with TCP-queryable context.

Co-authored-by: NyperYuhgard <93950153+NyperYuhgard@users.noreply.github.com>
ClaudioVarandas pushed a commit to ClaudioVarandas/psxrecomp that referenced this pull request Aug 18, 2026
…inational

Closes the four gaps named in issue mstan#103 and lands the separable, verified
parts of PR mstan#102. See docs/internal/SPU_FIDELITY_103.md for the per-piece
disposition and the oracle-verification queue.

SPU DSP (issue mstan#103), clean-room from hardware documentation:

- Reverb: the documented 22050 Hz engine — vLIN/vRIN input, same-side and
  crossed different-side IIR reflections, 4-tap comb early echo, both
  all-pass filters, vLOUT/vROUT output, and the work-area address advance.
  Gated on SPUCNT bit 7 for WRITES only; reads, IRQ checks, address advance
  and output always run, as on hardware. Addressing is confined to
  [mBASE, 0x80000). Per-voice sends from EON, CD send from SPUCNT bits 0+2.
- Noise: 16-bit LFSR with the documented parity feedback, clocked at the
  SPUCNT bits 8-13 rate; NON voices substitute it for their ADPCM sample.
- Volume sweeps: all 24 voice L/R plus main L/R. Bit 15 selects sweep vs
  direct; sweep is a live envelope on the same rate machinery as ADSR, and a
  guest read of a sweeping register returns the live level.
- Capture buffers: CD L/R and voice 1/3 output written to SPU RAM, so a game
  that parks the IRQ address there still gets its interrupt.
- SPU IRQ (I_STAT bit 9): checked at every SPU RAM access class — FIFO, DMA
  both directions, ADPCM block fetch, capture writes, reverb work-area
  accesses — gated on SPUCNT bit 6, mirrored in SPUSTAT bit 6, acked by
  clearing SPUCNT bit 6.

The CD-only fast path in spu_render was removed rather than extended: the
SPU now has per-frame work that must run in every path, and an FMV (XA audio
with zero active voices) is exactly the CD-with-reverb case.

This is a clean-room implementation from the psx-spx/nocash register map and
documented algorithm. Beetle's source was deliberately not consulted: it is
GPL-2.0-or-later and this project is PolyForm Noncommercial. PR mstan#16 was
parked for exactly that reason and PR mstan#13's reverb was separately wrong (it
gated on bit 15 of dAPF1, an address offset, instead of SPUCNT bit 7). The
one place this knowingly deviates from hardware is the 22.05 -> 44.1 kHz
reconstruction filter, which the documentation does not specify; it is
isolated in a single function and flagged for oracle comparison.

CAUSE.IP2 (from PR mstan#102):

IP2 is combinational on R3000A — it mirrors the interrupt line and falls the
instant the guest acks I_STAT or masks the source. It was being OR'd in at
delivery and never cleared, in TWO places (the compiled path and
psx_interpreter.c, the latter missed by PR mstan#102), leaving a phantom pending
interrupt that can spin a kernel dispatcher looping on CAUSE.IP & SR.IM.
psx_irq_refresh_cause_ip2() is now the sole writer of bit 10, refreshed at
raise, I_STAT ack, I_MASK write, HLE context restore and power-on. Verified
against the Beetle oracle's irq.cpp/cpu.cpp rather than asserted, and
covered by test_cause_ip2_combinational.

Mid-dispatch audio pump (from PR mstan#102), with a fix: routed through a gate
that mirrors sdl_audio_update's mute/sink decision. Pumping unconditionally
from the VBlank edge, as submitted, would have pushed real audio during every
turbo-load hard mute and defeated the freeze-in-place mute model.

SPU RAM DMA readback (from PR mstan#102): DMA4 in the SPU->RAM direction wrote
literal zeros; SPU RAM is readable memory and titles carry state through it
across an Exec boundary.

Also fixes an unrelated build race found on the way: tools/embed_spirv.py
wrote its intermediate SPIR-V to <source>.spv, so the two runtime targets
that embed the same shaders raced on one path and one deleted the file the
other was reading.

Co-Authored-By: Alexandros Mandravillis <Alexbeav@live.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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