Skip to content

v0.0.192

Choose a tag to compare

@github-actions github-actions released this 27 Sep 07:24
· 70 commits to main since this release

rePIU v0.0.192

이전 릴리스 v0.0.191 이후의 변경입니다(Tasks 741–744). Linux x64에서 trap 한 번의 비용을
41% 줄이고, pumpitea의 breakpoint 예외를 초당 38,000회에서 약 30,000회로 줄였습니다.
어느 guest
지점이 trap을 내는지 세는 census가 항상 켜져 있고, 번역되지 않은 블록으로 돌아오는 ret이 이름 없는
SIGTRAP으로 죽던 잠복 결함을 고쳤습니다. 이 릴리스는 Linux x64 엔진 내부의 변경이며 게임 동작은
바뀌지 않습니다(Windows 빌드는 이번 변경에 영향받는 코드가 없거나 원본 바이트 복사로 동일).

숫자로 (pumpitea attract 27~30초, WSL Linux x64 Release)

v0.0.191 v0.0.192
breakpoint 예외 1회의 VEH 비용 15.2k cycle 8.9k cycle
VEH의 벽시계 비중 14.3% 6.7~7.3%
breakpoint 예외 수 1.02M~1.22M 0.88M~0.97M
memcpy helper push es·mov eax,ds 53,914 × 2 0
jmp cs:[table] (0x010F659E) 27,594~36,659 0
pumpit2a 25초 breakpoint 149,911 108,557

fps는 그대로입니다. 남은 trap은 게임 자신의 대기 루프(Watcom delay()의 INT 21h AH=2Ch 보정 spin
과 폴링, lseek) 안에서 일어나므로 비용이 줄어든 만큼 같은 대기 안에서 더 돌 뿐이고, 얻은 것은 CPU
점유입니다.

고친 것

