Add engine-debug-tools and fix critter animation rendering bugs
build / build (push) Successful in 2m52s
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:
@@ -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);
|
||||
Reference in New Issue
Block a user