Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c138d54
feat(playset): scene3d surface contract + client + sim host, math lib…
doodlewind Jul 10, 2026
8ba5c55
feat(playset): port all 74 GameBlocks modules across 7 categories
doodlewind Jul 11, 2026
75bc06f
feat(playset): rally demo E2E, module catalog, skill + docs
doodlewind Jul 11, 2026
01872d8
feat(spec): PROP.scene3d + DRAW_OP.sceneQuad — host-composited 3D bac…
doodlewind Jul 11, 2026
ab90608
feat(scene3d): pocket-scene3d native core — rally renders on the desk…
doodlewind Jul 11, 2026
6024b0f
chore(rally): move milestone screenshots next to the demo
doodlewind Jul 11, 2026
370de45
feat(scene3d-psp): sceGu backend — the same rally bundle renders on t…
doodlewind Jul 11, 2026
9d25615
feat(demos): wave-1 GameBlocks game ports — runner, dogfight, snake
doodlewind Jul 11, 2026
0f387c8
fix(psp): pin 333/166 MHz clock + hardware perf channels
doodlewind Jul 20, 2026
51dc5ca
perf(playset): collision broadphase + static-scenery flush skip + ral…
doodlewind Jul 20, 2026
0e70e61
perf(playset): native sim cores — rally goes from 1.5 fps to 60 on a …
doodlewind Jul 20, 2026
04f89c5
Merge remote-tracking branch 'origin/main' into feat/playset
doodlewind Jul 20, 2026
793cd72
fix(psp): put -O2 where cc-rs will actually use it
doodlewind Jul 20, 2026
c40c1d0
chore(psp): repin the toolchain manifest, vita and lockfiles to ba5bdd0
doodlewind Jul 20, 2026
5df8c78
fix(playset): wheel offsets travel with the binding, not read back fr…
doodlewind Jul 20, 2026
6c0a041
feat(scene3d): freeze() — declare scenery immobile so the host can ba…
doodlewind Jul 21, 2026
dda8b29
feat(scene3d): dispatch freeze on both hosts
doodlewind Jul 21, 2026
e55ea9f
perf(scene3d): merge frozen scenery into per-cell batches
doodlewind Jul 21, 2026
0bad62f
perf(playset): createHudSignals — remove the HUD fan-out trap for eve…
doodlewind Jul 21, 2026
25a0419
perf(demos): split dogfight's terrain, clip it at the fog wall, pin r…
doodlewind Jul 21, 2026
2040bf7
refactor(playset): GameWorld enum — the scalable seam for native sims
doodlewind Jul 22, 2026
fdded42
perf(snake): native grid sim — the AI's ~750ms tick becomes microseconds
doodlewind Jul 22, 2026
2eb10e5
fix(snake): don't freeze sim-driven nodes — the body stopped followin…
doodlewind Jul 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/psp-toolchain.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
"quickJsRs": {
"repository": "https://github.com/pocket-stack/quickjs-rs.git",
"rev": "0fc946fb670c0c29bc0135f510bcb0f595415a61"
"rev": "ba5bdd0dc013518768e76cd9e05cd30ed53dd35b"
},
"cargoPsp": {
"package": "cargo-psp",
Expand Down
19 changes: 19 additions & 0 deletions core/src/draw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,25 @@ impl<'a> Walker<'a> {
return;
}

// -- host surface backdrop (PROP.scene3d) ----------------------------
// Emitted BEFORE background/children: the host composites the bound
// 3D scene as this node's backdrop layer at its world AABB (viewports
// are axis-aligned by contract; a rotated ancestor degrades to the
// AABB rather than dropping the scene).
let scene_handle = node
.overrides
.iter()
.find(|&&(p, _)| p == spec::prop::SCENE3D)
.map(|&(_, v)| v)
.unwrap_or(0);
if scene_handle != 0 {
let c = self.world_aabb(&world, l.w, l.h);
dl.words.push(spec::draw_op::SCENE_QUAD);
dl.words.push(xy_word(c.x0, c.y0));
dl.words.push(wh_word(c.x1 - c.x0, c.y1 - c.y0));
dl.words.push(scene_handle);
}

// -- background + shadow --------------------------------------------
let has_grad = r.grad_dir != NO_GRADIENT && r.grad_dir <= spec::GradDir::ToRight as u32;
let bg_color = scale_alpha(r.bg_color, op);
Expand Down
9 changes: 9 additions & 0 deletions core/src/raster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,15 @@ pub fn render_scaled(ui: &Ui, words: &[u32], fb: &mut [u8], scale: u32) {
tex_tri(ui, fb, width, scale, clip, &words[i + 1..i + 12]);
i += 12;
}
draw_op::SCENE_QUAD => {
// Host-composited 3D backdrop — the software raster has no
// scene3d core; the box stays whatever was painted below
// (graceful absence, deterministic for goldens).
if i + 4 > words.len() {
return;
}
i += 4;
}
// The op set is closed per DrawList version; anything else means
// corrupt data — stop instead of misinterpreting the stream.
_ => return,
Expand Down
4 changes: 3 additions & 1 deletion core/src/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ pub mod prop {
pub const ARC_START: u8 = 140;
pub const ARC_SWEEP: u8 = 141;
pub const ARC_WIDTH: u8 = 142;
pub const SCENE3D: u8 = 160;
}

/// How a prop's u32 payload is interpreted (see spec.ts VALUE_KIND).
Expand All @@ -181,7 +182,7 @@ pub const PROP_VALUE_KIND: [u8; 256] = [
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x02, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
Expand Down Expand Up @@ -398,6 +399,7 @@ pub mod draw_op {
pub const SCISSOR_POP: u32 = 6;
pub const TRI: u32 = 7;
pub const TEX_TRI: u32 = 8;
pub const SCENE_QUAD: u32 = 9;
}

/// .pak container constants (byte-compatible with dreamcart's format;
Expand Down
139 changes: 139 additions & 0 deletions demos/dogfight/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// demos/dogfight/app.tsx — "Pocket Dogfight": the playset flight+combat demo.
//
// The pure sim lives in game.ts; this component wires it to the runtime:
// createGameLoop steps the game at a fixed 1/60 s on the virtual clock
// (hz-invariant, DETERMINISM.md), the render callback flushes the Scene3D
// and refreshes one HUD signal, and the cockpit overlay — FlightHud pitch
// tape + data boxes, HeadingRelativeRadar, damage flash, message line —
// composes as ordinary Views over the <Viewport3D>. On hosts without a 3D
// core the viewport is an empty box and the HUD, driven by the same
// deterministic sim, still renders.

import { Show } from "solid-js";
import { Text, View } from "@pocketjs/framework/components";
import { createGameLoop } from "../../playset/loop.ts";
import { createHudSignals } from "../../playset/hud.ts";
import { Viewport3D } from "../../playset/scene3d/viewport.ts";
import { FlightHud, type FlightHudState } from "../../playset/modules/user-interface/flight-hud.ts";
import { HeadingRelativeRadar } from "../../playset/modules/user-interface/heading-relative-radar.ts";
import { createDogfightGame } from "./game.ts";

// spec/spec.ts ENUMS ordinals (stable wire values; same idiom as flight-hud.ts).
const POS_ABSOLUTE = 1;
const ALIGN_CENTER = 1;
const FLEX_COL = 1;

const SCREEN_W = 480;
const SCREEN_H = 272;
const RADAR_SIZE = 58;
const RADAR_RANGE = 3600;

function pad(value: number, width: number): string {
return String(Math.max(0, Math.round(value))).padStart(width, "0");
}

export default function Dogfight() {
const game = createDogfightGame();
// One signal per field, refreshed on the virtual 0.1 s grid — see
// playset/hud.ts for why a single snapshot signal is a trap on this
// interpreter (it re-runs every consumer whether or not its value moved).
const { fields: hud, refresh } = createHudSignals({ read: () => game.hudState() });

let steps = 0;
createGameLoop({
step: (dt, input) => {
game.step(dt, input);
steps += 1;
if (steps % 6 === 0) refresh();
},
render: () => game.scene.flush(),
});

// Status row budget is tight at 480 px: HP rides in the region slot, the
// score is abbreviated, and the time slot stays empty.
// Reads each field through its OWN accessor: FlightHud pulls this inside its
// reactive getters, so a per-field read means a changing airspeed wakes the
// airspeed tape and nothing else.
const hudSource = (): Partial<FlightHudState> => ({
regionName: hud.failed() ? "MISSION FAILED" : `HP ${pad(hud.health(), 3)}`,
speed: hud.speed(),
altitude: hud.altitude(),
agl: hud.agl(),
waveLabel: `WAVE ${hud.waveNumber()}`,
waveDetail: `${hud.banditsAlive()} BANDIT${hud.banditsAlive() === 1 ? "" : "S"}`,
compassHeadingDegrees: hud.headingDeg(),
timeText: "",
scoreText: `SCR ${pad(hud.score(), 4)}`,
throttle: hud.throttle(),
pitchDegrees: hud.pitchDeg(),
rollDegrees: hud.rollDeg(),
weaponLabel: hud.weaponLabel(),
lockStatus: hud.lockStatus(),
gunHeat: hud.gunHeat(),
pullUpWarning: hud.pullUp(),
});

// No bgColor on the viewport: on native hosts the 3D scene composites
// UNDER the ui layer, so the viewport (and its ancestors) stay unpainted.
return (
<Viewport3D scene={game.scene} class="w-full h-full">
<View class="w-full h-full">
<FlightHud state={hudSource} width={SCREEN_W} height={SCREEN_H} />

{/* damage flash — the demo's red vignette as a flat overlay */}
<Show when={hud.damageFlash() > 0.01}>
<View
style={{
posType: POS_ABSOLUTE,
insetL: 0,
insetT: 0,
width: SCREEN_W,
height: SCREEN_H,
bgColor: "#ff2418",
opacity: hud.damageFlash() * 0.3,
}}
/>
</Show>

{/* combat messages (FOX TWO / SPLASH ONE / WAVE N INBOUND) */}
<Show when={hud.message() !== ""}>
<View
style={{
posType: POS_ABSOLUTE,
insetT: Math.round(SCREEN_H * 0.62),
insetL: 0,
width: SCREEN_W,
flexDir: FLEX_COL,
align: ALIGN_CENTER,
}}
>
<Text class="text-sm font-bold tracking-wide" style={{ textColor: "#eafff2" }}>
{hud.message()}
</Text>
</View>
</Show>

{/* heading-relative radar, bottom-right (the demo's scope corner) */}
<View
style={{
posType: POS_ABSOLUTE,
insetR: 8,
insetB: 6,
width: RADAR_SIZE,
height: RADAR_SIZE,
}}
>
<HeadingRelativeRadar
playerPosition={() => hud.playerPosition()}
playerForward={() => hud.playerForward()}
contacts={() => hud.contacts()}
width={RADAR_SIZE}
height={RADAR_SIZE}
range={RADAR_RANGE}
contactColor="#ff4f42"
/>
</View>
</View>
</Viewport3D>
);
}
Loading