"ISR 입력 스캔의 trap 부하"는 오진이었습니다 (Task 741) — 새 breakpoint 지점 census(항상
켜짐, 최종 보고의 breakpoint site #N host/guest/cache/count/exits 상위 32개)가 실제 상위를 밝혔습니다.
Watcom delay()의 INT 21h AH=2Ch 호출 세 곳이 61%(시작 2초의 보정 spin 45만 회 + 초당 6,600회),
lseek이 10%, memcpy helper의 segment write 쌍이 10%, ISR은 1%대입니다. trap 수는 게임 대기 루프가
정하므로 trap 한 번의 비용을 줄였습니다: HLE 재진입마다 같은 guest 주소를 Zydis로 세 번씩(segment
write 여부, 앞 64 명령의 span 안전, long mode 동일성) 다시 디코드하던 것을 주소별 메모(1,024칸, guest
코드 쓰기마다 오르는 세대·8바이트 지문·code mode로 무효화)로 바꿨습니다. REPIU_AOT_REENTRY_MEMO=0
으로 끕니다. DOS 서비스 안의 호출마다 getenv와 localtime 변환도 캐시합니다.

segment 읽기·push가 trap 없이 (Task 742) — mov r16/r32,sreg와 32비트 push es/ds/fs/gs가
long-mode 슬롯으로 방출됩니다. 값은 guest가 보는 selector이고 엔진이 shadow에 유지하므로 슬롯은 guard
없이 shadow를 읽습니다(새 kind kGuardedSegmentPush; i386 이미지는 원본 복사). 이전 코드는 long-mode
이미지에 i386 read 슬롯을 방출해 host 스택에 pushfq하고 host DS=0을 RIP 상대 주소와 비교해 항상
fallback INT3로 가고 있었습니다. 이제 long mode에서는 방출하지 않습니다.

번역되지 않은 대상으로의 return (Task 743) — 동적 번역이 거절된 블록으로 ret이 돌아오면 return
thunk의 resolver가 "첫 명령이 long mode에서 byte-identical"일 때만 legacy resume을 허용해, 그 밖의
대상(pop edx가 첫 명령인 흔한 경우)에서 thunk의 int3로 죽었습니다. 간접 call과 같은 조건(읽을 수
있는 guest 코드)으로 넓혔습니다. 남는 막다른 곳(번역도 없고 동일하지도 않은 명령)은
[repiu-x64-untranslatable] stage= eip= bytes= …로 주소와 바이트를 남기고 끝나며, [repiu-fault] unhandled …에 마지막 VEH exit site(last_exit_site=/last_exit_eip=)가 붙습니다.

xor edx,edx; mov dl,bl로 정규화하는 jump table (Task 744) — planner의 low-byte guard(cmp bl,imm; ja)가 정규화로 and r32,0xFF만 알아, 이 Watcom switch 형식은 CS: 접두어 때문에 매번
trap했습니다. guard가 xor/sub r32,r32를 지나 mov r8,r8'(또는 movzx r32,r8)에서 완성됩니다.

새 진단 (설정하지 않으면 동작 없음)

변수 / 출력 내용
breakpoint site #N host/guest/cache/count/exits (최종 보고, 항상) breakpoint 예외를 낸 host 주소·guest 주소·횟수·끝낸 VEH exit site 상위 32개
AOT reentry memo hit/miss segment/span/long-mode (최종 보고, 항상) 재진입 메모 적중률
REPIU_AOT_REENTRY_MEMO=0 재진입 메모를 끄고 매번 디코드(A/B용)
REPIU_AOT_DYNAMIC_REJECT=<guest 주소> 또는 read worker가 그 동적 이미지를 거절해 번역 실패 상황을 재현
[repiu-x64-untranslatable] … 번역 없고 long mode 비동일한 명령에서 멈출 때 주소·바이트 (8회까지)
[repiu-fault] unhandled … last_exit_site= last_exit_eip= 거절한 VEH exit site 코드(VehExitSite 번호)와 EIP
[repiu-aot-unsafe] line=N 동적 이미지 append가 unsafe 실패로 끝난 검사 지점

알려진 것

  • 번역도 없고 long mode 동일하지도 않은 명령(예: 절대 주소 cmp)에 이르면 Linux x64는 여전히 실행할
    수 없습니다(인터프리터 없음). 동적 번역이 정상이면 도달하지 않으며, 이제 주소를 남기고 끝납니다.
  • Linux x64의 남은 trap은 전부 게임 자신의 것입니다: delay() AH=2Ch, lseek, ISR의 batch된
    in·EOI·iret. 곡 재생 중 tick이 초당 234244로 흔들리는 것과 장면 전환 중 40160 ms의 tick
    공백은 조사하지 않았습니다.
  • REPIU_GUEST_POSITION_CENSUS는 Linux에서 표본을 잡지 못합니다. REPIU_DOS_INT_TRACE는 stderr
    출력이 대기 루프를 늦춰 호출 수를 왜곡하므로 trap을 셀 때 쓰지 마십시오.
  • v0.0.191의 알려진 것(Windows legacy backend pumpitea 크래시, VS 2026 v145 dos4gw_hello 크래시,
    CD 미리듣기 무음 보고)은 그대로입니다.
  • Linux 측정은 WSLg에서 했습니다.

검증: Linux x64 core probe 전체 통과(새 case: 재진입 메모, long-mode segment read/push 슬롯 3개,
jump-table guard 3개), Windows x86 core probe 전체 통과, WSL 합성 입력으로 pumpitea 코인 → 곡 선택 →
플레이(60초, 폴트 0), pumpit2a 25초 회귀, 수정 빌드 4/4 실행 폴트 0.


rePIU v0.0.192

Changes since v0.0.191 (Tasks 741–744). On Linux x64 one trap costs 41% less and pumpitea's
breakpoint exceptions fell from about 38,000 to about 30,000 a second.
A census of which guest sites
trap is always on, and a latent defect where a ret into an untranslated block died as an anonymous
SIGTRAP is fixed. This release changes the Linux x64 engine internals only; game behaviour is
unchanged (the Windows build either has no affected code or copies the original bytes as before).

In numbers (pumpitea attract, 27–30 s, WSL Linux x64 Release)

v0.0.191 v0.0.192
VEH cost of one breakpoint exception 15.2k cycles 8.9k cycles
VEH share of wall 14.3% 6.7–7.3%
Breakpoint exceptions 1.02–1.22 M 0.88–0.97 M
memcpy helper push es, mov eax,ds 53,914 × 2 0
jmp cs:[table] (0x010F659E) 27,594–36,659 0
pumpit2a 25 s breakpoints 149,911 108,557

Frame rate is unchanged: the remaining traps sit inside the game's own wait loops (Watcom delay()'s
INT 21h AH=2Ch calibration spin and polling, lseek), so a cheaper trap only spins more inside the same
wait. The gain is CPU occupancy.

Fixed

