Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions docs/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,17 @@ scene whose 2D ends at the screen's edge stays 4:3. The first game's issue #71 h
choice"): 16:9 is the widest the first game's scenes were checked at, and a 16:10 screen gets a 16:10 picture instead
of a 16:9 one with bars.

## More texture room: Psy-Q's 2 MB VRAM layout, textures only (2026-10-26)
A game that draws more than its PS1 screen held needs textures the 1 MB VRAM has no room for (the first game's field:
3 more 128x128 map tiles for a 16:9 picture, the first game's issue #78). `port_video_vram_extend` makes `gpu.c`'s
VRAM 1024x1024 in Psy-Q's 2 MB layout: its `getTPage` already puts y's bit 9 into the texture page's bit 11 and
`getClut` into the CLUT's bit 15, so the game's code reaches rows 512-1023 with its own macros, and the 1 MB GPU (every
build that does not ask) ignores both bits as before. Only textures and CLUTs live there: the drawing
area and the display stay in rows 0-511 (E3/E4 have 9 bits of y), so the hardware renderer's target keeps its size and
only its VRAM copy, which every texel comes from, grows. Rejected: a second texture memory behind a port-only command
(a new primitive tag, another sampler, the same cost for nothing more), and room found in the game's own VRAM (what a
stage overlay loads varies by map, so every map would have to be measured, and a later change could take it).

## Texture keys by content, SHA-1, per VRAM word (2026-10-08)
Texture replacement (docs/PORT.md "Texture replacement") names a texture by the transfer that loaded it and by its
CLUT's content, never by VRAM position: the first game loads one image at up to 11 places and reuses places for
Expand Down
3 changes: 2 additions & 1 deletion docs/GAME_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ C the game compiles into the port alongside the runtime. It implements:
the stack's own mods (fast-forward and faster loading, which every game gets) belong to the stack. The manifests (`mods/<id>/mod.json`) sit in the game
repo and the build copies them next to the binary; `port_video_widescreen_enable()` and `port_video_widescreen(on)`
let a game mod show the scenes it names wide, at the window's aspect up to 16:9 (the hardware renderer's wide
canvas: docs/PORT.md "Rendering");
canvas: docs/PORT.md "Rendering"), `port_video_widescreen_pad()` how far past the display's edges its 2D may draw,
and `port_video_vram_extend()` gives it a 1024x1024 VRAM for the textures that takes;
`port_fast_forward_request()` lets a game mod ask for
fast-forward.
- **Save states:** `game_savestate(PortState *s)` names the adapter's own state that lives across vsyncs outside the
Expand Down
7 changes: 5 additions & 2 deletions docs/PORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ and the shim lacks stops the run with `port_unimplemented(name)` (status 3) and
against the PS1 or an emulator) are listed in `psyq/README.md` "Behaviour assumed".

