Skip to content

feat: show the Pocket3D title card at launch on PS Vita, PSP and 3DS - #35

Merged
doodlewind merged 2 commits into
mainfrom
feat/pocket3d-title
Oct 5, 2026
Merged

doodlewind merged 2 commits into
mainfrom
feat/pocket3d-title

Conversation

@doodlewind

@doodlewind doodlewind commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

OpenStrike moves to the current PocketJS and plays the Pocket3D title card when it starts on PS Vita, PSP and Nintendo 3DS: the mark and the wordmark on the plum ground, 144 ticks at 60 Hz (2.4 s), drawn by the CPU into the display frame buffer before the renderer is initialized.

What changed

vendor/pocketjs moves 1c2002a2 → 757e90ec (five commits: a Vita GXP alignment fix, the 3d.pocket.nexus site, the pocket3d-title crate, the Pocket3D License). None of them changes a file under hosts/; crates/openstrike-3ds/host.patch applies to the same host sources as before.

Console Call Where Notes
PS Vita pocket3d_title::vita::play() crates/openstrike-vita/src/wired.rs, first statement of run(), before graphics::init_with_pool main() only calls wired::run(), so this is the one launch path. Dependency added under the Vita target.
PSP title() crates/openstrike-psp/src/main.rs, in run() after the clock is set and before host::init_graphics Draws 8888 into the uncached mirror of video memory (sceGeEdramGetAddr() | 0x4000_0000), 480 x 272, stride 512. Zeroes those bytes when the card has ended (see below).
Nintendo 3DS pocket3d_title_play() crates/openstrike-3ds/host.patch, after gfxInitDefault() and before C3D_Init The patch also adds #include <pocket3d_title.h>; crates/openstrike-3ds/Makefile adds the crate's include/ directory. The vendored host stays unmodified.

README.md gains a "Pocket3D title card" section: the credit with a link to https://3d.pocket.nexus, the three call sites, and the rule that the card is not skipped, shortened, recoloured or redrawn here. The repository has no AGENTS.md or CLAUDE.md, so the rule lives there.

PSP hand-over. The card ends on black pixels whose fourth byte is 255. With the default 16-bit frame buffer the GE's display buffer occupies the same bytes and is shown from sceGuDisplay(true) until the first frame is drawn, so it would read them as alternating 0x0000 / 0xff00 columns. title() therefore fills the bytes it used with zero before it returns.

No skip switch. None of the three hosts reads a development boot switch before graphics start, so every build plays the card, capture and benchmark builds included. Capture frame indices are counted in the host loop and are not shifted by it (checked on PSP, below).

Platforms without the card

These builds have no drawer in pocket3d-title for their display path and do not show the card yet. This PR adds none.

  • Desktop (crates/openstrike, wgpu)
  • iPod touch 4 (crates/openstrike-ipod)
  • Nokia E7 (crates/openstrike-symbian)
  • The static map viewer (site/map-viewer). pocket3d-title ships a browser playTitle(); the viewer does not call it here.

domain/crates/gu-demo and the pocket3d-vita fullscreen_sky example are renderer bring-up utilities and are unchanged.

License

The pinned PocketJS puts Pocket3D (pocket3d/, devices/, engine/pocket3d/ in vendor/pocketjs) under the Pocket3D License 1.0: MIT's grant plus the condition that a distributed product drawing 3D scenes with Pocket3D shows the title card at each start. OpenStrike's own LICENSE (MIT) is unchanged. For a platform without a drawer the license asks for the mark and wordmark from the artwork for at least two seconds (condition 2b); the four builds listed above do not do that yet.

Verification

Everything below ran on a macOS host. Nothing was run on a console.

Cooked maps (<maps>: the eight classic maps and wwdc24-parkour.p3d) were supplied from a local directory; each build verified them with pocket3d-cook --verify-cooked.

Host builds

Command Result
bun scripts/psp.ts --release --cooked-maps <maps> --package pass: EBOOT.PBP, 5,263,106 bytes
OPENSTRIKE_COOKED_MAPS=<maps> bun scripts/vita.ts --release pass: dist/vita/OpenStrike.vpk, 26,949,949 bytes (USB debugging on, the default); pocket3d_title::draw, sceDisplaySetFrameBuf and sceKernelAllocMemBlock are in the ELF
OPENSTRIKE_COOKED_MAPS=<maps> bun run build:3ds pass: dist/3ds/openstrike.3dsx, 3,636,052 bytes; pocket3d_title_draw and pocket3d_title_art_half are in the link map
OPENSTRIKE_COOKED_MAPS=<maps> bun scripts/3ds.ts --capture pass (compile and link of the capture configuration only)
bun run build:3ds --pocket-only pass

Tests the workflows run (Rust stable 1.98.0)

CI on this PR (commit 518d186): all seven jobs pass, including bun run test:3ds with the lease test and the iPod cargo miri test job. The table is the local run.

