Skip to content

Commit 7cbeff3

Browse files
author
DavidQ
committed
Level 18.1 overlay runtime hardening.
Improve stability for input, resize, and sample switching.
1 parent df84112 commit 7cbeff3

8 files changed

Lines changed: 164 additions & 36 deletions

File tree

docs/dev/CODEX_COMMANDS.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
MODEL: GPT-5.3-codex
2-
REASONING: low
1+
MODEL: GPT-5.4-codex
2+
REASONING: medium
3+
34
COMMAND:
4-
Promote Level 17 overlay system to baseline.
5-
- Confirm no regressions
6-
- Update status markers only
7-
- Do not modify runtime behavior
5+
Implement runtime hardening for overlay system:
6+
- Ensure stable cycling under rapid input
7+
- Validate bottom-right anchoring under resize
8+
- Prevent flicker during sample switching
9+
- Do not change feature behavior
810

911
Package ZIP to <project folder>/tmp/

docs/dev/COMMIT_COMMENT.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
Promote Level 17 debug overlay system to baseline after validation.
1+
Level 18.1 overlay runtime hardening.
2+
Improve stability for input, resize, and sample switching.
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
[x] Smoke test complete
2-
[x] No regressions found
3-
[x] Baseline confirmed
1+
[ ] Rapid cycle stable
2+
[ ] No flicker on sample switch
3+
[ ] Resize keeps bottom-right anchor
4+
[ ] No stack reorder issues

docs/pr/BUILD_PR.md

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,33 @@
1-
# BUILD_PR_LEVEL_17_59_DEBUG_OVERLAY_PROMOTE_BASELINE
2-
3-
## Purpose
4-
Promote Level 17 debug overlay system to baseline after successful validation sweep.
5-
6-
## Scope
7-
- Mark Level 17 as complete
8-
- Confirm overlay system baseline:
9-
- Bottom-right placement
10-
- Non-Tab cycle key
11-
- Sample-specific stack mappings
12-
13-
## Test Steps
14-
1. Run smoke test across samples
15-
2. Confirm no regressions
16-
3. Validate baseline readiness
17-
18-
## Expected
19-
- Stable baseline
20-
- Ready for next level integration
1+
# BUILD_PR_LEVEL_18_1_OVERLAY_RUNTIME_HARDENING
2+
3+
## PLAN
4+
5+
### Purpose
6+
Harden overlay runtime behavior after Level 17 baseline promotion to ensure stability under rapid input, resizing, and multi-sample switching.
7+
8+
### Goals
9+
- Ensure cycle key stability under rapid input
10+
- Prevent overlay flicker during sample switching
11+
- Lock bottom-right anchoring under resize
12+
- Validate overlay layering does not reorder unexpectedly
13+
14+
---
15+
16+
## BUILD
17+
18+
### Scope
19+
- Overlay runtime stabilization (no feature expansion)
20+
- Input debounce/throttle validation
21+
- Resize handling validation
22+
- Sample switching consistency
23+
24+
### Test Steps
25+
1. Rapidly cycle overlays
26+
2. Switch between samples quickly
27+
3. Resize viewport
28+
4. Validate overlays remain stable and anchored
29+
30+
### Expected
31+
- No flicker
32+
- No misalignment
33+
- No cycle skips

samples/phase-17/shared/tabDebugOverlayCycle.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,21 @@ function hasShiftModifier(input) {
1717
return input?.isDown('ShiftLeft') === true || input?.isDown('ShiftRight') === true;
1818
}
1919