## Rendering
- **Software GPU** (`psyq/gpu.c`): a 1024x512 VRAM of 16-bit pixels and every GP0 drawing command the primitive types produce: flat/Gouraud/textured polygons with 4/8/15-bit textures and CLUTs, texture windows,
- **Software GPU** (`psyq/gpu.c`): a 1024x512 VRAM of 16-bit pixels (1024x1024 for a game that asks for more texture room: `port_video_vram_extend`, gpu.c "The extension") and every GP0 drawing command the primitive types produce: flat/Gouraud/textured polygons with 4/8/15-bit textures and CLUTs, texture windows,
the four blend modes, dithering, mask bits, lines, rectangles, fills, VRAM copies and transfers, the draw area and
offset. Exact VRAM semantics matter: games reuse VRAM (`MoveImage`, `DR_MOVE` cursors with `BreakDraw`/`ContinueDraw`). It is built at `-O3` in every build.
- **Video output** (`runtime/video.c`): every vsync the display area set by `PutDispEnv` is read from the VRAM in 15-
Expand Down Expand Up @@ -426,7 +426,10 @@ against the PS1 or an emulator) are listed in `psyq/README.md` "Behaviour assume
at that aspect inside the window (a new window opens 16:9), placed by `video.scaling` (integer: at the canvas's
own aspect, pixel for pixel); `--gpu-screenshot` gives
the wide image; the software
renderer, `--screenshot` and the frame hash stay 4:3.
renderer, `--screenshot` and the frame hash stay 4:3. A game whose 2D can draw past the screen's edges (a tiled
map) asks `port_video_widescreen_pad()` for the canvas's columns past each side (0 while the picture is 4:3) and
draws that far; the extra textures that takes may go in the extended VRAM (`port_video_vram_extend`: rows 512-1023
hold textures and CLUTs, in the renderer's VRAM copy, not in its target).
- The software GPU stays the reference and the default; every existing test uses it.

## Texture replacement
Expand Down
4 changes: 4 additions & 0 deletions docs/RUNTIME.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ unfiltered (docs/PORT.md "Rendering").
`port_video_widescreen(1)` (every vsync of the scenes that widen), the hardware renderer presents those scenes 16:9
inside the window, the rest 4:3; the window opens 16:9 when the mod is on and `video.renderer` is `gpu`. With the
software renderer the picture stays 4:3 and the run logs `widescreen: needs the GPU renderer` once.
`port_video_widescreen_pad()` is the canvas's columns past each side of the display as of the last vsync (53 for a
320-wide display at 16:9, 0 while the picture is 4:3), for a game whose 2D can draw that far; `port_video_vram_extend()`
(from a mod's start) makes the VRAM 1024x1024 for the extra textures (`psyq/gpu.c` "The extension"); a save state then
holds the extra rows (`gpu_vram_ext`), and a state without them does not load.
`<PREFIX>_PORT_PRESENT_READBACK=FRAME:PATH` reads SDL_Renderer's output back (a game's renderer test compares the two
present paths). `<PREFIX>_PORT_GPU_VRAM_CHECK=N` compares the rasteriser's whole target with the software VRAM every N
vsyncs and logs any difference (with `--renderer gpu --window`, or `--gpu-screenshot` headless).
Expand Down
12 changes: 11 additions & 1 deletion include/psxstack/port_harness.h
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,19 @@ void port_fast_forward_request(int on);
/* video.c: widescreen (GAME_CONTRACT.md "4. The adapter units", the mods; render_gpu_wide.c). A game mod that widens
* some scenes calls port_video_widescreen_enable from its start (before the window opens: with the GPU renderer a new
* window opens 16:9 and the rasteriser keeps a wide canvas) and port_video_widescreen every vsync: on while the
* scene's 3D reaches past the display's edges, off elsewhere. The software renderer stays 4:3 (logged once). */
* scene's 3D reaches past the display's edges, off elsewhere. The software renderer stays 4:3 (logged once).
* port_video_widescreen_pad: the columns the picture shows past each side of the display (P: 53 for a 320-wide display
* at 16:9, 32 at 16:10), as of the last vsync; 0 while the picture is 4:3 (off, the software renderer, a 4:3 window).
* A game whose 2D can draw past the display's edges (the canvas shows what lies there: render_gpu_wide.c) draws that
* far while it is non-zero.
* port_video_vram_extend: the VRAM grows to 1024x1024 (psyq/gpu.c "The extension": Psy-Q's 2 MB VRAM layout), more
* texture room for a game that draws more than its PS1 VRAM held (rows 512-1023: textures and CLUTs only, through
* getTPage's and getClut's y bit 9). Called from a mod's start; it stays for the run, and a save state of the run has
* the extra rows (one without them does not load). */
void port_video_widescreen_enable(void);
void port_video_widescreen(int on);
int port_video_widescreen_pad(void);
void port_video_vram_extend(void);
long port_pace_get(void);
int port_video_available(void);
int port_video_screenshot_add(const char *spec); /* "FRAME:PATH"; 0 when malformed (or too many) */
Expand Down
2 changes: 1 addition & 1 deletion psyq/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ per frame") plugs into; the runtime may ignore them.
| Library | Real (computes the right answer) | Fixed answer / recorded only |
|---|---|---|
| LIBGPU `libgpu.c` | `SetDefDispEnv`, `SetDefDrawEnv` (`dfe = h < 289`), `SetDrawEnv` (the PS1's packet: E3, E4 clamped to the VRAM, E5, E1, E2, E6, and with `isbg` a TILE over the clip area), `PutDrawEnv` (the same packet, but a fill when the clip's x and width are multiples of 64; terminated and drawn at once, part of the frame's primitive stream), `SetDrawTPage`, `SetDrawMove`, `SetDrawArea` (E3/E4 clamped), `SetTexWindow`, `SetDrawStp`, `SetDrawMode` (E1 + E2, a 0 word for no window), `GetTPage`, `GetClut`, `SetSemiTrans`, `SetShadeTex`, the function forms `SetSprt`, `SetPolyF3/FT3/G3/GT3/F4/FT4/G4/GT4`, `SetLineF2/F3/F4/G2/G3/G4`, `SetSprt8/16`, `SetTile`, `SetTile1/8/16` (the macros' length and command bytes), `AddPrim` (the tag mechanism: `addPrim`), `MargePrim` (-1 past 16 words), `ClearOTag`, `ClearOTagR`; `DrawOTag`/`ContinueDraw` walk the list, hash the primitives and draw them; `LoadImage`/`LoadImage2`, `MoveImage`/`MoveImage2` (-1 and nothing for an empty rectangle), `ClearImage`/`ClearImage2` (a fill, or E1 + a TILE when x or w is not a multiple of 64, as LIBGPU does) into the VRAM, `StoreImage`/`StoreImage2` from it (wrapping at its edges); `OpenTIM`/`ReadTIM` (the CLUT and image blocks of TIMs back to back, NULL at the end); `GetDispEnv` (the last `PutDispEnv`'s, all 0xFF after `ResetGraph` 0/3 or `SetDispMask(0)`); `ResetGraph` 0/3 resets the drawing state; `BreakDraw` NULL (idle, as on the PS1); `psyq_gpu_vram`/`psyq_gpu_display` (the video output, `psyq.h`) | `DrawSync` 0, `IsIdleGPU` 0 (drawing completes when queued: the `2` forms are the same calls), `SetGraphDebug` 0, `SetDispMask`, `PutDispEnv` (returns env, stores the display) |
| GPU `gpu.c` | The GPU in software (session 16, M2): a 1024x512 VRAM of 16-bit pixels and every GP0 drawing command (polygons flat/Gouraud/textured with 4/8/15-bit textures and CLUTs, the texture window, raw and modulated texels, the four blend modes, dithering, mask set/check, lines and polylines, rectangles, fill, VRAM copy, CPU-to-VRAM transfer; E1..E6), the draw area and offset; the emulator's rasterisation rules taken over where documentation is silent (`gpu.c`'s header) | GP1 beyond the drawing state; timing, the texture cache, VRAM-to-CPU |
| GPU `gpu.c` | The GPU in software (session 16, M2): a 1024x512 VRAM of 16-bit pixels (1024x1024 extended: textures and CLUTs in rows 512-1023, Psy-Q's 2 MB layout) and every GP0 drawing command (polygons flat/Gouraud/textured with 4/8/15-bit textures and CLUTs, the texture window, raw and modulated texels, the four blend modes, dithering, mask set/check, lines and polylines, rectangles, fill, VRAM copy, CPU-to-VRAM transfer; E1..E6), the draw area and offset; the emulator's rasterisation rules taken over where documentation is silent (`gpu.c`'s header) | GP1 beyond the drawing state; timing, the texture cache, VRAM-to-CPU |
| GTE `gte.c` | The geometry coprocessor in software (session 16): the 64 registers with their read/write rules, every command (RTPS/RTPT with the UNR division, NCLIP, OP, DPCS/DPCT, INTPL, MVMVA, NCDS/NCDT, CDP, NCCS/NCCT, CC, NCS/NCT, SQR, DCPL, AVSZ3/4, GPF, GPL), FLAG, the saturations; the game's `gte_*` macros call it (`tools/port_gen.py overrides` translates each MIPS sequence of `include/psyq/gtemac.h` into the same register accesses: `psyq_gte_mtc2/mfc2/ctc2/cfc2/cmd`, declared in `psyq_internal.h`; a `swc2` is `psyq_gte_swc2_`). Beside SXY, `gte_shadow.c` keeps the fractions RTPS cuts away for the hardware renderer (`docs/PORT.md` "Sub-pixel precision") | — |
| LIBGTE `libgte.c` | `rsin`, `rcos` (computed 4096-entry table), `RotMatrixYXZ_gte`, `RotMatrixZYX_gte` (LIBGTE's GPF sequence), `ScaleMatrix`, `ApplyMatrixSV` (MVMVA); the register setters write the GTE: `InitGeom` (ZSF3/4, H, DQA/DQB, OFX/OFY), `SetGeomOffset`, `SetBackColor`, `SetGeomScreen` (LIBGS's `GsSetProjection`), `SetRotMatrix`, `SetLightMatrix`, `SetTransMatrix`, and for LIBGS (not called yet: see below) `SetFarColor`, `SetColorMatrix`; for LIBGS's view: `MulMatrix`, `MulMatrix2`, `ApplyMatrixLV` (MVMVA), `TransposeMatrix`, `SquareRoot0` (LZCS/LZCR and the table). For the second game (each issues the commands of LIBGTE's own objects, in their order, with their stores and FLAG): `RotTransPers`/`3`/`4`, `RotTrans`, `RotAverage4`, `RotNclip3`/`4`, `RotAverageNclip3`/`4` (RTPS/RTPT, NCLIP, AVSZ3/4), `NormalColorCol`/`3` (NCCS/NCCT), `MulMatrix0`, `CompMatrix` (MVMVA, the translation through MAC), `PushMatrix`/`PopMatrix` (RT and TR, 20 deep), `TransMatrix`, `VectorNormal`, `MatrixNormal` (SQR, OP, GPF, the leading-zero count and the 1/sqrt table), `RotMatrix`, `RotMatrixYXZ` (on the CPU, every product floored), `csqrt` (the hyperbolic CORDIC), `ratan2` (the atan table), `catan` (the circular CORDIC: see below) | — |
| LIBGS `libgs.c`, `libgs_sort.c` | `GsGetTimInfo` (parses the TIM header); `GsSetProjection` (H); `GsSetRefView2` (the world-screen matrix from viewpoint, reference point, twist and `super`), `GsGetLw` (the coordinate hierarchy and its `flg`/PSDCNT cache) and `GsSetFlatLight` (the light and light colour matrices, LCM), checked against the PS1 by the layer-1 family `libgs_view` (issues #7, #19: the battle camera and lights; `tests/host/libgs_replay.py`, with LIBGTE's `MulMatrix`/`ApplyMatrixLV`/`TransposeMatrix`/`SquareRoot0`); `GsInitGraph`'s GTE set-up and matrices (the view base with the aspect in `m[1][1]`, `GsIDMATRIX`, the zero light matrices, PSDCNT 1); owns `GsWSMATRIX`, `GsLIGHTWSMATRIX`, `GsIDMATRIX`, `GsFCALL4`, `GsOUT_PACKET_P`. For the second game (each with the calls and order of the PS1's LIBGS 4.x objects; `tests/psyq_test.py` checks hand-worked views, a three-level hierarchy, a synthetic TMD sorted and drawn): `GsGetLs`/`GsGetLws` (GsGetLw's walk, then `GsWSMATRIX`), `GsInitCoordinate2`, `GsMulCoord3`, `GsSetLsMatrix` (RT, TR), `GsSetLightMatrix` (LLM = `GsLIGHTWSMATRIX` x m, the GTE's rotation kept), `GsSetAmbient` (BK = c >> 4), `GsSetLightMode` (0..3), `GsSetWorkBase`/`GsGetWorkBase`, `GsInit3D` (the screen's centre to the GTE, or with GsOFSGPU to the drawing environment), `GsSwapDispBuff` (puts LIBGS's display and drawing environments, counts PSDCNT, alternates the buffers), `GsMapModelingData` (the host's form: offsets from each object's entry, `libgs.h`), `GsLinkObject4` (the entry; the primitive runs' lengths), `GsSortObject4` (the attribute's GsDOFF/GsALON/GsDIV/GsLOFF/GsLLMOD, the run through its `GsFCALL4` entry) and the 18 fast handlers `GsTMDfast{F3L,G3L,TF3L,TF3NL,TG3L,TG3NL,TNF3,TNG3,F4L,F4NL,NF4,G4L,TF4L,TF4NL,TG4L,TG4NL,TNF4,TNG4}` (RTPT, NCLIP, RTPS, AVSZ3/4, NCCS/NCCT and the packet words in the PS1's order) | `GsInitGraph` records the rest (no GPU reset, no PutDrawEnv/PutDispEnv: the games draw with their own); the 9 subdividing handlers `GsTMDdiv*` call `port_unimplemented` (none is reached by a game yet); a `GsFCALL4` entry left 0 calls `port_unimplemented` (the PS1 jumps to 0) |
Expand Down
Loading
Loading