Merge branch 'flow/m3te/P20.1' into m3te-plan
This commit is contained in:
32
README.md
32
README.md
@@ -344,6 +344,36 @@ env SIMCTL_CHILD_M3TE_FX=11 SIMCTL_CHILD_M3TE_ANIM_TIME=3.0 \
|
|||||||
The change is render-only — no `board.sx` model change, and normal play is
|
The change is render-only — no `board.sx` model change, and normal play is
|
||||||
byte-identical apart from the fall's per-column timing.
|
byte-identical apart from the fall's per-column timing.
|
||||||
|
|
||||||
|
### FPS counter — dev overlay (P20.1)
|
||||||
|
|
||||||
|
A small FPS readout for gauging frame cost while tuning the animations. It is a
|
||||||
|
**dev overlay, OFF by default**: only the `M3TE_FPS` env pin turns it on, so default
|
||||||
|
play and every committed golden stay byte-identical (with `M3TE_FPS` unset the
|
||||||
|
rendered scene is unchanged — `goldens/p6_idle_t0.png` reproduces exactly).
|
||||||
|
|
||||||
|
- `M3TE_FPS=<non-zero>` renders the FPS counter in the **top-left corner** (inside
|
||||||
|
the safe area, clear of the centered notch / Dynamic Island and the centered HUD).
|
||||||
|
The rate is computed from the per-frame `delta_time` as an exponential moving
|
||||||
|
average (`FPS_DT_SMOOTH = 0.9`) so the digits don't jitter. Read once at startup
|
||||||
|
like every other `M3TE_*` pin; `=0` or unset leaves it off. Purely a render
|
||||||
|
overlay — no board / score / move / animation state changes, and it never gates
|
||||||
|
input. `delta_time` is real wall-clock even when `M3TE_ANIM_TIME` pins the
|
||||||
|
animation, so the counter stays live while the rest of the scene is frozen.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# FPS counter over the resting board: goldens/p20_fps.png
|
||||||
|
# (the FPS digits are DYNAMIC — only the FPS text varies run-to-run; the rest of the
|
||||||
|
# scene is pinned at M3TE_ANIM_TIME=0, byte-identical to goldens/p6_idle_t0.png)
|
||||||
|
env SIMCTL_CHILD_M3TE_FPS=1 SIMCTL_CHILD_M3TE_ANIM_TIME=0 \
|
||||||
|
xcrun simctl launch --terminate-running-process booted co.swipelab.m3te
|
||||||
|
# On device, pass the same flag through devicectl's environment instead:
|
||||||
|
# xcrun devicectl device process launch --environment M3TE_FPS=1 ... co.swipelab.m3te
|
||||||
|
```
|
||||||
|
|
||||||
|
`goldens/p20_fps.png` is the only golden that captures this overlay; because the FPS
|
||||||
|
digits are dynamic, compare the FPS text's PRESENCE in the top-left corner, not its
|
||||||
|
exact value. Every other golden is captured with `M3TE_FPS` unset and is unaffected.
|
||||||
|
|
||||||
## Audio bank (P10) — final model
|
## Audio bank (P10) — final model
|
||||||
|
|
||||||
The SFX bank (`audio.sx`) is a purely additive layer over iOS **System Sound
|
The SFX bank (`audio.sx`) is a purely additive layer over iOS **System Sound
|
||||||
@@ -470,5 +500,5 @@ sips -z 128 768 --setProperty format png <generated>.png --out assets/gems/gems.
|
|||||||
|
|
||||||
After any art change, re-capture the affected goldens with the deterministic hooks
|
After any art change, re-capture the affected goldens with the deterministic hooks
|
||||||
above (`M3TE_ANIM_TIME` / `M3TE_SELECT` / `M3TE_FX` / `M3TE_BADSWAP` / `M3TE_TARGET` /
|
above (`M3TE_ANIM_TIME` / `M3TE_SELECT` / `M3TE_FX` / `M3TE_BADSWAP` / `M3TE_TARGET` /
|
||||||
`M3TE_MOVE_LIMIT` / `M3TE_RESTART`) and state per golden whether it was refreshed,
|
`M3TE_MOVE_LIMIT` / `M3TE_RESTART` / `M3TE_FPS`) and state per golden whether it was refreshed,
|
||||||
left unchanged, or removed.
|
left unchanged, or removed.
|
||||||
|
|||||||
@@ -60,6 +60,15 @@ HUD_PANEL :: Color.{ r = 92, g = 46, b = 150, a = 224 }; // bright grape ca
|
|||||||
HUD_PANEL_HI :: Color.{ r = 196, g = 138, b = 240, a = 92 }; // glossy top sheen
|
HUD_PANEL_HI :: Color.{ r = 196, g = 138, b = 240, a = 92 }; // glossy top sheen
|
||||||
HUD_PANEL_RIM:: Color.{ r = 236, g = 204, b = 255, a = 150 }; // bright candy rim
|
HUD_PANEL_RIM:: Color.{ r = 236, g = 204, b = 255, a = 150 }; // bright candy rim
|
||||||
|
|
||||||
|
// FPS dev overlay (P20.1): a small corner readout, OFF unless M3TE_FPS pins it on
|
||||||
|
// (so default play + every golden are unchanged). Pinned to the top-left of the
|
||||||
|
// safe area — clear of the centered notch / Dynamic Island and the centered HUD.
|
||||||
|
// Dark grape text over a bright halo keeps it legible on the light lavender art.
|
||||||
|
FPS_FONT :f32: 22.0;
|
||||||
|
FPS_PAD :f32: 8.0;
|
||||||
|
FPS_TEXT :: Color.{ r = 40, g = 16, b = 64, a = 235 }; // dark grape, readable on lavender
|
||||||
|
FPS_TEXT_SH:: Color.{ r = 255, g = 255, b = 255, a = 170 }; // bright halo for contrast
|
||||||
|
|
||||||
// Win/lose banner (P12.2): a warm dim over the board, a glossy candy panel, the
|
// Win/lose banner (P12.2): a warm dim over the board, a glossy candy panel, the
|
||||||
// win/lose headline, and a playful restart button. Built from text + rects only —
|
// win/lose headline, and a playful restart button. Built from text + rects only —
|
||||||
// the engine's image path can't tint/fade at draw time (issue 0002), but rects and
|
// the engine's image path can't tint/fade at draw time (issue 0002), but rects and
|
||||||
@@ -229,6 +238,11 @@ BoardView :: struct {
|
|||||||
// Seed for `restart`: the same fixed seed main seeded the board with, so the
|
// Seed for `restart`: the same fixed seed main seeded the board with, so the
|
||||||
// restart button reproduces the identical starting level.
|
// restart button reproduces the identical starting level.
|
||||||
seed: s64;
|
seed: s64;
|
||||||
|
// FPS dev overlay (P20.1). `fps_on` gates the corner readout (off by default,
|
||||||
|
// set only by the M3TE_FPS env pin); `fps` is the smoothed reciprocal frame
|
||||||
|
// rate computed in the frame loop. Purely a render overlay.
|
||||||
|
fps_on: bool;
|
||||||
|
fps: f32;
|
||||||
|
|
||||||
// Where the grid sits + the touch↔cell mapping. Recomputed each render /
|
// Where the grid sits + the touch↔cell mapping. Recomputed each render /
|
||||||
// event from the current frame so the hit-test matches what was drawn.
|
// event from the current frame so the hit-test matches what was drawn.
|
||||||
@@ -601,6 +615,21 @@ BoardView :: struct {
|
|||||||
ctx.add_text(bfr, btxt, bfont, BANNER_BTN_TEXT);
|
ctx.add_text(bfr, btxt, bfont, BANNER_BTN_TEXT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FPS dev overlay (P20.1): a small "FPS n" readout pinned to the top-left of
|
||||||
|
// the safe area, on top of everything. Drawn only when fps_on (the M3TE_FPS
|
||||||
|
// pin) is set, so the unset render path is byte-identical. A bright halo under
|
||||||
|
// the dark text keeps the digits legible over the light background art.
|
||||||
|
render_fps_overlay :: (self: *BoardView, ctx: *RenderContext, frame: Frame) {
|
||||||
|
n := cast(s64) (self.fps + 0.5);
|
||||||
|
txt := format("FPS {}", n);
|
||||||
|
sz := measure_text(txt, FPS_FONT);
|
||||||
|
x := frame.origin.x + self.safe.left + FPS_PAD;
|
||||||
|
y := frame.origin.y + self.safe.top + FPS_PAD;
|
||||||
|
f := Frame.make(x, y, sz.width, sz.height);
|
||||||
|
ctx.add_text(Frame.make(f.origin.x + 1.0, f.origin.y + 1.5, f.size.width, f.size.height), txt, FPS_FONT, FPS_TEXT_SH);
|
||||||
|
ctx.add_text(f, txt, FPS_FONT, FPS_TEXT);
|
||||||
|
}
|
||||||
|
|
||||||
// Restart action behind the banner's button: reseed the SAME starting level
|
// Restart action behind the banner's button: reseed the SAME starting level
|
||||||
// through the model (board.restart) and drop every transient view layer
|
// through the model (board.restart) and drop every transient view layer
|
||||||
// (selection, in-flight drag, move animation, FX, and the per-gem landing
|
// (selection, in-flight drag, move animation, FX, and the per-gem landing
|
||||||
@@ -694,6 +723,12 @@ impl View for BoardView {
|
|||||||
if self.banner_up() {
|
if self.banner_up() {
|
||||||
self.render_banner(ctx, level_status(self.board));
|
self.render_banner(ctx, level_status(self.board));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 7. FPS dev overlay (P20.1), on top of everything. Off by default; only
|
||||||
|
// renders when M3TE_FPS pinned it on, so the unset path is unchanged.
|
||||||
|
if self.fps_on {
|
||||||
|
self.render_fps_overlay(ctx, frame);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Touch input. A press records the drag start; the release resolves the
|
// Touch input. A press records the drag start; the release resolves the
|
||||||
|
|||||||
BIN
goldens/p20_fps.png
Normal file
BIN
goldens/p20_fps.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 MiB |
28
main.sx
28
main.sx
@@ -45,6 +45,15 @@ g_viewport_w : f32 = 800.0;
|
|||||||
g_viewport_h : f32 = 600.0;
|
g_viewport_h : f32 = 600.0;
|
||||||
g_safe_insets : EdgeInsets = .{};
|
g_safe_insets : EdgeInsets = .{};
|
||||||
|
|
||||||
|
// FPS dev overlay (P20.1). OFF unless the M3TE_FPS env pin is set, so default
|
||||||
|
// play and every committed golden stay byte-identical. `g_fps_avg_dt` is an
|
||||||
|
// exponential moving average of the per-frame delta, smoothed so the readout
|
||||||
|
// doesn't jitter wildly; the displayed FPS is its reciprocal. Both are only
|
||||||
|
// touched on the gated path, so the unset path is unchanged.
|
||||||
|
FPS_DT_SMOOTH :f32: 0.9; // weight on the running average vs. this frame's delta
|
||||||
|
g_fps_on : bool = false;
|
||||||
|
g_fps_avg_dt : f32 = 0.016;
|
||||||
|
|
||||||
// iOS-only concrete handles kept alongside the boxed `g_plat` so the frame loop
|
// iOS-only concrete handles kept alongside the boxed `g_plat` so the frame loop
|
||||||
// can reach the CAMetalLayer pointer / pixel dims without going through the
|
// can reach the CAMetalLayer pointer / pixel dims without going through the
|
||||||
// protocol box.
|
// protocol box.
|
||||||
@@ -96,7 +105,8 @@ g_banner_prev_up : bool = false;
|
|||||||
// Rebuilt each frame inside the pipeline's arena; carries the current safe-area
|
// Rebuilt each frame inside the pipeline's arena; carries the current safe-area
|
||||||
// insets so the grid stays inside the notch / home-indicator region.
|
// insets so the grid stays inside the notch / home-indicator region.
|
||||||
build_ui :: () -> View {
|
build_ui :: () -> View {
|
||||||
BoardView.{ board = g_board, assets = g_assets, sel = g_sel, drag = g_drag, anim = g_anim, fx = g_fx, fxassets = g_fxassets, motion = g_motion, safe = g_safe_insets, seed = BOARD_SEED }
|
fps : f32 = if g_fps_avg_dt > 0.0 then 1.0 / g_fps_avg_dt else 0.0;
|
||||||
|
BoardView.{ board = g_board, assets = g_assets, sel = g_sel, drag = g_drag, anim = g_anim, fx = g_fx, fxassets = g_fxassets, motion = g_motion, safe = g_safe_insets, seed = BOARD_SEED, fps_on = g_fps_on, fps = fps }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deterministic capture (P6.3). The idle loop is always-on, so a live screenshot
|
// Deterministic capture (P6.3). The idle loop is always-on, so a live screenshot
|
||||||
@@ -200,6 +210,14 @@ frame :: () {
|
|||||||
g_viewport_h = fc.viewport_h;
|
g_viewport_h = fc.viewport_h;
|
||||||
g_safe_insets = g_plat.safe_insets();
|
g_safe_insets = g_plat.safe_insets();
|
||||||
|
|
||||||
|
// FPS dev overlay (P20.1): advance the smoothed frame-time average ONLY when
|
||||||
|
// the env pin enabled it, so an unset run never touches this and renders
|
||||||
|
// byte-identically. delta_time is real wall-clock even when M3TE_ANIM_TIME
|
||||||
|
// pins the animation clock, so the readout is live while the scene is frozen.
|
||||||
|
if g_fps_on and g_delta_time > 0.0 {
|
||||||
|
g_fps_avg_dt = g_fps_avg_dt * FPS_DT_SMOOTH + g_delta_time * (1.0 - FPS_DT_SMOOTH);
|
||||||
|
}
|
||||||
|
|
||||||
if fc.viewport_w != g_pipeline.screen_width or fc.viewport_h != g_pipeline.screen_height {
|
if fc.viewport_w != g_pipeline.screen_width or fc.viewport_h != g_pipeline.screen_height {
|
||||||
g_pipeline.resize(fc.viewport_w, fc.viewport_h);
|
g_pipeline.resize(fc.viewport_w, fc.viewport_h);
|
||||||
}
|
}
|
||||||
@@ -380,6 +398,14 @@ main :: () -> void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FPS dev-overlay hook (P20.1): a non-zero M3TE_FPS turns on the corner FPS
|
||||||
|
// readout. Default (unset / =0) leaves it off, so normal play and every
|
||||||
|
// committed golden stay byte-identical. Purely a render overlay — no board /
|
||||||
|
// score / move / animation state changes and it never gates input.
|
||||||
|
if fp := read_env("M3TE_FPS") {
|
||||||
|
if parse_s64(fp) != 0 { g_fps_on = true; }
|
||||||
|
}
|
||||||
|
|
||||||
// Match-FX capture hook (P11.1). The bursts/popups spawn off a committed move,
|
// Match-FX capture hook (P11.1). The bursts/popups spawn off a committed move,
|
||||||
// which the sim can't script (no public touch injection), so M3TE_FX forces a
|
// which the sim can't script (no public touch injection), so M3TE_FX forces a
|
||||||
// representative match at startup the same way a swipe would: it commits the
|
// representative match at startup the same way a swipe would: it commits the
|
||||||
|
|||||||
Reference in New Issue
Block a user