From 674729eb7fa522f5b87100d251084701edf6e358 Mon Sep 17 00:00:00 2001 From: Gabriel Scarcella Date: Mon, 26 Oct 2026 12:23:34 -0300 Subject: [PATCH] The VRAM extended to 1024x1024 for textures; the wide canvas's pad for the game A game that draws more than its PS1 screen held may need more texture room than the 1 MB VRAM has (dw2003recomp issue #78: the field's 3 more map tiles for a 16:9 picture). port_video_vram_extend makes gpu.c's VRAM 1024x1024 in Psy-Q's 2 MB layout: getTPage's y bit 9 is the texture page's bit 11, getClut's the CLUT's bit 15, both ignored as before without it; transfers, fills and copies wrap at the VRAM's height. Only textures and CLUTs live in rows 512-1023: the drawing area and the display stay below. The hardware renderer's VRAM copy has 1024 rows (its target keeps 512; a transfer or copy there is drawn only where it reaches the target), the texture keys track the extra rows, and a save state holds them as a block of its own. port_video_widescreen_pad tells a game how far past the display's edges the wide canvas shows, so that its 2D can draw that far. Without the call everything is as before. Co-Authored-By: Claude Opus 5.5 --- docs/DECISIONS.md | 11 +++++ docs/GAME_CONTRACT.md | 3 +- docs/PORT.md | 7 ++- docs/RUNTIME.md | 4 ++ include/psxstack/port_harness.h | 12 +++++- psyq/README.md | 2 +- psyq/gpu.c | 76 ++++++++++++++++++++++----------- psyq/libgpu.c | 2 +- psyq/psyq_internal.h | 5 ++- runtime/render_gpu.c | 45 ++++++++++++------- runtime/render_gpu.h | 5 ++- runtime/render_gpu_textures.c | 36 ++++++++-------- runtime/render_gpu_wide.c | 7 ++- runtime/video.c | 13 ++++++ shaders/raster.frag.hlsl | 3 +- tests/psyq/psyq_test.c | 64 +++++++++++++++++++++++++++ 16 files changed, 225 insertions(+), 70 deletions(-) diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 1840dbd..d30a7eb 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -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 diff --git a/docs/GAME_CONTRACT.md b/docs/GAME_CONTRACT.md index f992683..5ad76c8 100644 --- a/docs/GAME_CONTRACT.md +++ b/docs/GAME_CONTRACT.md @@ -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//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 diff --git a/docs/PORT.md b/docs/PORT.md index 9cd2285..36d76ae 100644 --- a/docs/PORT.md +++ b/docs/PORT.md @@ -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- @@ -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 diff --git a/docs/RUNTIME.md b/docs/RUNTIME.md index e40f521..fb09b3e 100644 --- a/docs/RUNTIME.md +++ b/docs/RUNTIME.md @@ -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. `_PORT_PRESENT_READBACK=FRAME:PATH` reads SDL_Renderer's output back (a game's renderer test compares the two present paths). `_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). diff --git a/include/psxstack/port_harness.h b/include/psxstack/port_harness.h index 9ba41db..86b9268 100644 --- a/include/psxstack/port_harness.h +++ b/include/psxstack/port_harness.h @@ -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) */ diff --git a/psyq/README.md b/psyq/README.md index 9c34068..1383021 100644 --- a/psyq/README.md +++ b/psyq/README.md @@ -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) | diff --git a/psyq/gpu.c b/psyq/gpu.c index 437cece..ef636a6 100644 --- a/psyq/gpu.c +++ b/psyq/gpu.c @@ -1,4 +1,5 @@ /* psyq/gpu.c: the GPU in software: a 1024x512 VRAM of 16-bit pixels and the GP0 commands that draw into it. + * A game may extend it to 1024x1024 (gpu_vram_extend: Psy-Q's 2 MB VRAM layout, below). * Written from public hardware documentation (psx-spx "GPU"), then measured against the emulator: the layer-1 family * `gpu` (tests/golden/families/gpu.py) runs command lists on the PS1 in PCSX-Redux (its software GPU) and reads the * VRAM back, tests/host/gpu_replay.py replays them here. Where the two disagree the emulator's pixels win (the M2 test is @@ -44,10 +45,20 @@ #endif #define VRAM_W 1024 -#define VRAM_H 512 +#define VRAM_H 1024 /* the rows kept; the VRAM has gpu_rows of them */ static u16 gpu_vram[VRAM_W * VRAM_H]; +/* The extension (gpu_vram_extend; psyq_internal.h): the VRAM's rows, 512, or 1024 for a game that asked for more + * texture room. Rows 512-1023 are Psy-Q's 2 MB VRAM: getTPage puts y bit 9 into the texture page's bit 11 and getClut + * into the CLUT's bit 15, which a 1 MB GPU ignores; transfers, fills and copies wrap at the VRAM's height. Only textures + * and CLUTs live there: the drawing area stays in rows 0-511 (E3/E4 have 9 bits of y), and so does the display. A + * texture page's rows stay within the VRAM either way (its corner is at most 768 extended, 256 otherwise, and a + * windowed v at most 255), so the texel fetches mask with VRAM_H - 1 in both. */ +static int gpu_rows = 512; +static u32 gpu_page_bits = 0x1FF; /* the texture page's bits kept: 0-8, and 11 once extended */ +#define VRAM_YMASK (gpu_rows - 1) + /* Write stamps: every write to the VRAM stamps the 64-pixel blocks it touches with the current stamp (gpu_touch), which * advances whenever a texture segment is decoded (gpu_decoded): a decoded segment stays valid while none of the blocks * it was made from (its texture words, its CLUT) carries a stamp newer than its own. */ @@ -130,6 +141,15 @@ const u16 *gpu_vram_pixels(void) { return gpu_vram; } +void gpu_vram_extend(void) { + gpu_rows = 1024; + gpu_page_bits = 0x9FF; +} + +int gpu_vram_rows(void) { + return gpu_rows; +} + void gpu_reset_state(void) { g.texpage = 0; g.dither = 0; @@ -207,7 +227,7 @@ GPU_INLINE u16 gpu_fetch(const u16 *row, const u16 *clut, int tex_x, int clut_x, } static inline u16 gpu_texel(const Mode *m, int u, int v) { - const u16 *row = &gpu_vram[((m->tex_y + ((v & m->v_and) | m->v_or)) & 511) * VRAM_W]; + const u16 *row = &gpu_vram[((m->tex_y + ((v & m->v_and) | m->v_or)) & (VRAM_H - 1)) * VRAM_W]; const u16 *clut = &gpu_vram[m->clut_y * VRAM_W]; u = (u & m->u_and) | m->u_or; @@ -494,14 +514,15 @@ static void gpu_event_mode(GpuEvent *ev, GpuEventKind kind, const Mode *m) { ev->check_mask = g.check_mask; } -/* The texture page attribute of a textured polygon (also E1's bits 0-8): sets the mode's page, depth and blend. */ +/* The texture page attribute of a textured polygon (also E1's bits 0-8, and 11 when extended): sets the mode's page, + * depth and blend. */ static void gpu_apply_texpage(u32 page) { - g.texpage = (g.texpage & ~0x1FFu) | (page & 0x1FF); + g.texpage = (g.texpage & ~gpu_page_bits) | (page & gpu_page_bits); } static void gpu_mode_from_texpage(Mode *m) { m->tex_x = (g.texpage & 0xF) * 64; - m->tex_y = ((g.texpage >> 4) & 1) * 256; + m->tex_y = ((g.texpage >> 4) & 1) * 256 + ((g.texpage >> 11) & 1) * 512; m->abr = (g.texpage >> 5) & 3; if (m->textured) { int depth = (g.texpage >> 7) & 3; @@ -585,7 +606,7 @@ GPU_INLINE void gpu_span_flat_tri(const Mode *m, u16 *dst, int n, Interp iu, Int if (depth) { int u = ((au + iu.q) & u_and) | u_or, v = ((av + iv.q) & v_and) | v_or; - t = gpu_fetch(&gpu_vram[((tex_y + v) & 511) * VRAM_W], clut, tex_x, clut_x, u, depth); + t = gpu_fetch(&gpu_vram[((tex_y + v) & (VRAM_H - 1)) * VRAM_W], clut, tex_x, clut_x, u, depth); gpu_interp_step(&iu, d); gpu_interp_step(&iv, d); if (t == 0) { @@ -625,7 +646,7 @@ GPU_INLINE void gpu_span_gouraud_tri(const Mode *m, u16 *dst, int x, int y, int if (depth) { int u = ((au + iu.q) & u_and) | u_or, v = ((av + iv.q) & v_and) | v_or; - t = gpu_fetch(&gpu_vram[((tex_y + v) & 511) * VRAM_W], clut, tex_x, clut_x, u, depth); + t = gpu_fetch(&gpu_vram[((tex_y + v) & (VRAM_H - 1)) * VRAM_W], clut, tex_x, clut_x, u, depth); gpu_interp_step(&iu, d); gpu_interp_step(&iv, d); if (t == 0) { @@ -1186,7 +1207,7 @@ static void gpu_polygon(const u32 *w) { vx[i].v = (w[k] >> 8) & 0xFF; if (i == 0) { m.clut_x = ((w[k] >> 16) & 0x3F) * 16; - m.clut_y = (w[k] >> 22) & 0x1FF; + m.clut_y = (w[k] >> 22) & VRAM_YMASK; } else if (i == 1) { gpu_apply_texpage(w[k] >> 16); } @@ -1228,7 +1249,7 @@ static void gpu_rectangle(const u32 *w) { u0 = w[2] & 0xFF; v0 = (w[2] >> 8) & 0xFF; m.clut_x = ((w[2] >> 16) & 0x3F) * 16; - m.clut_y = (w[2] >> 22) & 0x1FF; + m.clut_y = (w[2] >> 22) & VRAM_YMASK; if (m.raw) { r = gg = b = 128; /* the same pixels as raw (the span loops' comment) */ } @@ -1285,7 +1306,7 @@ static void gpu_rectangle(const u32 *w) { } kind = gpu_flat_kind(&m, r, gg, b); for (y = ys; y < ye; y++) { - int tv = (m.tex_y + (((v0 + (y - y0)) & m.v_and) | m.v_or)) & 511; + int tv = (m.tex_y + (((v0 + (y - y0)) & m.v_and) | m.v_or)) & (VRAM_H - 1); const u16 *row = &gpu_vram[tv * VRAM_W]; u16 *dst = &gpu_vram[y * VRAM_W + xs]; int u = u0 + (xs - x0); @@ -1407,9 +1428,9 @@ static void gpu_line_mode(Mode *m, u32 code) { static void gpu_fill(const u32 *w) { u16 c = (u16)(((w[0] >> 3) & 0x1F) | (((w[0] >> 11) & 0x1F) << 5) | (((w[0] >> 19) & 0x1F) << 10)); int x0 = w[1] & 0x3F0; - int y0 = (w[1] >> 16) & 0x1FF; + int y0 = (w[1] >> 16) & VRAM_YMASK; int width = ((w[2] & 0x3FF) + 0xF) & ~0xF; - int height = (w[2] >> 16) & 0x1FF; + int height = (w[2] >> 16) & VRAM_YMASK; int x, y; if (gpu_listener != NULL) { @@ -1424,34 +1445,34 @@ static void gpu_fill(const u32 *w) { gpu_listener(&ev); } for (y = 0; y < height; y++) { - u16 *row = &gpu_vram[((y0 + y) & 511) * VRAM_W]; + u16 *row = &gpu_vram[((y0 + y) & VRAM_YMASK) * VRAM_W]; for (x = 0; x < width; x++) { row[(x0 + x) & 1023] = c; } - gpu_touch((y0 + y) & 511, 0, VRAM_W); + gpu_touch((y0 + y) & VRAM_YMASK, 0, VRAM_W); } } /* GP0 80h: VRAM to VRAM, pixel by pixel, wrapping at the edges, with the mask rules. */ static void gpu_copy(const u32 *w) { - int sx = w[1] & 0x3FF, sy = (w[1] >> 16) & 0x1FF; - int dx = w[2] & 0x3FF, dy = (w[2] >> 16) & 0x1FF; + int sx = w[1] & 0x3FF, sy = (w[1] >> 16) & VRAM_YMASK; + int dx = w[2] & 0x3FF, dy = (w[2] >> 16) & VRAM_YMASK; int width = (((w[3] & 0xFFFF) - 1) & 0x3FF) + 1; - int height = ((((w[3] >> 16) & 0xFFFF) - 1) & 0x1FF) + 1; + int height = ((((w[3] >> 16) & 0xFFFF) - 1) & VRAM_YMASK) + 1; int x, y; for (y = 0; y < height; y++) { for (x = 0; x < width; x++) { - u16 p = gpu_vram[((sy + y) & 511) * VRAM_W + ((sx + x) & 1023)]; - u16 *d = &gpu_vram[((dy + y) & 511) * VRAM_W + ((dx + x) & 1023)]; + u16 p = gpu_vram[((sy + y) & VRAM_YMASK) * VRAM_W + ((sx + x) & 1023)]; + u16 *d = &gpu_vram[((dy + y) & VRAM_YMASK) * VRAM_W + ((dx + x) & 1023)]; if (g.check_mask && (*d & 0x8000)) { continue; } *d = p | g.set_mask; } - gpu_touch((dy + y) & 511, 0, VRAM_W); + gpu_touch((dy + y) & VRAM_YMASK, 0, VRAM_W); } if (gpu_listener != NULL) { GpuEvent ev; @@ -1487,7 +1508,7 @@ static void gpu_load_done(void) { static void gpu_load_pixel(u16 p) { int x = (g.load_x + g.load_i % g.load_w) & 1023; - int y = (g.load_y + g.load_i / g.load_w) & 511; + int y = (g.load_y + g.load_i / g.load_w) & VRAM_YMASK; u16 *d = &gpu_vram[y * VRAM_W + x]; g.load_i++; @@ -1565,9 +1586,9 @@ static void gpu_execute(void) { return; case 5: g.load_x = w[1] & 0x3FF; - g.load_y = (w[1] >> 16) & 0x1FF; + g.load_y = (w[1] >> 16) & VRAM_YMASK; g.load_w = (((w[2] & 0xFFFF) - 1) & 0x3FF) + 1; - g.load_h = ((((w[2] >> 16) & 0xFFFF) - 1) & 0x1FF) + 1; + g.load_h = ((((w[2] >> 16) & 0xFFFF) - 1) & VRAM_YMASK) + 1; g.load_i = 0; g.load_left = g.load_w * g.load_h; return; @@ -1576,7 +1597,7 @@ static void gpu_execute(void) { case 7: switch (code) { case 0xE1: - g.texpage = w[0] & 0x1FF; + g.texpage = w[0] & gpu_page_bits; g.dither = (w[0] >> 9) & 1; g.dfe = (w[0] >> 10) & 1; return; @@ -1684,7 +1705,7 @@ void gpu_load_image(int x, int y, int w, int h, const u16 *pixels) { int row, x; for (row = 0, i = 0; row < g.load_h; row++) { - int y = (g.load_y + row) & 511; + int y = (g.load_y + row) & VRAM_YMASK; u16 *line = &gpu_vram[y * VRAM_W]; for (x = 0; x < g.load_w; x++) { @@ -1712,7 +1733,10 @@ void gpu_load_image(int x, int y, int w, int h, const u16 *pixels) { void gpu_state(PortState *s) { int y; - port_state_bytes(s, "gpu_vram", gpu_vram, sizeof(gpu_vram)); + port_state_bytes(s, "gpu_vram", gpu_vram, (size_t)VRAM_W * 512 * sizeof(u16)); + if (gpu_rows > 512) { + port_state_bytes(s, "gpu_vram_ext", &gpu_vram[VRAM_W * 512], (size_t)VRAM_W * 512 * sizeof(u16)); + } port_state_bytes(s, "gpu", &g, sizeof(g)); if (port_state_loading(s)) { for (y = 0; y < VRAM_H; y++) { diff --git a/psyq/libgpu.c b/psyq/libgpu.c index 1839b1f..d1d40ad 100644 --- a/psyq/libgpu.c +++ b/psyq/libgpu.c @@ -618,7 +618,7 @@ static int psyq_gpu_clear(RECT *rect, u8 r, u8 g, u8 b, int dfe) { u32 n = 0; gpu_draw_state(&e1, &e3, &e4, &e5); - e1 = (e1 & 0xFF0007FFu) | ((u32)dfe << 10); + e1 = (e1 & 0xFF000FFFu) | ((u32)dfe << 10); w[n++] = 0xE6000000u; w[n++] = e1; if ((rect->x & 0x3F) != 0 || (cw & 0x3F) != 0) { diff --git a/psyq/psyq_internal.h b/psyq/psyq_internal.h index f3aea2f..dafa36b 100644 --- a/psyq/psyq_internal.h +++ b/psyq/psyq_internal.h @@ -118,13 +118,16 @@ void psyq_gte_swc2_(void *p, int reg); /* gpu.c: the GPU. gpu_power_on zeroes the VRAM and resets the drawing state; gpu_reset_state resets the drawing state * only (GP1(00h)); gpu_gp0_write takes one GP0 word (a command, its parameters, a transfer's pixels), gpu_gp0_words a * run of them (a DMA packet); gpu_load_image is a whole CPU-to-VRAM transfer (LoadImage); gpu_vram_pixels is the - * VRAM, 1024 pixels per row; gpu_draw_state the current E1/E3/E4/E5 words. */ + * VRAM, 1024 pixels per row, gpu_vram_rows rows (512, or 1024 after gpu_vram_extend: gpu.c "The extension"); gpu_draw_state + * the current E1/E3/E4/E5 words. */ void gpu_power_on(void); void gpu_reset_state(void); void gpu_gp0_write(u32 word); void gpu_gp0_words(const u32 *w, u32 n); void gpu_load_image(int x, int y, int w, int h, const u16 *pixels); const u16 *gpu_vram_pixels(void); +void gpu_vram_extend(void); +int gpu_vram_rows(void); void gpu_draw_state(u32 *e1, u32 *e3, u32 *e4, u32 *e5); /* gpu.c's decoded commands for the hardware renderer (runtime/render_gpu.c; issue #31): an optional listener, called diff --git a/runtime/render_gpu.c b/runtime/render_gpu.c index 7d073bf..63a5855 100644 --- a/runtime/render_gpu.c +++ b/runtime/render_gpu.c @@ -16,7 +16,9 @@ * (no fixed-function blending: DECISIONS "The hardware renderer"). Two copies keep the stream's order: * - the software VRAM's copy (R16_UINT, the texels and CLUTs): before a unit samples a block that gpu.c wrote since * the last upload (gpu.c's write stamps), the block is copied now and uploaded at that point of the frame; so is a - * transfer's rectangle before the transfer is drawn; + * transfer's rectangle before the transfer is drawn. It has 1024 rows: an extended VRAM's rows 512-1023 (gpu.c + * "The extension") hold only textures and CLUTs, so they are in the copy and not in the target (a transfer, fill + * or copy there is drawn only where it reaches the target's rows); * - the background (a copy of the target): before a unit that reads the background (semi-transparent, mask-checked; * a VRAM copy), the 16x16 tiles of its rectangle that something drew into since their last copy are copied. A unit * is one triangle or one line segment (a quad's two triangles may overlap, a polyline's segments share a pixel). @@ -59,7 +61,8 @@ #define RENDER_IMAGE_W 640 /* video.c's VIDEO_MAX_W, VIDEO_MAX_H: the largest display image */ #define RENDER_IMAGE_H 576 #define VRAM_W 1024 -#define VRAM_H 512 +#define VRAM_H 512 /* the target's rows (the drawing area's) */ +#define MIRROR_H 1024 /* the VRAM copy's: gpu.c's VRAM, extended or not (gpu_vram_rows) */ #define TILE 16 /* the background's dirty tiles */ #define TILES_X ((VRAM_W + RENDER_WIDE_COLS) / TILE) /* the target's widest: the VRAM and the wide canvas strip */ #define TILES_Y (VRAM_H / TILE) @@ -138,7 +141,7 @@ static struct { SDL_GPUGraphicsPipeline *pipe_raster; SDL_GPUTexture *vram_target; /* RGBA8 1024 scale x 512 scale: the rasteriser's VRAM */ SDL_GPUTexture *background; /* its copy, what units read */ - SDL_GPUTexture *mirror; /* R16_UINT 1024x512: the software VRAM's copy */ + SDL_GPUTexture *mirror; /* R16_UINT 1024x1024: the software VRAM's copy */ SDL_GPUTransferBuffer *staging_buf; /* the frame's uploads */ Uint32 staging_buf_size; SDL_GPUTransferBuffer *vram_download; /* render_gpu_read_vram */ @@ -146,7 +149,7 @@ static struct { size_t nrecs, caprecs; u8 *staging; /* the frame's upload pixels */ size_t nstaging, capstaging; - u32 uploaded[VRAM_H][BLOCKS]; /* the stamp each block had at its last upload; STAMP_NONE: never */ + u32 uploaded[MIRROR_H][BLOCKS]; /* the stamp each block had at its last upload; STAMP_NONE: never */ u32 generation; /* gpu.c's stamp generation at the last check */ u8 dirty[TILES_Y][TILES_X]; /* drawn into since the tile's last background copy */ SDL_GPUFence *inflight[RENDER_INFLIGHT]; /* the last frames' fences: at most RENDER_INFLIGHT frames queued */ @@ -199,7 +202,7 @@ static void sync_vram(int x0, int y0, int w, int h) { const u32 *stamps = gpu_block_stamps(); const u16 *vram = gpu_vram_pixels(); unsigned need = 0; - int j; + int j, rows = gpu_vram_rows(); Record *run = NULL; u32 now; @@ -220,8 +223,8 @@ static void sync_vram(int x0, int y0, int w, int h) { need |= 1u << (((x0 + w - 1) & 1023) >> 6); } now = gpu_stamp_bump(); /* the blocks' stamps are at most `now`; later writes carry newer ones */ - for (j = 0; j < (h < VRAM_H ? h : VRAM_H); j++) { - int y = (y0 + j) & 511, b, lo = -1, hi = -1; + for (j = 0; j < (h < rows ? h : rows); j++) { + int y = (y0 + j) & (rows - 1), b, lo = -1, hi = -1; for (b = 0; b < BLOCKS; b++) { if ((need >> b & 1) && (r.uploaded[y][b] == STAMP_NONE || stamps[y * BLOCKS + b] > r.uploaded[y][b])) { lo = lo < 0 ? b : lo; @@ -594,28 +597,32 @@ static void raster_segment(const GpuEvent *ev) { } /* A rectangle at (x, y) wrapping at the VRAM's edges, as up to four rectangles inside it: each piece's corner and size - * in piece[k][0..3], the piece's offset from (x, y) in piece[k][4..5]. */ + * in piece[k][0..3], the piece's offset from (x, y) in piece[k][4..5]. An extended VRAM's rows past the target's are + * left out (they have no pixels in the target). */ static int split_wrap(int x, int y, int w, int h, int piece[4][6]) { - int n = 0, i, j; + int n = 0, i, j, rows = gpu_vram_rows(); int xs[2][3], ys[2][3], nx = 0, ny = 0; x &= 1023; - y &= 511; + y &= rows - 1; w = SDL_min(w, VRAM_W); - h = SDL_min(h, VRAM_H); + h = SDL_min(h, rows); xs[nx][0] = x, xs[nx][1] = SDL_min(w, VRAM_W - x), xs[nx++][2] = 0; if (x + w > VRAM_W) { xs[nx][0] = 0, xs[nx][1] = x + w - VRAM_W, xs[nx++][2] = VRAM_W - x; } - ys[ny][0] = y, ys[ny][1] = SDL_min(h, VRAM_H - y), ys[ny++][2] = 0; - if (y + h > VRAM_H) { - ys[ny][0] = 0, ys[ny][1] = y + h - VRAM_H, ys[ny++][2] = VRAM_H - y; + ys[ny][0] = y, ys[ny][1] = SDL_min(h, rows - y), ys[ny++][2] = 0; + if (y + h > rows) { + ys[ny][0] = 0, ys[ny][1] = y + h - rows, ys[ny++][2] = rows - y; } for (j = 0; j < ny; j++) { + if (ys[j][0] >= VRAM_H) { + continue; + } for (i = 0; i < nx; i++) { piece[n][0] = xs[i][0]; piece[n][1] = ys[j][0]; piece[n][2] = xs[i][1]; - piece[n][3] = ys[j][1]; + piece[n][3] = SDL_min(ys[j][1], VRAM_H - ys[j][0]); piece[n][4] = xs[i][2]; piece[n][5] = ys[j][2]; n++; @@ -685,6 +692,12 @@ static void raster_copy(const GpuEvent *ev) { int xs[4], ys[4], nx, ny, i, j; int ddx = ((ev->sx - ev->x + 512) & 1023) - 512, ddy = ((ev->sy - ev->y + 256) & 511) - 256; RasterUnit u; + /* A copy from or into an extended VRAM's rows past the target's: the destination's rows in the target from the + * VRAM (the source may have no pixels in the target). */ + if (gpu_vram_rows() > VRAM_H && (ev->sy + ev->h > VRAM_H || ev->y + ev->h > VRAM_H)) { + raster_load(ev->x, ev->y, ev->w, ev->h, 0); + return; + } /* gpu.c copies pixel by pixel, rows from the top, each from the left, reading the VRAM as it writes: a copy whose * destination overlaps its source further on re-reads pixels it wrote (it smears). The event comes after the copy: * such a copy's destination is then taken from the VRAM, as a transfer is. */ @@ -1024,7 +1037,7 @@ int render_gpu_raster_start(int scale, char *why, size_t why_size) { return 0; } r.pipe_raster = render_pipeline(r.raster_vert, r.raster_frag, SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM); - r.mirror = render_texture(SDL_GPU_TEXTUREFORMAT_R16_UINT, VRAM_W, VRAM_H, SDL_GPU_TEXTUREUSAGE_SAMPLER); + r.mirror = render_texture(SDL_GPU_TEXTUREFORMAT_R16_UINT, VRAM_W, MIRROR_H, SDL_GPU_TEXTUREUSAGE_SAMPLER); r.no_replacement = render_texture(SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM, 1, 1, SDL_GPU_TEXTUREUSAGE_SAMPLER); { SDL_GPUSamplerCreateInfo si; diff --git a/runtime/render_gpu.h b/runtime/render_gpu.h index 732fe52..2f8bf29 100644 --- a/runtime/render_gpu.h +++ b/runtime/render_gpu.h @@ -59,11 +59,14 @@ void render_gpu_set_filter(const PortFilter *f); * the wide canvas strip, 1.5 times as wide); on or off every vsync (the scenes that widen; off without the * rasteriser); the canvas's aspect a:b (from 4:3 to 16:9, the window's: video.c video_wide_follow; 16:9 until it is * set); the width a 15-bit display of w x h at vram_xy is presented with: its canvas's (a:b at the display's pixel - * aspect), or 0 when it stays 4:3. render_gpu_present and render_gpu_readback show the canvas for that width. */ + * aspect), or 0 when it stays 4:3. render_gpu_present and render_gpu_readback show the canvas for that width. + * render_gpu_wide_pad: the columns the canvas adds on each side of the display while it is on (0 when off, or when the + * display stays 4:3). */ void render_gpu_wide_enable(void); void render_gpu_wide_set(int on); void render_gpu_wide_set_aspect(int a, int b); int render_gpu_wide_width(const int vram_xy[2], int w, int h); +int render_gpu_wide_pad(void); /* Releases everything, the window's claim, then the device (before SDL_Quit: video.c port_video_quit). */ void render_gpu_close(void); #endif diff --git a/runtime/render_gpu_textures.c b/runtime/render_gpu_textures.c index 0005761..43beadb 100644 --- a/runtime/render_gpu_textures.c +++ b/runtime/render_gpu_textures.c @@ -40,7 +40,7 @@ #include "sha1.h" #define VRAM_W 1024 -#define VRAM_H 512 +#define VRAM_H 1024 /* the rows kept; gpu.c's VRAM has gpu_vram_rows() of them (512, or 1024 extended) */ #define TILE 16 #define RING 65536 /* the transfers remembered: a word of an older one has no key */ #define CLUT_CACHE 256 @@ -113,7 +113,7 @@ static void tex_hash_row(PortSha1 *c, const u16 *vram, int x, int y, int w) { u8 buf[2 * VRAM_W]; int i; for (i = 0; i < w; i++) { - u16 p = vram[(y & 511) * VRAM_W + ((x + i) & 1023)]; + u16 p = vram[(y & (gpu_vram_rows() - 1)) * VRAM_W + ((x + i) & 1023)]; buf[2 * i] = (u8)p; buf[2 * i + 1] = (u8)(p >> 8); } @@ -127,11 +127,11 @@ static int tex_wrap(int d, int size) { /* ---- The owner map ---- */ static void own_set(int x, int y, int w, int h, u32 seq) { - int i, j; + int i, j, rows = gpu_vram_rows(); w = SDL_min(w, VRAM_W); - h = SDL_min(h, VRAM_H); + h = SDL_min(h, rows); for (j = 0; j < h; j++) { - int yy = (y + j) & 511; + int yy = (y + j) & (rows - 1); for (i = 0; i < w; i++) { int xx = (x + i) & 1023; t.own[yy][xx] = seq; @@ -165,21 +165,21 @@ static void own_clear_clipped(int x0, int y0, int x1, int y1) { /* The same over a rectangle that wraps at the VRAM's edges. */ static void own_clear(int x, int y, int w, int h) { - int xs[2][2], ys[2][2], nx = 1, ny = 1, i, j; + int xs[2][2], ys[2][2], nx = 1, ny = 1, i, j, rows = gpu_vram_rows(); if (w <= 0 || h <= 0) { return; } x &= 1023; - y &= 511; + y &= rows - 1; w = SDL_min(w, VRAM_W); - h = SDL_min(h, VRAM_H); + h = SDL_min(h, rows); xs[0][0] = x, xs[0][1] = SDL_min(x + w, VRAM_W) - 1; if (x + w > VRAM_W) { xs[1][0] = 0, xs[1][1] = x + w - VRAM_W - 1, nx = 2; } - ys[0][0] = y, ys[0][1] = SDL_min(y + h, VRAM_H) - 1; - if (y + h > VRAM_H) { - ys[1][0] = 0, ys[1][1] = y + h - VRAM_H - 1, ny = 2; + ys[0][0] = y, ys[0][1] = SDL_min(y + h, rows) - 1; + if (y + h > rows) { + ys[1][0] = 0, ys[1][1] = y + h - rows - 1, ny = 2; } for (j = 0; j < ny; j++) { for (i = 0; i < nx; i++) { @@ -193,7 +193,7 @@ static void own_clear_drawn(const GpuEvent *ev, int x0, int y0, int x1, int y1) x0 = SDL_max(SDL_max(x0, ev->area_x0), 0); y0 = SDL_max(SDL_max(y0, ev->area_y0), 0); x1 = SDL_min(SDL_min(x1, ev->area_x1), VRAM_W - 1); - y1 = SDL_min(SDL_min(y1, ev->area_y1), VRAM_H - 1); + y1 = SDL_min(SDL_min(y1, ev->area_y1), 511); if (x0 <= x1 && y0 <= y1) { own_clear_clipped(x0, y0, x1, y1); } @@ -605,12 +605,12 @@ int render_gpu_tex_key(const GpuEvent *ev, int u_lo, int u_hi, int v_lo, int v_h shift = shifts[ev->textured]; x0 = u_lo >> shift; x1 = u_hi >> shift; - seq = t.own[(ev->tex_y + v_lo) & 511][(ev->tex_x + x0) & 1023]; + seq = t.own[(ev->tex_y + v_lo) & (VRAM_H - 1)][(ev->tex_x + x0) & 1023]; if (seq == 0 || t.loads[seq & (RING - 1)].seq != seq) { return 0; } for (j = v_lo; j <= v_hi; j++) { - const u32 *row = t.own[(ev->tex_y + j) & 511]; + const u32 *row = t.own[(ev->tex_y + j) & (VRAM_H - 1)]; for (i = x0; i <= x1; i++) { if (row[(ev->tex_x + i) & 1023] != seq) { return 0; @@ -623,7 +623,7 @@ int render_gpu_tex_key(const GpuEvent *ev, int u_lo, int u_hi, int v_lo, int v_h key->w = l->w << shift; key->h = l->h; key->u0 = tex_wrap(l->x - ev->tex_x, VRAM_W) * (1 << shift); - key->v0 = tex_wrap(l->y - ev->tex_y, VRAM_H); + key->v0 = tex_wrap(l->y - ev->tex_y, gpu_vram_rows()); key->vx = l->x; key->vy = l->y; key->clut = ev->textured < 3 ? clut_hash(ev->clut_x, ev->clut_y, ev->textured == 1 ? 16 : 256) : 0; @@ -700,7 +700,7 @@ void render_gpu_tex_event(const GpuEvent *ev) { break; case GPU_EV_LOAD: { const u16 *vram = gpu_vram_pixels(); - int w = SDL_min(ev->w, VRAM_W), h = SDL_min(ev->h, VRAM_H), j; + int rows = gpu_vram_rows(), w = SDL_min(ev->w, VRAM_W), h = SDL_min(ev->h, rows), j; PortSha1 sha; TexLoad *l; if (w <= 0 || h <= 0) { @@ -717,7 +717,7 @@ void render_gpu_tex_event(const GpuEvent *ev) { l->seq = t.seq; l->hash = tex_digest64(&sha); l->x = ev->x & 1023; - l->y = ev->y & 511; + l->y = ev->y & (rows - 1); l->w = w; l->h = h; own_set(l->x, l->y, w, h, t.seq); @@ -726,7 +726,7 @@ void render_gpu_tex_event(const GpuEvent *ev) { for (j = 0; j < h; j++) { int i; for (i = 0; i < w; i++) { - img->pixels[j * w + i] = vram[((l->y + j) & 511) * VRAM_W + ((l->x + i) & 1023)]; + img->pixels[j * w + i] = vram[((l->y + j) & (rows - 1)) * VRAM_W + ((l->x + i) & 1023)]; } } } diff --git a/runtime/render_gpu_wide.c b/runtime/render_gpu_wide.c index 2b60c6d..1f7361b 100644 --- a/runtime/render_gpu_wide.c +++ b/runtime/render_gpu_wide.c @@ -173,6 +173,11 @@ void render_gpu_wide_set(int on) { } } +int render_gpu_wide_pad(void) { + PsyqDisplay d; + return wide.on && wide_display(&d) ? wide.pad : 0; +} + int render_gpu_wide_width(const int vram_xy[2], int w, int h) { PsyqDisplay d; WideSlot *s; @@ -317,7 +322,7 @@ void render_gpu_wide_event(const GpuEvent *ev) { break; case GPU_EV_COPY: case GPU_EV_LOAD: - wide_mirror(ev->x & 1023, ev->y & 511, ev->w, ev->h); + wide_mirror(ev->x & 1023, ev->y & (gpu_vram_rows() - 1), ev->w, ev->h); break; case GPU_EV_POWER_ON: memset(wide.slot, 0, sizeof(wide.slot)); diff --git a/runtime/video.c b/runtime/video.c index 9b9cdc2..b3a2cb3 100644 --- a/runtime/video.c +++ b/runtime/video.c @@ -46,6 +46,7 @@ #include "port_harness.h" #include "port_runtime.h" #include "psyq.h" +#include "psyq_internal.h" #ifdef PSXSTACK_SDL #include @@ -754,6 +755,18 @@ void port_video_widescreen(int on) { } } +int port_video_widescreen_pad(void) { +#ifdef PSXSTACK_SDL + return render_gpu_wide_pad(); +#else + return 0; +#endif +} + +void port_video_vram_extend(void) { + gpu_vram_extend(); +} + /* The present cap (fast-forward): at most `hz` presents a second (0: every vsync). Every vsync is still drawn into * the VRAM by the software GPU; only the conversion and the present are skipped. */ static int video_cap_hz; diff --git a/shaders/raster.frag.hlsl b/shaders/raster.frag.hlsl index 5a7eace..755c1e1 100644 --- a/shaders/raster.frag.hlsl +++ b/shaders/raster.frag.hlsl @@ -100,8 +100,9 @@ int span_start(int y) { return max(xl, nva.z); } +// The VRAM copy has 1024 rows (an extended VRAM's: render_gpu.c); a page's rows never wrap past it. uint texel(int u, int v) { - int row = (tex.y + v) & 511; + int row = (tex.y + v) & 1023; if (kind.w == 1) { uint px = vram.Load(int3((tex.x + (u >> 2)) & 1023, row, 0)); return vram.Load(int3((tex.z + int((px >> ((u & 3) * 4)) & 0xF)) & 1023, tex.w, 0)); diff --git a/tests/psyq/psyq_test.c b/tests/psyq/psyq_test.c index f869e67..2badf59 100644 --- a/tests/psyq/psyq_test.c +++ b/tests/psyq/psyq_test.c @@ -1252,6 +1252,69 @@ static void test_libc2(void) { psyq_reset(); } +/* A 1x1 8-bit sprite at (x, 10) from the texture page and CLUT that getTPage and getClut make of (640, ty), (0, cy). */ +static void vram_sprite(int x, int ty, int cy) { + u32 *ot = &arena[1000]; + SPRT *s = (SPRT *)&arena[2000]; + DR_TPAGE *tp = (DR_TPAGE *)&arena[2010]; + + ClearOTag(ot, 1); + setSprt(s); + SetShadeTex(s, 1); + s->x0 = (s16)x; + s->y0 = 10; + s->u0 = 0; + s->v0 = (u8)ty; + s->w = 1; + s->h = 1; + s->clut = getClut(0, cy); + setDrawTPage(tp, 0, 0, getTPage(1, 0, 640, ty)); + AddPrim(ot, s); + AddPrim(ot, tp); /* added last: drawn first */ + DrawOTag(ot); +} + +/* The extension (gpu.c "The extension"): before it, getTPage's and getClut's y bit 9 are ignored (the 1 MB VRAM: rows + * 512-1023 are rows 0-511); after it the texture and the CLUT come from rows 512 and 1012, and a transfer there + * leaves rows 0-511 alone. Last: it stays for the run. */ +static void test_vram_extend(void) { + static u16 clut[256]; + const u16 *vram = psyq_gpu_vram(); + u16 texel = 0x0101; + RECT tr = { 640, 0, 1, 1 }, cr = { 0, 500, 256, 1 }; + DRAWENV env; + DR_ENV *de = (DR_ENV *)&arena[3000]; + + SetDefDrawEnv(&env, 0, 0, 320, 240); + SetDrawEnv(de, &env); + termPrim(de); + DrawOTag((u32 *)de); + clut[1] = 0x001F; + LoadImage(&tr, (u32 *)&texel); + LoadImage(&cr, (u32 *)clut); + CHECK(gpu_vram_rows() == 512, "the VRAM's rows before the extension: %d", gpu_vram_rows()); + vram_sprite(10, 512, 1012); + CHECK(vram[10 * 1024 + 10] == 0x001F, "1 MB: page y 512 and CLUT row 1012 read rows 0 and 500: %04x", + vram[10 * 1024 + 10]); + gpu_vram_extend(); + CHECK(gpu_vram_rows() == 1024, "the VRAM's rows after the extension: %d", gpu_vram_rows()); + texel = 0x0202; + tr.y = 512; + cr.y = 1012; + clut[1] = 0x03E0; + clut[2] = 0x7C00; + LoadImage(&tr, (u32 *)&texel); + LoadImage(&cr, (u32 *)clut); + CHECK(vram[640] == 0x0101 && vram[512 * 1024 + 640] == 0x0202 && vram[500 * 1024 + 1] == 0x001F, + "a transfer to rows 512 and 1012 kept rows 0 and 500: %04x %04x %04x", vram[640], vram[512 * 1024 + 640], + vram[500 * 1024 + 1]); + vram_sprite(11, 512, 1012); + CHECK(vram[10 * 1024 + 11] == 0x7C00, "2 MB: the texel from row 512, its colour from CLUT row 1012: %04x", + vram[10 * 1024 + 11]); + vram_sprite(12, 0, 500); + CHECK(vram[10 * 1024 + 12] == 0x001F, "2 MB: rows 0 and 500 as before: %04x", vram[10 * 1024 + 12]); +} + int main(void) { psyq_reset(); test_scalar(); @@ -1267,6 +1330,7 @@ int main(void) { test_gs_setup(); test_gs_sort(); test_libc2(); + test_vram_extend(); printf("psyq_test: %d checks, %d failed\n", checks, failures); return failures ? 1 : 0; }