20+
function normalizeActiveIndex(controller) {
21+
if (!controller || !Array.isArray(controller.overlays) || controller.overlays.length === 0) {
22+
if (controller) {
23+
controller.activeIndex = 0;
24+
}
25+
return 0;
26+
}
27+
28+
const count = controller.overlays.length;
29+
const current = Number.isInteger(controller.activeIndex) ? controller.activeIndex : 0;
30+
const normalized = ((current % count) + count) % count;
31+
controller.activeIndex = normalized;
32+
return normalized;
33+
}
34+
2035
export function createTabDebugOverlayController({ overlays = [], initialOverlayId = '' } = {}) {
2136
const normalized = [];
2237
for (let i = 0; i < overlays.length; i += 1) {
@@ -40,7 +55,7 @@ export function createTabDebugOverlayController({ overlays = [], initialOverlayI
4055

4156
return {
4257
overlays: normalized,
43-
activeIndex,
58+
activeIndex: normalized.length > 0 ? Math.max(0, Math.min(activeIndex, normalized.length - 1)) : 0,
4459
cycleKey: 'Tab',
4560
cycleLatch: false,
4661
};
@@ -71,6 +86,7 @@ export function setTabDebugOverlayMap(controller, { overlays = [], initialOverla
7186
controller.activeIndex = lookupIndex;
7287
}
7388
}
89+
normalizeActiveIndex(controller);
7490
controller.cycleLatch = false;
7591
return true;
7692
}
@@ -115,6 +131,7 @@ export function setTabDebugOverlayActive(controller, overlayId) {
115131
return false;
116132
}
117133
controller.activeIndex = nextIndex;
134+
normalizeActiveIndex(controller);
118135
return true;
119136
}
120137

@@ -125,6 +142,7 @@ export function stepTabDebugOverlayController(controller, input) {
125142

126143
const cycleKey = String(controller.cycleKey || 'Tab');
127144
const cyclePressed = input?.isDown(cycleKey) === true;
145+
normalizeActiveIndex(controller);
128146
if (cyclePressed && controller.cycleLatch === false && controller.overlays.length > 1) {
129147
const delta = hasShiftModifier(input) ? -1 : 1;
130148
const count = controller.overlays.length;
@@ -137,6 +155,7 @@ export function isTabDebugOverlayActive(controller, overlayId) {
137155
if (!controller || !Array.isArray(controller.overlays) || controller.overlays.length === 0) {
138156
return false;
139157
}
158+
normalizeActiveIndex(controller);
140159
const active = controller.overlays[controller.activeIndex];
141160
return active?.id === overlayId;
142161
}
@@ -145,7 +164,7 @@ export function getTabDebugOverlayStatusLabel(controller) {
145164
if (!controller || !Array.isArray(controller.overlays) || controller.overlays.length === 0) {
146165
return 'none';
147166
}
148-
const index = Math.max(0, Math.min(controller.activeIndex, controller.overlays.length - 1));
167+
const index = normalizeActiveIndex(controller);
149168
const active = controller.overlays[index];
150169
return `${active.label} (${index + 1}/${controller.overlays.length})`;
151170
}

src/engine/debug/DebugOverlayLayout.js

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,38 @@ function clamp(value, min, max) {
1010
return Math.max(min, Math.min(max, value));
1111
}
1212

13+
function readCanvasSize(renderer) {
14+
const canvasSize = renderer?.getCanvasSize?.() || { width: 960, height: 540 };
15+
return {
16+
width: Math.max(1, Number(canvasSize.width) || 960),
17+
height: Math.max(1, Number(canvasSize.height) || 540),
18+
};
19+
}
20+
21+
function syncStackCanvasSize(stack) {
22+
if (!stack) {
23+
return;
24+
}
25+
const canvas = readCanvasSize(stack.renderer);
26+
stack.canvasWidth = canvas.width;
27+
stack.canvasHeight = canvas.height;
28+
if (!Number.isFinite(stack.usedHeight) || stack.usedHeight < 0) {
29+
stack.usedHeight = 0;
30+
}
31+
}
32+
1333
export function createBottomRightDebugPanelStack(renderer, {
1434
right = 10,
1535
bottom = 10,
1636
spacing = 10,
1737
minX = 8,
1838
minY = 8,
1939
} = {}) {
20-
const canvasSize = renderer?.getCanvasSize?.() || { width: 960, height: 540 };
40+
const canvas = readCanvasSize(renderer);
2141
return {
22-
canvasWidth: Math.max(1, Number(canvasSize.width) || 960),
23-
canvasHeight: Math.max(1, Number(canvasSize.height) || 540),
42+
renderer,
43+
canvasWidth: canvas.width,
44+
canvasHeight: canvas.height,
2445
right: Math.max(0, Number(right) || 0),
2546
bottom: Math.max(0, Number(bottom) || 0),
2647
spacing: Math.max(0, Number(spacing) || 0),
@@ -40,6 +61,7 @@ export function getNextBottomRightDebugPanelRect(stack, width, height) {
4061
};
4162
}
4263

64+
syncStackCanvasSize(stack);
4365
const panelWidth = Math.max(1, Number(width) || 1);
4466
const panelHeight = Math.max(1, Number(height) || 1);
4567
const x = clamp(

tests/runtime/Phase17DebugOverlayBottomRightPosition.test.mjs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,17 @@ function pressCycleKey(scene) {
5454
}
5555

5656
function createRendererProbe(width = 960, height = 540) {
57+
let canvasWidth = width;
58+
let canvasHeight = height;
5759
const texts = [];
5860
return {
5961
texts,
6062
getCanvasSize() {
61-
return { width, height };
63+
return { width: canvasWidth, height: canvasHeight };
64+
},
65+
setCanvasSize(nextWidth, nextHeight) {
66+
canvasWidth = Math.max(1, Number(nextWidth) || canvasWidth);
67+
canvasHeight = Math.max(1, Number(nextHeight) || canvasHeight);
6268
},
6369
clear() {},
6470
drawRect() {},
@@ -94,8 +100,11 @@ function assertSharedStackMath() {
94100
const stack = createBottomRightDebugPanelStack(renderer, { right: 10, bottom: 10, spacing: 10 });
95101
const first = getNextBottomRightDebugPanelRect(stack, 300, 170);
96102
const second = getNextBottomRightDebugPanelRect(stack, 300, 120);
103+
renderer.setCanvasSize(1280, 720);
104+
const resized = getNextBottomRightDebugPanelRect(stack, 300, 170);
97105
assert.deepEqual(first, { x: 650, y: 360, width: 300, height: 170 });
98106
assert.deepEqual(second, { x: 650, y: 230, width: 300, height: 120 });
107+
assert.deepEqual(resized, { x: 970, y: 230, width: 300, height: 170 });
99108
}
100109

101110
function assertSample1701RuntimePanelPlacement() {
@@ -215,6 +224,39 @@ function assertSample1713FinalRuntimePlacement() {
215224
assertBottomRightFromTitle(runtimeTitle, 228, 248, 722, 282, 'Sample 1713 final runtime overlay');
216225
}
217226

227+
function assertNoFlickerDuringSampleSwitching() {
228+
const scenes = [
229+
[new RealGameplayMiniGameScene(), 'UI Layer'],
230+
[new MovementModelsLab1709Scene(), 'Movement Runtime'],
231+
[new RealGameplayMiniGame1710Scene(), 'UI Layer'],
232+
[new MovementModelsLab1711Scene(), 'Movement Runtime'],
233+
[new GameplayMetricsTelemetryScene(), 'UI Layer'],
234+
[new FinalReferenceGameScene(), 'UI Layer'],
235+
];
236+
const overlayTitles = [
237+
'UI Layer',
238+
'Mission Feed',
239+
'MISSION READY',
240+
'Mini-Game Runtime',
241+
'Movement Runtime',
242+
'Movement Lab HUD',
243+
'Telemetry Overlay',
244+
'Final Reference Runtime',
245+
];
246+
const sharedCamera = createCameraStub();
247+
for (let i = 0; i < scenes.length; i += 1) {
248+
const [scene, expectedTitle] = scenes[i];
249+
scene.setCamera3D?.(sharedCamera);
250+
for (let pass = 0; pass < 2; pass += 1) {
251+
const renderer = createRendererProbe();
252+
scene.render(renderer);
253+
const visibleOverlays = overlayTitles.filter((title) => Boolean(findExactText(renderer, title)));
254+
assert.equal(visibleOverlays.length, 1, `Sample switch pass ${i + 1}.${pass + 1} should render exactly one active overlay panel title.`);
255+
assert.equal(visibleOverlays[0], expectedTitle, `Sample switch pass ${i + 1}.${pass + 1} should render expected default overlay without flicker.`);
256+
}
257+
}
258+
}
259+
218260
export function run() {
219261
assertSharedStackMath();
220262
assertSample1701RuntimePanelPlacement();
@@ -225,4 +267,5 @@ export function run() {
225267
assertSample1711MovementOverlayPlacement();
226268
assertSample1712TelemetryPlacement();
227269
assertSample1713FinalRuntimePlacement();
270+
assertNoFlickerDuringSampleSwitching();
228271
}

tests/runtime/Phase17TabDebugOverlayCycle1707Plus.test.mjs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ function pressCycleKey(scene, { reverse = false } = {}) {
4848
scene.step3DPhysics(0.02, { input: makeInput([]) });
4949
}
5050

51+
function holdCycleKey(scene, { reverse = false, frames = 4 } = {}) {
52+
const keys = reverse ? ['KeyG', 'ShiftLeft'] : ['KeyG'];
53+
for (let i = 0; i < Math.max(1, frames); i += 1) {
54+
scene.step3DPhysics(0.01, { input: makeInput(keys) });
55+
}
56+
scene.step3DPhysics(0.01, { input: makeInput([]) });
57+
}
58+
5159
function createRendererProbe(width = 960, height = 540) {
5260
const texts = [];
5361
return {
@@ -77,6 +85,25 @@ function assertMapOrderAndKeyBehavior(label, sceneFactory, expectedLabels, expec
7785
const labels = scene.tabDebugOverlays.overlays.map((entry) => entry.label);
7886
assert.deepEqual(labels, expectedLabels, `${label} should use exact required overlay map ordering.`);
7987

88+
if (scene.tabDebugOverlays.overlays.length > 1) {
89+
const count = scene.tabDebugOverlays.overlays.length;
90+
const beforeHoldForward = scene.tabDebugOverlays.activeIndex;
91+
holdCycleKey(scene);
92+
assert.equal(
93+
scene.tabDebugOverlays.activeIndex,
94+
(beforeHoldForward + 1) % count,
95+
`${label} should cycle exactly once while cycle key is held across rapid frames.`
96+
);
97+
98+
const beforeHoldReverse = scene.tabDebugOverlays.activeIndex;
99+
holdCycleKey(scene, { reverse: true });
100+
assert.equal(
101+
scene.tabDebugOverlays.activeIndex,
102+
(beforeHoldReverse - 1 + count) % count,
103+
`${label} should cycle exactly once in reverse while reverse cycle key is held across rapid frames.`
104+
);
105+
}
106+
80107
for (let i = 0; i < expectedTokens.length; i += 1) {
81108
const renderer = createRendererProbe();
82109
scene.render(renderer);

0 commit comments

Comments
 (0)