Workflow Command Result
simulation cargo test --locked -p openstrike-core -p openstrike-character -p openstrike-mods -p openstrike-companion pass
simulation cargo check --locked -p openstrike-core --no-default-features --features libm pass
simulation cargo check --locked --manifest-path crates/openstrike-symbian/Cargo.toml --lib pass
simulation cargo test --locked --manifest-path crates/openstrike-vita/Cargo.toml --lib pass
simulation bun test test/strike-sdk.test.ts test/openstrike-vita-artwork.test.ts pass (3)
domain cargo test --locked --manifest-path domain/Cargo.toml pass
domain cargo check --locked --manifest-path domain/Cargo.toml -p pocket3d-bsp --no-default-features --features libm pass
domain cargo test --locked --manifest-path domain/crates/pocket3d-vita/Cargo.toml pass
domain cargo test --locked --manifest-path domain/crates/pocket3d-gles2/Cargo.toml pass
domain cargo check --locked -p openstrike pass
domain bun test test/cook-map.test.ts test/symbian-tooling.test.ts test/psp-toolchain.test.ts pass (15)
ipod bun run test:ipod pass
ipod cargo check --locked --manifest-path crates/openstrike-ipod/Cargo.toml --features native-check pass
3ds, ipod bun run check:platforms (includes bun run typecheck) pass
3ds bun run test:3ds 1 test failed locally: test/3ds-device-lease.test.ts reported Device busy: 3ds:wire, because that lease was held by another process on the machine. It was not retried locally; it passes in CI.
3ds the rest of test:3ds, run separately: python3 -B -m unittest discover -s test -p test_3ds_deploy.py; bun test test/3ds-tooling.test.ts test/tactical-touch.test.ts; cargo test --locked --manifest-path crates/openstrike-3ds/Cargo.toml pass (4; 11; 9)
psp-map-read bun test test/psp-map-read.test.ts test/psp-memory.test.ts pass (4)

test/3ds-tooling.test.ts applies host.patch to the pinned host, so it covers the extended patch.

Emulator: PPSSPPHeadless, software renderer, at the commit in test/goldens-psp/PPSSPP-COMMIT.txt

Check Result
OPENSTRIKE_COOKED_MAPS=<maps> bun scripts/e2e-psp.ts, liveness pass: the capture EBOOT plays the card, boots and dumps every frame of the four cases (4, 44, 12 and 30 frames)
Same run, golden comparison fails, 8 of 8 shots, with and without this change. The goldens were last recorded on 2026-09-18 with the 32-bit frame buffer; scripts/psp.ts has defaulted to the 16-bit buffer since 4b528d5 (2026-09-20). A captured frame has 611 colours, its golden 12,247.
Frames with the card against frames of the same tree with the title(); call removed byte-identical, 8 of 8 shots, in the default 16-bit build and in a --framebuffer32 build. The 32-bit frames are within 2 to 405 of 130,560 pixels of the goldens in both.
The card itself, from a temporary instrumented build (not committed) that dumps video memory at tick 60 FNV-1a 64 of the 512 x 272 x 4 bytes is 0x6605dcb9c1790b74, the PSP frame hash recorded in pocket3d-title's tests. The display frame buffer reads back as 0x44000000, width 512, format 8888, and the present closure ran 144 times.

Not done

  • Not run on a PSP, a PS Vita or a Nintendo 3DS. The fade timing, the hand-over to GXM / the GE / citro3d and the first frames after the card are unobserved on hardware. The PSP zero fill is reasoned from the buffer layout and the dumped end-of-card bytes, not seen on a screen.
  • bun scripts/e2e-vita.ts (Vita3K) and bun run test:e2e:3ds (Azahar) were not run; both open an emulator window. Their capture builds now play the card before frame 0.
  • The cargo miri test job of the iPod workflow was not run locally (it passes in CI).
  • The PSP goldens were not re-baselined.

🤖 Generated with Claude Code

vendor/pocketjs moves from 1c2002a2 to 757e90ec: a Vita GXP alignment fix,
the 3d.pocket.nexus site, the pocket3d-title crate, and the Pocket3D License
1.0 for pocket3d/, devices/ and engine/pocket3d/. That license makes the
title card a condition of distributing a game that draws with Pocket3D, so
the three console builds play it first at every launch, before the renderer
starts: the mark and the wordmark, 144 ticks at 60 Hz.

- vita: pocket3d_title::vita::play() in wired::run, before
  graphics::init_with_pool. main() has no other path into the game.
- psp: title() in main.rs draws into the uncached mirror of video memory in
  8888, before host::init_graphics. It zeroes those bytes afterwards: the
  card ends on [0, 0, 0, 255] pixels, which the GE's 16-bit display buffer
  would show as 0x0000/0xff00 columns until the first frame is drawn.
- 3ds: host.patch includes <pocket3d_title.h> and calls
  pocket3d_title_play() after gfxInitDefault(), before C3D_Init. The
  Makefile adds the crate's include directory.

No build flag or boot switch skips the card: capture and benchmark builds
play it too. The desktop, iPod touch 4 and Nokia E7 builds and the static
map viewer have no call and do not show it. OpenStrike's own LICENSE is
unchanged.

Verified on the host, not on a console:
- release builds of the PSP EBOOT, the Vita VPK and the 3DS .3dsx, and the
  3DS capture configuration;
- the cargo and bun suites the workflows run. test/3ds-device-lease.test.ts
  takes the 3ds:wire lease and is left to CI;
- under PPSSPPHeadless the capture EBOOT dumps every frame of the four e2e
  cases, those frames equal the frames of a build without the call byte for
  byte, and video memory at tick 60 hashes to the PSP frame pocket3d-title
  records (0x6605dcb9c1790b74).

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

vendor/pocketjs moves from 757e90ec to 223be88d on PocketJS main. It carries
the title card with its name in plain white (pocketjs#531) and the fix this
integration prompted (pocketjs#532): after its last frame the card sets its
surface to zero bytes, so the GE's 16-bit display buffer on the same video
memory is black until the first frame. The PSP host drops its own fill.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@doodlewind
doodlewind marked this pull request as ready for review October 5, 2026 02:19
@doodlewind
doodlewind merged commit ef701ca into main Oct 5, 2026
7 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.

1 participant