Add engine-debug-tools and fix critter animation rendering bugs
build / build (push) Successful in 2m52s

- engine-debug-tools/: a debug dump hotkey + dump2png to inspect
  decoded critter frames independent of the renderer (see root
  README.md's "Debugging" section).
- engine-patches/04,05: fix critter sprite scale and an OpenGL
  texture cache collision, found using that tooling.
- mdlp: fix encodeFrames RLE-diffing upscaled frames against a
  previous-frame reference the real RSD8 decoder never implements
  (verified against Night Dive's original Mac source down to the
  68K assembly) - was rendering as scattered static in-game.
- Upscale pipeline: better color quantization (dithering, pure-ramp
  exclusion) and per-resource model selection.
This commit is contained in:
ml
2026-10-18 06:59:47 +02:00
parent ab2b778f44
commit 9d0a0d5798
21 changed files with 1185 additions and 82 deletions
@@ -1,11 +1,12 @@
--- a/src/GameSrc/objsim.c 2026-09-27 19:44:17.014441306 +0200
+++ b/src/GameSrc/objsim.c 2026-09-27 20:44:23.147083066 +0200
@@ -1867,6 +1867,40 @@
--- a/src/GameSrc/objsim.c
+++ b/src/GameSrc/objsim.c
@@ -1867,6 +1867,63 @@
return (OK);
}
+// Every critter view's base-game bitmap size, keyed by resource ID - filled
+// in by cache_critter_native_size() below while objart2.res/objart3.res's
+// Every critter view's base-game bitmap size, keyed by resource ID and
+// frame index within that animation - filled in by
+// cache_critter_native_size() below while objart2.res/objart3.res's
+// base-game data is still the active resolver for these IDs (before
+// LoadModFiles() hands priority to a mod again), read back by
+// get_critter_native_size() in gameobj.c. A mod that replaces a critter's
@@ -13,35 +14,57 @@
+// not the object's on-screen size: g3_bitmap_common() (Bitmap.c) uses this
+// cached size instead of the (possibly modded, possibly larger or smaller)
+// bitmap's own width/height when computing the sprite's world-space extent.
+// Keyed per frame, not just per view: an animation's bounding box commonly
+// changes size between poses, same as the base game's own art does, so a
+// single per-view size doesn't fit every frame of it - using one here left
+// every frame but the first using the wrong override, visibly corrupting
+// the sprite's UV mapping (g3_bitmap_common() uses this same size for
+// texture coordinates, not just world-space extent) once a mod's per-frame
+// sizes diverge from each other. MAX_CRITTER_FRAMES comfortably covers any
+// animation in the base game's own data; cache_critter_native_size() clamps
+// to it defensively rather than overrunning the array if that ever changes.
+// Id is uint16_t, so a flat array indexed by it needs no bounds check.
+static short s_critterNativeW[65536];
+static short s_critterNativeH[65536];
+#define MAX_CRITTER_FRAMES 32
+static short s_critterNativeW[65536][MAX_CRITTER_FRAMES];
+static short s_critterNativeH[65536][MAX_CRITTER_FRAMES];
+
+static void cache_critter_native_size(Id id) {
+ RefTable *prt;
+ FrameDesc *f;
+ if (s_critterNativeW[id] != 0)
+ int numFrames, frame;
+ if (s_critterNativeW[id][0] != 0)
+ return;
+ f = (FrameDesc *)RefLock(MKREF(id, 0));
+ if (f == NULL)
+ prt = (RefTable *)ResLock(id);
+ if (prt == NULL)
+ return;
+ s_critterNativeW[id] = f->bm.w;
+ s_critterNativeH[id] = f->bm.h;
+ RefUnlock(MKREF(id, 0));
+ numFrames = prt->numRefs;
+ ResUnlock(id);
+ if (numFrames > MAX_CRITTER_FRAMES)
+ numFrames = MAX_CRITTER_FRAMES;
+ for (frame = 0; frame < numFrames; frame++) {
+ f = (FrameDesc *)RefLock(MKREF(id, frame));
+ if (f == NULL)
+ continue;
+ s_critterNativeW[id][frame] = f->bm.w;
+ s_critterNativeH[id][frame] = f->bm.h;
+ RefUnlock(MKREF(id, frame));
+ }
+}
+
+// w/h are left untouched (so a caller can default to 0, meaning "no cached
+// native size, use the bitmap's own dimensions") if id was never cached.
+void get_critter_native_size(Id id, short *w, short *h) {
+ if (s_critterNativeW[id] == 0)
+// native size, use the bitmap's own dimensions") if id/frame was never
+// cached.
+void get_critter_native_size(Id id, short frame, short *w, short *h) {
+ if ((frame < 0) || (frame >= MAX_CRITTER_FRAMES) || (s_critterNativeW[id][frame] == 0))
+ return;
+ *w = s_critterNativeW[id];
+ *h = s_critterNativeH[id];
+ *w = s_critterNativeW[id][frame];
+ *h = s_critterNativeH[id][frame];
+}
+
errtype obj_set_secondary_properties() {
char i, j;
RefTable *prt;
@@ -1878,6 +1912,28 @@
@@ -1878,6 +1935,28 @@
// Warning(("Problem opening object art cache!\n"));
return (ERR_NOEFFECT);
}
@@ -70,18 +93,18 @@
// The two opens above re-register every resource ID objart2.res/
// objart3.res contain, reclaiming any of them a mod file already
// overrode (see LoadModFiles() in Modding.c) - restore mod priority
--- a/src/GameSrc/Headers/objsim.h 2026-07-19 13:11:51.000000000 +0200
+++ b/src/GameSrc/Headers/objsim.h 2026-09-27 19:39:39.585009491 +0200
--- a/src/GameSrc/Headers/objsim.h
+++ b/src/GameSrc/Headers/objsim.h
@@ -108,6 +108,7 @@
errtype obj_load_properties();
errtype obj_create_player(ObjLoc *plr_loc);
Ref ref_from_critter_data(ObjID oid, int triple, byte posture, short frame, short view);
+void get_critter_native_size(Id id, short *w, short *h);
+void get_critter_native_size(Id id, short frame, short *w, short *h);
errtype obj_zero_unused();
grs_bitmap *obj_get_model_data(ObjID id, fix *x, fix *y, fix *z, grs_bitmap *bm2, Ref *ref1, Ref *ref2);
errtype obj_model_hack(ObjID id, uchar *hack_x, uchar *hack_y, uchar *hack_z, uchar *hack_type);
--- a/src/GameSrc/gameobj.c 2026-07-19 13:11:51.000000000 +0200
+++ b/src/GameSrc/gameobj.c 2026-09-27 20:44:35.195019539 +0200
--- a/src/GameSrc/gameobj.c
+++ b/src/GameSrc/gameobj.c
@@ -104,7 +104,7 @@
void draw_ice(void);
void draw_ice_wall(void);
@@ -134,7 +157,7 @@
- _fr_draw_bitmap(tpdata, _fdt_dist, 0, anch.ul.x, anch.ul.y);
+ {
+ short native_w = 0, native_h = 0;
+ get_critter_native_size(REFID(ref), &native_w, &native_h);
+ get_critter_native_size(REFID(ref), REFINDEX(ref), &native_w, &native_h);
+ _fr_draw_bitmap(tpdata, _fdt_dist, 0, anch.ul.x, anch.ul.y, native_w, native_h);
+ }
release_critter_bitmap_fast(ref);
@@ -145,8 +168,8 @@
}
} break;
--- a/src/Libraries/3D/Source/Bitmap.c 2026-07-19 13:11:51.000000000 +0200
+++ b/src/Libraries/3D/Source/Bitmap.c 2026-09-27 20:44:47.378954871 +0200
--- a/src/Libraries/3D/Source/Bitmap.c
+++ b/src/Libraries/3D/Source/Bitmap.c
@@ -88,6 +88,14 @@
// fix _g3d_bitmap_y_iscale = 0x010000;
long _g3d_bitmap_u_anchor = 0;
@@ -208,8 +231,8 @@
tempL = p->gZ; // mov eax,[edi].z
tempL <<= 8; // sal eax,8 ;should be
--- a/src/Libraries/3D/Source/3d.h 2026-07-19 13:11:51.000000000 +0200
+++ b/src/Libraries/3D/Source/3d.h 2026-09-27 19:42:26.528424908 +0200
--- a/src/Libraries/3D/Source/3d.h
+++ b/src/Libraries/3D/Source/3d.h
@@ -720,7 +720,8 @@
grs_vertex **g3_light_bitmap(grs_bitmap *bm, g3s_phandle p);
// MLA #pragma aux g3_light_bitmap "*" parm [esi] [edi] modify [eax ebx ecx
@@ -0,0 +1,84 @@
--- a/src/MacSrc/OpenGL.cc
+++ b/src/MacSrc/OpenGL.cc
@@ -41,6 +41,7 @@
#include <map>
#include <sstream>
+#include <tuple>
struct CachedTexture {
SDL_Surface *bitmap;
@@ -83,8 +84,23 @@
static SDL_Palette *opaquePalette;
static SDL_Palette *transparentPalette;
-// Texture cache to keep SDL surfaces and GL textures in memory
-static std::map<uint64_t, CachedTexture> texturesByBitsPtr;
+// Texture cache to keep SDL surfaces and GL textures in memory. Keyed by the
+// full (bits, w, h) tuple, not a packed uint64_t - bm->bits is a full
+// pointer, and a 64-bit Linux heap/mmap address routinely has non-zero bits
+// in the 32-47 range, so OR-packing w/h on top of it (as this used to do)
+// can collide: a genuinely different (bits, w, h) triple produces the same
+// packed key as a cached entry for a different bitmap, so a lookup reports a
+// false "hit" and set_texture() reuses that entry's SDL surfaces - sized for
+// the old entry's own dimensions - for an SDL_memmove and glTexImage2D using
+// the *current* bitmap's byte count, corrupting the result with a buffer
+// overflow or leftover stale pixel data from whatever was cached before.
+// bm->bits itself is a recycled slot in the resource system's own bounded
+// LRU cache, not a stable per-resource address, so a collision is a real
+// risk in practice, not just theoretical - most noticeable with content
+// whose per-frame bitmap dimensions vary a lot (critter animations with
+// different poses per frame, especially after an AI upscale mod re-exports
+// each frame at its own independently-upscaled size).
+static std::map<std::tuple<void *, int, int>, CachedTexture> texturesByBitsPtr;
static float view_scale;
static int phys_width;
@@ -577,7 +593,7 @@
bind_texture(toCache.texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, bm->w, bm->h, 0, GL_RGBA, GL_UNSIGNED_BYTE, toCache.converted->pixels);
- texturesByBitsPtr[(uint64_t)bm->bits | ((uint64_t)bm->w << 32u) | ((uint64_t)bm->h << 48u)] = toCache;
+ texturesByBitsPtr[std::make_tuple(bm->bits, (int)bm->w, (int)bm->h)] = toCache;
return true;
}
@@ -589,7 +605,7 @@
}
static CachedTexture *opengl_get_texture(grs_bitmap *bm) {
- auto iter = texturesByBitsPtr.find((uint64_t)bm->bits | ((uint64_t)bm->w << 32u) | ((uint64_t)bm->h << 48u));
+ auto iter = texturesByBitsPtr.find(std::make_tuple(bm->bits, (int)bm->w, (int)bm->h));
if (iter != texturesByBitsPtr.end()) {
return &iter->second;
}
@@ -685,13 +701,27 @@
isDirty = true;
}
} else {
+ // t->bitmap wraps its source pixels rather than owning a copy (see
+ // convert_texture()), and a cache hit means bm->bits is by
+ // definition the same pointer this entry was first cached with
+ // (opengl_get_texture()'s key includes it) - so memmove-ing
+ // bm->bits into t->bitmap->pixels used to be a no-op self-copy,
+ // never actually refreshing anything. bm->bits is a slot in the
+ // resource system's own bounded LRU cache, reused for different
+ // content across draws while its address stays the same, so a hit
+ // here doesn't mean the pixels are still current - only that this
+ // entry's GL texture object and converted-surface buffer are the
+ // right size to reuse. Re-pointing at the current source instead
+ // keeps every other field (dimensions, palette, clip rect) intact
+ // while making the surface actually see this draw's real pixels.
if (bm->type == BMT_RSD8) {
grs_bitmap decoded;
gr_rsd8_convert(bm, &decoded);
- SDL_memmove(t->bitmap->pixels, decoded.bits, bm->w * bm->h);
+ t->bitmap->pixels = decoded.bits;
} else {
- SDL_memmove(t->bitmap->pixels, bm->bits, bm->w * bm->h);
+ t->bitmap->pixels = bm->bits;
}
+ t->bitmap->pitch = bm->row;
setPaletteForBitmap(bm, t->bitmap);
SDL_BlitSurface(t->bitmap, nullptr, t->converted, nullptr);