"The ISR input scan's trap load" was a misdiagnosis (Task 741): the new breakpoint site
census
(always on; breakpoint site #N host/guest/cache/count/exits, the 32 busiest, in the final
report) named the real top sites: three INT 21h AH=2Ch calls of Watcom delay() at 61% (a 450,000-call
calibration spin in the first two seconds plus 6,600 a second), lseek at 10%, a memcpy helper's
segment-write pair at 10%, the ISR about 1%. The count is the game's own wait loop, so the cost of
one trap
was cut: every HLE reentry decoded the same guest address three ways with Zydis (segment
write, span safety up to 64 instructions, long-mode identity); a per-address memo (1,024 slots,
invalidated by a generation that moves on every guest code write, an eight-byte fingerprint and the
code mode) replaces that. REPIU_AOT_REENTRY_MEMO=0 turns it off. The per-call getenv and the
localtime conversion inside the DOS service are cached too.

Segment reads and pushes without a trap (Task 742): mov r16/r32,sreg and a 32-bit push es/ds/fs/gs are emitted as long-mode slots. The value is the guest-visible selector the engine keeps
in the shadow, so the slots read the shadow with no guard (new kind kGuardedSegmentPush; i386 images
copy the bytes). The earlier code emitted the i386 read slot into long-mode images, pushing RFLAGS
onto the host stack and comparing the host DS of 0 RIP-relatively, so it always fell back to INT3; it
is no longer emitted in long mode.

Returning to an untranslated target (Task 743): when a ret came back to a block whose dynamic
translation had been rejected, the return thunk's resolver allowed the legacy resume only for a
byte-identical first instruction in long mode and died at the thunk's int3 for anything else (a
pop edx, commonly). It now uses the indirect call's condition (readable guest code). The dead ends
that remain (no translation and no long-mode identity) end with [repiu-x64-untranslatable] stage= eip= bytes= …, and [repiu-fault] unhandled … carries the last VEH exit site (last_exit_site=,
last_exit_eip=).

Jump tables normalized by xor edx,edx; mov dl,bl (Task 744): the planner's low-byte guard (cmp bl,imm; ja) knew only and r32,0xFF as its normalization, so this Watcom switch form trapped every
time through its CS: prefix. The guard now survives xor/sub r32,r32 and completes on mov r8,r8'
(or movzx r32,r8).

New diagnostics (inert when unset)

Variable / output What it does
breakpoint site #N host/guest/cache/count/exits (final report, always) the 32 host/guest addresses raising the most breakpoint exceptions, with the VEH exit that ended them
AOT reentry memo hit/miss segment/span/long-mode (final report, always) the reentry memo's hit rate
REPIU_AOT_REENTRY_MEMO=0 turns the reentry memo off, decoding every time (for A/B)
REPIU_AOT_DYNAMIC_REJECT=<guest address> or read makes the worker reject those dynamic images, reproducing a translation failure
[repiu-x64-untranslatable] … address and bytes where execution stops at an untranslated, non-identical instruction (up to eight)
[repiu-fault] unhandled … last_exit_site= last_exit_eip= the refusing VEH exit site (VehExitSite code) and EIP
[repiu-aot-unsafe] line=N which check ended a dynamic image append as an unsafe failure

Known

  • An instruction with no translation and no long-mode identity (an absolute cmp, say) still cannot
    run on Linux x64 (no interpreter); unreachable while dynamic translation works, and now named.
  • The remaining Linux x64 traps are all the game's own: delay() AH=2Ch, lseek, the ISR's batched
    in, EOI and iret. The tick rate wandering 234–244 a second during a song and the 40–160 ms tick
    gaps during scene loads were not investigated.
  • REPIU_GUEST_POSITION_CENSUS captures nothing on Linux. REPIU_DOS_INT_TRACE's stderr writes slow
    the wait loops and distort call counts; do not use it to count traps.
  • v0.0.191's known items (the Windows legacy backend's pumpitea crash, the VS 2026 v145 dos4gw_hello
    crash, the CD-preview silence report) remain.
  • Linux figures were measured under WSLg.

Verified by the whole Linux x64 core probe (new cases: the reentry memo, three long-mode segment
read/push slots, three jump-table guards), the whole Windows x86 core probe, an automated WSL
reproduction of pumpitea from coin to song select to play (60 s, no faults), a pumpit2a 25 s
regression, and 4 of 4 runs of the fixed build without a fault.