Skip to content

Keep values carried around a loop live past its inner branches. - #9

Merged
fjtrujy merged 1 commit into
ps2dev:masterfrom
glampert:fix/loop-liveness
Oct 26, 2026
Merged

fjtrujy merged 1 commit into
ps2dev:masterfrom
glampert:fix/loop-liveness

Conversation

@glampert

Copy link
Copy Markdown

The register allocator walks each path through a code block, and stops a path at a branch it has already taken (or after --bthres visits). A loop's second trip is therefore never walked past its first inner branch. A value written in one trip and read after that branch in the next trip gets no live range around the back edge, and a later temporary can take its register:

isw.x   iCount, 0(iPtr)
iaddiu  iPtr, iPtr, 1      ; VI01, read again after the inner branch
iaddiu  iT2, vi00, 7       ; also VI01

Values loaded before a --cont loop and read after an inner branch lose their registers the same way. Two real programs are affected:

  • Tyra's static pipelines load their GIF tags once and continue with MSCNT for each further buffer, so from the second buffer on the tags are overwritten by vertex colours.
  • ps2gl general: material_spec, loaded before the directional-light loop, is overwritten inside it.

Fix

After the walk, completeLiveRanges follows each block's control flow back from every read to the writes that reach it, lane by lane. It joins them into one alias that is live over the tokens in between, as a complete walk would have. Inputs count as written at the block's entry; a broadcast operand keeps only its lane live. Where the walk was already complete this adds nothing, and the code is unchanged.

Effect

On a local corpus of 46 programs (ps2gl, Tyra, quake2-ps2, the openvcl examples), against current master:

  • 12 programs change: quake2-ps2 textured_triangles, Tyra stapip_as_is_c, stapip_as_is_tc, stapip_cull_c and stapip_cull_tc, ps2gl general, general_nospec, general_pv_diff, general_pv_diff_tri, general_tri and indexed, and one small repro.
  • None fails to compile.
  • The other 34 programs are byte-identical.

Tests

test/integration/test_loop_liveness.cpp, 4 cases:

  • a pointer carried around a loop keeps its register past an inner branch;
  • a value loaded before a --cont loop keeps its register through the loop;
  • a value set in one loop keeps its register through a sibling loop;
  • a broadcast operand keeps only its lane live.

The register allocator walks each path through a code block, and stops
a path at a branch the path has already taken. A loop's second trip is
never walked past its first inner branch, so a value written in one
trip and read after that branch in the next has no range around the
back edge, and a later temporary can take its register:

	iaddiu VI01, VI01, 1	; iPtr, read again after the inner branch
	iaddiu VI01, VI00, 7	; iT2

Values loaded before a --cont loop and read after an inner branch lose
their registers the same way. Tyra's static pipeline programs load their
GIF tags once and continue with MSCNT for each further buffer, so from
the second buffer on the tags were overwritten by vertex colours.

After the walk, follow each block's control flow back from every read
to the writes that reach it, lane by lane, and join them into one alias
live over the tokens in between, as a complete walk would have. Where
the walk was complete this adds nothing, and the code is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@fjtrujy fjtrujy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@fjtrujy
fjtrujy merged commit 8b96e0b into ps2dev:master Oct 26, 2026
6 checks passed
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