Fix critter sprites cropped in the software renderer
build / build (push) Successful in 2m41s

g3_bitmap_common() (Bitmap.c) used the same bm_w/bm_h for both the
on-screen quad size and its texture-coordinate bounds. The former
should use patch 04's native-size override for a modded/upscaled
bitmap; the latter must always use the bitmap's own real dimensions,
since that's still what's actually being sampled. OpenGL never hit
this (opengl_bitmap() ignores these bounds and always maps the whole
real texture), but the software renderer's h_map() takes them as
literal pixel coordinates to interpolate, so the override capped how
much of a larger bitmap it ever read - cropping anything past the
original sprite's bounds, such as an upscaled critter's limbs.

Split into a separate tex_w/tex_h pair, always the bitmap's real
size, used for every texture-coordinate computation.

README.md: document this, and the previously-undocumented
engine-debug-tools RSD8 decode tracing patch (02).
This commit is contained in:
ml
2026-10-18 07:33:56 +02:00
parent 9d0a0d5798
commit a8d7ff76df
2 changed files with 115 additions and 12 deletions
+45 -9
View File
@@ -387,6 +387,29 @@ which the desktop build does not apply.
the sprite from where it should sit, since it shares its coordinate the sprite from where it should sit, since it shares its coordinate
system with this patch's own size override rather than with the system with this patch's own size override rather than with the
bitmap's real pixel dimensions. bitmap's real pixel dimensions.
`g3_bitmap_common()`'s world-space size and its texture-coordinate range
are computed from the same `bm_w`/`bm_h` locals, but only the former
should ever use this patch's override - the pixels actually being
sampled are still the bitmap's own real (possibly upscaled) buffer,
regardless of what on-screen size it's drawn at. Overriding both left the
texture-coordinate range capped at the smaller, un-upscaled size while
the real bitmap underneath it was larger, so the renderer only ever
sampled that smaller top-left region of it - cropping off anything past
it, such as an upscaled critter's limbs extending beyond its original
sprite's bounding box. OpenGL never showed this: `opengl_bitmap()`
ignores `g3_bitmap_common()`'s computed `u`/`v` entirely and always maps
the *whole* bound texture over the quad via fixed `0.0`-`1.0` corners.
The software renderer's own scanline texture mapper (`h_map()`,
`gentm.c`) has no such shortcut - it takes `u`/`v` as literal pixel
coordinates into the bitmap and interpolates between exactly the values
it's given, so a capped range there directly caps how much of the real
bitmap it ever reads. Fixed by splitting `g3_bitmap_common()`'s single
`bm_w`/`bm_h` into the override-aware pair (world-space size only) and a
`tex_w`/`tex_h` pair that always holds the bitmap's own real dimensions,
used for every texture-coordinate computation (including the later
blend-buffer-size check, which sizes a buffer against the same real
pixel data these coordinates sample from).
- **`05-desktop-fix-texture-cache-collision.patch`** - `OpenGL.cc`'s - **`05-desktop-fix-texture-cache-collision.patch`** - `OpenGL.cc`'s
per-bitmap GL texture cache (`texturesByBitsPtr`) was keyed by per-bitmap GL texture cache (`texturesByBitsPtr`) was keyed by
`bm->bits | (w << 32) | (h << 48)`, a plain bitwise OR rather than a `bm->bits | (w << 32) | (h << 48)`, a plain bitwise OR rather than a
@@ -443,15 +466,28 @@ included in `make dist`/`make package`.
numbered the same way as `engine-patches/` (section 4.4's own process numbered the same way as `engine-patches/` (section 4.4's own process
applies: scratch copy, `patch -p1`, regenerate via `diff -u` rather than applies: scratch copy, `patch -p1`, regenerate via `diff -u` rather than
hand-editing hunks), applied on top of the regular `engine-patches/` hand-editing hunks), applied on top of the regular `engine-patches/`
series, never in place of it. Currently one patch, adding a debug dump series, never in place of it. Two patches so far:
hotkey: pressing **K** in-game dumps every frame of the - **`01`** - a debug dump hotkey: pressing **K** in-game dumps every
currently/most-recently-drawn critter's animation to `dumps/` (created frame of the currently/most-recently-drawn critter's animation to
next to the running binary) as raw palette-index pixel data - 2-byte LE `dumps/` (created next to the running binary) as raw palette-index
width, 2-byte LE height, then width\*height index bytes, one file per pixel data - 2-byte LE width, 2-byte LE height, then width\*height
frame, named `id<resource-id>_frame<N>.raw`. Deliberately independent of index bytes, one file per frame, named `id<resource-id>_frame<N>.raw`.
the renderer's own texture/GL state, so a frame already corrupted in the Deliberately independent of the renderer's own texture/GL state, so a
dump is a decode-time bug, while a frame that's clean in the dump but frame already corrupted in the dump is a decode-time bug, while a
wrong on screen is a rendering-time one. frame that's clean in the dump but wrong on screen is a rendering-time
one.
- **`02`** - RSD8 decode tracing: `gr_rsd8_convert()`/`gr_rsd8_unpack()`
(`engine/src/Libraries/2D/Source/RSD/`) print a `[debug rsd8]` summary
per frame (decode path, source/destination dimensions, bytes written
versus the bitmap's own area) and a `[debug rsd8 op]` line per RLE
opcode consumed, gated by a flag `debug_dump_critter_animation()` (01,
above) sets only around its own decode calls - tracing covers exactly
the frames the K hotkey dumps, not every RSD8 decode in the game. Each
frame is also decoded a second time back to back and logged as
`identical`/`DIFFERS` against the first, to tell a decode result that's
deterministic given the compressed source (a bad source stream, or a
decoder bug it triggers) apart from one that depends on something else,
such as a resource-cache slot being reused mid-decode.
- `engine-debug-tools/dump2png/` - converts those `.raw` dumps into - `engine-debug-tools/dump2png/` - converts those `.raw` dumps into
viewable PNGs using the real game palette (the dump format has no viewable PNGs using the real game palette (the dump format has no
palette of its own, to keep the in-game dump path from depending on the palette of its own, to keep the in-game dump path from depending on the
@@ -200,7 +200,15 @@
_g3d_bitmap_clut = (p->i & 0x00ff00) + grd_screen->ltab; _g3d_bitmap_clut = (p->i & 0x00ff00) + grd_screen->ltab;
_g3d_bitmap_poly = vpl; _g3d_bitmap_poly = vpl;
return (g3_bitmap_common(bm, p)); return (g3_bitmap_common(bm, p));
@@ -185,6 +196,15 @@ @@ -176,6 +187,7 @@
grs_vertex *tempG1, *tempG2;
fix tempResult;
int32_t bm_w, bm_h;
+ int32_t tex_w, tex_h;
fix dx, dy;
// MLA- these were globals, I made them locals for PPC speed, they aren't
@@ -185,6 +197,15 @@
long rm2; long rm2;
long rm3; long rm3;
@@ -216,7 +224,7 @@
#ifdef stereo_on #ifdef stereo_on
if (_g3d_stereo & 1) { if (_g3d_stereo & 1) {
@@ -209,9 +229,11 @@ @@ -209,9 +230,23 @@
if ((p->codes & CC_CLIP_OVERFLOW) != 0) if ((p->codes & CC_CLIP_OVERFLOW) != 0)
return (0L); return (0L);
@@ -225,12 +233,71 @@
- bm_h = bm->h; - bm_h = bm->h;
+ // copy a few things into locals - w_override/h_override, if set (see + // copy a few things into locals - w_override/h_override, if set (see
+ // _g3d_bitmap_w_override's own comment), take the place of the bitmap's + // _g3d_bitmap_w_override's own comment), take the place of the bitmap's
+ // own pixel dimensions here. + // own pixel dimensions here, for the on-screen quad's world-space size
+ // only. tex_w/tex_h stay the bitmap's own real dimensions always: the
+ // quad's u/v bounds (below) are pixel coordinates into bm's own actual
+ // pixel data - the software renderer's scanline texture mapper
+ // (h_map(), gentm.c) interpolates and samples them literally, so
+ // overriding them to a smaller size here would make it only ever read
+ // part of a bitmap bigger than that override (such as an upscaled mod's
+ // replacement art), cropping the rest. OpenGL doesn't read u/v from here
+ // at all (opengl_bitmap() always maps the whole real texture over the
+ // quad), which is why this only ever showed up in the software
+ // renderer.
+ bm_w = w_override ? w_override : bm->w; + bm_w = w_override ? w_override : bm->w;
+ bm_h = h_override ? h_override : bm->h; + bm_h = h_override ? h_override : bm->h;
+ tex_w = bm->w;
+ tex_h = bm->h;
tempL = p->gZ; // mov eax,[edi].z tempL = p->gZ; // mov eax,[edi].z
tempL <<= 8; // sal eax,8 ;should be tempL <<= 8; // sal eax,8 ;should be
@@ -299,7 +334,7 @@
tempG1->y = tempL2;
// vpl[1][2] = bitmap.w-1
- tempG1->u = (bm_w - 1) << 16;
+ tempG1->u = (tex_w - 1) << 16;
// vpl[1][3] = 0
tempG1->v = 0;
@@ -326,10 +361,10 @@
tempG1->y = tempL2;
// vpl[2][2] = bitmap.w-1
- tempG1->u = (bm_w - 1) << 16;
+ tempG1->u = (tex_w - 1) << 16;
// vpl[2][3] = bitmap.h-1
- tempG1->v = (bm_h - 1) << 16;
+ tempG1->v = (tex_h - 1) << 16;
// vpl[2][4] undefined
@@ -356,7 +391,7 @@
tempG1->u = 0;
// vpl[3][3] = bitmap.h
- tempG1->v = bm_h << 16;
+ tempG1->v = tex_h << 16;
// vpl[3][4] undefined
@@ -380,11 +415,13 @@
if (dy > dx)
dx = dy; // get max value in dx
- // shift down because bm_w isn't fixed, and we only double when twice the size
+ // shift down because tex_w isn't fixed, and we only double when twice the size
dx >>= 0x11;
// make sure doubled bitmap won't overflow unpack buffer (that's roughly 1/5 of 64k)
- if ((bm_w <= dx) && (bm_w * bm_h <= 12000)) {
+ // - against the bitmap's own real size (tex_w/tex_h), same as the u/v this doubles
+ // just below: both operate on bm's actual pixel data, not the on-screen override.
+ if ((tex_w <= dx) && (tex_w * tex_h <= 12000)) {
// first fix all u's and v's
for (tempL = 0; tempL < 4; tempL++) {
_g3d_bitmap_poly[tempL]->u <<= 1;
--- a/src/Libraries/3D/Source/3d.h --- a/src/Libraries/3D/Source/3d.h
+++ b/src/Libraries/3D/Source/3d.h +++ b/src/Libraries/3D/Source/3d.h
@@ -720,7 +720,8 @@ @@ -720,7 +720,8 @@