From 9d0a0d57985e0e6f4cc7335465a1434ef1b6c305 Mon Sep 17 00:00:00 2001 From: ml Date: Sun, 18 Oct 2026 06:59:47 +0200 Subject: [PATCH] Add engine-debug-tools and fix critter animation rendering bugs - 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. --- .gitignore | 4 + Makefile | 18 +- README.md | 113 +++++++++++- build-image/build-engine.sh | 11 ++ engine-debug-tools/build-dump2png.sh | 65 +++++++ engine-debug-tools/dump2png/go.mod | 12 ++ engine-debug-tools/dump2png/go.sum | 10 ++ engine-debug-tools/dump2png/main.go | 105 +++++++++++ .../01-desktop-debug-dump-hotkey.patch | 153 ++++++++++++++++ .../02-desktop-debug-rsd8-decode-log.patch | 166 ++++++++++++++++++ .../04-desktop-fix-critter-sprite-scale.patch | 81 ++++++--- ...-desktop-fix-texture-cache-collision.patch | 84 +++++++++ modding/README.md | 102 +++++++++++ modding/mdlp-image/Dockerfile | 48 +++-- modding/mdlp-image/upscale_batch.py | 111 +++++++++++- modding/src/animgallery/main.go | 4 +- modding/src/mdlp/upscale.go | 150 ++++++++++++++-- res/convert-dumps.sh | 19 ++ res/run.sh | 5 +- res/shockmac-master.zip | Bin 0 -> 22895333 bytes run-image.sh | 6 +- 21 files changed, 1185 insertions(+), 82 deletions(-) create mode 100755 engine-debug-tools/build-dump2png.sh create mode 100644 engine-debug-tools/dump2png/go.mod create mode 100644 engine-debug-tools/dump2png/go.sum create mode 100644 engine-debug-tools/dump2png/main.go create mode 100644 engine-debug-tools/engine-patches/01-desktop-debug-dump-hotkey.patch create mode 100644 engine-debug-tools/engine-patches/02-desktop-debug-rsd8-decode-log.patch create mode 100644 engine-patches/05-desktop-fix-texture-cache-collision.patch create mode 100644 res/convert-dumps.sh create mode 100644 res/shockmac-master.zip diff --git a/.gitignore b/.gitignore index 79ece2a..6da17d6 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,7 @@ # modding/README.md) /modding/dist/ /modding/build/ + +# engine-debug-tools/dump2png's own scratch, patched source copy (see +# engine-debug-tools/build-dump2png.sh) +/engine-debug-tools/build/ diff --git a/Makefile b/Makefile index 1e1ec7f..863cc04 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ ASSETS_DIR := res/assets/ss_ee BUILD_DIR := build ARCH := $(shell uname -m) -.PHONY: all dist dist-linux dist-win build-image engine engine-win assets \ +.PHONY: all dist dist-linux dist-win dist_debug build-image engine engine-win assets \ package package-linux package-win apk android-studio clean all: dist @@ -87,6 +87,22 @@ dist-linux: engine assets chmod +x "$(DIST_DIR)/run-mods.sh" @echo "== Done - run $(DIST_DIR)/run.sh to play ==" +# Same as dist-linux, but also applies engine-debug-tools/engine-patches/ +# (debug-only instrumentation - currently a dump hotkey, see +# engine-debug-tools/ and README.md's "Debugging" section), builds +# engine-debug-tools/dump2png into $(DIST_DIR) alongside it, and copies in +# convert-dumps.sh (res/convert-dumps.sh - a dump2png wrapper pointed at +# $(DIST_DIR)'s own res/data and dumps/, same convention as run-mods.sh +# wrapping run.sh). Overwrites whatever dist-linux last produced in +# $(DIST_DIR) - there's one dist/, and which of these two targets built it +# last is the only difference. +dist_debug: export ENGINE_DEBUG_TOOLS := 1 +dist_debug: dist-linux + ./engine-debug-tools/build-dump2png.sh "$(DIST_DIR)/dump2png" + cp res/convert-dumps.sh "$(DIST_DIR)/convert-dumps.sh" + chmod +x "$(DIST_DIR)/convert-dumps.sh" + @echo "== dist_debug: debug dump hotkey + $(DIST_DIR)/dump2png + $(DIST_DIR)/convert-dumps.sh are ready ==" + # Windows counterpart of dist-linux. Unlike dist/, the DLLs sit flat # alongside systemshock.exe instead of in a lib/ subdirectory - Windows' # default DLL search order already checks the executable's own directory diff --git a/README.md b/README.md index 4126586..d6b1312 100644 --- a/README.md +++ b/README.md @@ -358,17 +358,25 @@ which the desktop build does not apply. mod that replaces a critter's art with a higher-resolution bitmap for more detail made the critter itself render bigger, proportionally to the resolution increase. Fixed by caching every critter view's base-game - bitmap size (`objsim.c`'s `cache_critter_native_size()`/ - `get_critter_native_size()`, keyed by resource ID) while - `objart2.res`/`objart3.res`'s base-game data is still the active - resolver in `obj_set_secondary_properties()` (before patch 03's - `LoadModFiles()` call hands priority to a mod), then using that cached - size instead of the bitmap's own (possibly modded) dimensions when - computing the sprite's on-screen extent - threaded through + bitmap size per animation frame (`objsim.c`'s + `cache_critter_native_size()`/`get_critter_native_size()`, keyed by + resource ID and frame index - an animation's bounding box commonly + changes size between poses, so a single per-view size doesn't fit every + frame of it) while `objart2.res`/`objart3.res`'s base-game data is still + the active resolver in `obj_set_secondary_properties()` (before patch + 03's `LoadModFiles()` call hands priority to a mod), then using that + cached size instead of the bitmap's own (possibly modded) dimensions + when computing the sprite's on-screen extent - threaded through `_fr_draw_bitmap()` and `g3_light_anchor_bitmap()`'s new `native_w`/ `native_h` parameters, consumed and reset by `g3_bitmap_common()` so an unrelated bitmap draw never inherits a stale override. A mod's higher - resolution now only changes visible detail, not the object's size. + resolution now only changes visible detail, not the object's size. Since + this same size also sets `g3_bitmap_common()`'s texture coordinate + range, not just its world-space extent, caching only the first frame's + size and reusing it for every frame of an animation also visibly + corrupted the UV mapping on any frame whose real size differed from the + first - caught once a mod's per-frame sizes started genuinely varying + (see `modding/README.md` section 5). This depends on the mod's bitmap keeping a real anchor point in its own `Header.Area`, in the original, un-upscaled unit space (see `modding/src/mdlp/upscale.go`'s `imageToBitmap()`) - `_fr_draw_bitmap()` @@ -379,6 +387,95 @@ which the desktop build does not apply. the sprite from where it should sit, since it shares its coordinate system with this patch's own size override rather than with the bitmap's real pixel dimensions. +- **`05-desktop-fix-texture-cache-collision.patch`** - `OpenGL.cc`'s + per-bitmap GL texture cache (`texturesByBitsPtr`) was keyed by + `bm->bits | (w << 32) | (h << 48)`, a plain bitwise OR rather than a + genuinely disjoint encoding - `bm->bits` is a full pointer, and a 64-bit + Linux heap/mmap address routinely has non-zero bits in the 32-47 range + the width field occupies, so a different `(bits, w, h)` triple can + collide with an already-cached entry's key. `bm->bits` itself is a + recycled slot in the resource system's own bounded LRU cache, not a + stable per-resource address, making a collision a real risk rather than + a theoretical one. On a collision, `set_texture()` (same file) treats it + as a genuine cache hit and reuses that entry's SDL surfaces - allocated + for whatever `(w, h)` was cached before - for an `SDL_memmove` and + `glTexImage2D` sized to the *current* bitmap's byte count, corrupting + the result with a buffer overflow or leftover stale pixel data from a + previous, differently-sized bitmap. Fixed by keying the cache on a + `std::tuple` instead, which `std::map` compares + field-by-field with no collision risk - a hit now always means an exact + `(bits, w, h)` match, so the cached surfaces are guaranteed to already + be sized correctly for the current bitmap. + + A second, independent bug in the same cache: on a hit, `set_texture()` + "refreshed" the cached `SDL_Surface` with + `SDL_memmove(t->bitmap->pixels, bm->bits, ...)` - but `t->bitmap` was + created by `convert_texture()` via `SDL_CreateRGBSurfaceFrom(bm->bits, + ...)`, which wraps the given pointer rather than copying it, so + `t->bitmap->pixels` *is* `bm->bits` from whenever this entry was first + cached. Since a hit means the current call's `bm->bits` is that same + pointer by definition (it's part of the key), that memmove was always a + no-op self-copy - it never refreshed anything. `bm->bits` is a recycled + resource-cache slot reused for different content across draws while its + address stays the same, so a hit here was never a guarantee the pixels + underneath were still current, only that this entry's GL texture and + converted-surface buffers are the right size to reuse. Fixed by + re-pointing `t->bitmap->pixels`/`pitch` at the current frame's real + source (`bm->bits`, or the RSD8-decoded buffer) before blitting, instead + of copying into what was already the same address. Most noticeable with + content whose per-frame bitmap dimensions vary a lot, such as critter + animations after an AI upscale mod re-exports each frame at its own + independently upscaled size (see `modding/README.md` section 5) - + vanilla assets mostly dodge the first bug because many critter + animations reuse the same bounding-box size across most of their + frames, but the second bug affects any critter whose rendering revisits + the same resource-cache slot across different draws, regardless of mod + content. + +### 4.5. Debugging + +`engine-debug-tools/` holds engine instrumentation that's useful for +diagnosing rendering bugs but has no place in a normal build - kept +separate from `engine-patches/` (section 4.4) so it's never silently +included in `make dist`/`make package`. + +- `engine-debug-tools/engine-patches/` - patches in the same format and + numbered the same way as `engine-patches/` (section 4.4's own process + applies: scratch copy, `patch -p1`, regenerate via `diff -u` rather than + hand-editing hunks), applied on top of the regular `engine-patches/` + series, never in place of it. Currently one patch, adding a debug dump + hotkey: pressing **K** in-game dumps every frame of the + currently/most-recently-drawn critter's animation to `dumps/` (created + next to the running binary) as raw palette-index pixel data - 2-byte LE + width, 2-byte LE height, then width\*height index bytes, one file per + frame, named `id_frame.raw`. Deliberately independent of + the renderer's own texture/GL state, so a frame already corrupted in the + dump is a decode-time bug, while a frame that's clean in the dump but + wrong on screen is a rendering-time one. +- `engine-debug-tools/dump2png/` - converts those `.raw` dumps into + 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 + renderer's own palette/SDL state). Builds the same way + `modding/gen_animgallery.sh` builds `animgallery` - against + `modding/src/hacked` inside `modding/mdlp-image`'s Go toolchain, since + neither has a cgo dependency of its own - via + `engine-debug-tools/build-dump2png.sh `. + +`make dist_debug` builds everything above into `dist/` alongside the +normal `make dist` output: the engine with `engine-debug-tools/ +engine-patches/` applied in addition to the regular `engine-patches/` +series, plus `dist/dump2png` and `dist/convert-dumps.sh` (a `dump2png` +wrapper, same convention as `run-mods.sh` wrapping `run.sh`, pointed at +`dist/`'s own `res/data` and `dumps/` so no arguments are needed). There's +one `dist/` - whichever of `dist`/`dist_debug` ran last determines whether +the debug hotkey, `dump2png` and `convert-dumps.sh` are present; running +the other target again overwrites it back. + +```sh +make dist_debug +./dist/run.sh # or run-mods.sh - play until the bug shows, press K, quit +./dist/convert-dumps.sh # writes dist/dumps-png/*.png from dist/dumps/*.raw +``` ## 5. Android / Quest build diff --git a/build-image/build-engine.sh b/build-image/build-engine.sh index a5b27fc..d2a4f2a 100755 --- a/build-image/build-engine.sh +++ b/build-image/build-engine.sh @@ -42,6 +42,17 @@ for p in "$REPO_ROOT"/engine-patches/*.patch; do patch -p1 -d "$SCRATCH_ENGINE" < "$p" done +# ENGINE_DEBUG_TOOLS=1 (set by the Makefile's dist_debug target) additionally +# applies engine-debug-tools/engine-patches/ - debug-only instrumentation +# (see root README.md's "Debugging" section) that stays out of every normal +# build. +if [ -n "${ENGINE_DEBUG_TOOLS:-}" ]; then + echo "== ENGINE_DEBUG_TOOLS set - also applying engine-debug-tools/engine-patches/ ==" + for p in "$REPO_ROOT"/engine-debug-tools/engine-patches/*.patch; do + patch -p1 -d "$SCRATCH_ENGINE" < "$p" + done +fi + cd "$SCRATCH_ENGINE" echo "== Wiring up prebuilt SDL2/SDL2_mixer/fluidsynth-lite from the image ==" diff --git a/engine-debug-tools/build-dump2png.sh b/engine-debug-tools/build-dump2png.sh new file mode 100755 index 0000000..048bfec --- /dev/null +++ b/engine-debug-tools/build-dump2png.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# Builds dump2png (see dump2png/main.go) inside modding/mdlp-image's Go +# toolchain - dump2png has no cgo dependencies of its own, so it reuses that +# image rather than needing one of its own, same reasoning as +# modding/gen_animgallery.sh. +# +# Usage: ./build-dump2png.sh +# is relative to the repository root (typically +# dist/dump2png - see the root Makefile's dist_debug target and +# README.md's "Debugging" section) - it's written on both sides of the +# bind mount below, so an absolute or ../-escaping path won't resolve to +# the same file on the host afterward. +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")" +REPO_ROOT="$(cd .. && pwd)" + +[ "$#" -eq 1 ] || { echo "usage: $0 " >&2; exit 1; } +OUT_PATH="$1" +case "$OUT_PATH" in + /*) echo "PREFLIGHT FAIL: must be relative to the repo root, not absolute: $OUT_PATH" >&2; exit 1 ;; +esac + +fail() { echo "PREFLIGHT FAIL: $*" >&2; exit 1; } + +command -v docker >/dev/null 2>&1 \ + || fail "docker not found in PATH" + +IMAGE="${MDLP_REGISTRY_IMAGE:-questshock/mdlp}" +if [ -f "$REPO_ROOT/registry.env" ]; then + # shellcheck disable=SC1091 + source "$REPO_ROOT/registry.env" + IMAGE="${MDLP_REGISTRY_IMAGE:-$IMAGE}" +fi +IMAGE_TAG="$(<"$REPO_ROOT/modding/mdlp-image/VERSION")" +[ -n "$IMAGE_TAG" ] || fail "modding/mdlp-image/VERSION is empty" +IMAGE="$IMAGE:$IMAGE_TAG" + +docker image inspect "$IMAGE" >/dev/null 2>&1 \ + || fail "$IMAGE not found locally - run modding/mdlp-image.sh first" + +docker run --rm \ + -v "$REPO_ROOT:/workspace" \ + -w /workspace \ + -e HOST_UID="$(id -u)" \ + -e HOST_GID="$(id -g)" \ + "$IMAGE" \ + bash -c ' + set -euo pipefail + # dump2png builds against ../../modding/src/hacked via its own + # go.mod replace (matching that relative depth exactly, since + # engine-debug-tools/ sits next to modding/ at the repo root) - + # stage a patched copy (see modding/src/hacked-patches/) at that + # same relative position rather than building against the + # unpatched vendored source. + SCRATCH_DIR="engine-debug-tools/build/dump2png-src" + rm -rf "$SCRATCH_DIR" + mkdir -p "$SCRATCH_DIR/engine-debug-tools" + bash modding/mdlp-image/stage-hacked.sh modding "$SCRATCH_DIR/modding/src/hacked" + cp -a engine-debug-tools/dump2png "$SCRATCH_DIR/engine-debug-tools/dump2png" + mkdir -p "$(dirname "/workspace/$1")" + (cd "$SCRATCH_DIR/engine-debug-tools/dump2png" && go build -o "/workspace/$1" .) + ' _ "$OUT_PATH" + +echo "Built: $OUT_PATH" diff --git a/engine-debug-tools/dump2png/go.mod b/engine-debug-tools/dump2png/go.mod new file mode 100644 index 0000000..c40e7e2 --- /dev/null +++ b/engine-debug-tools/dump2png/go.mod @@ -0,0 +1,12 @@ +module questshock/dump2png + +go 1.25.0 + +// hacked/ is vendored under modding/src/ (see ../../modding/README.md); +// dump2png reuses its resource/palette reading code (ss1/world, +// ss1/content/bitmap) instead of duplicating System Shock's file formats. +require github.com/inkyblackness/hacked v0.0.0 + +require github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + +replace github.com/inkyblackness/hacked => ../../modding/src/hacked diff --git a/engine-debug-tools/dump2png/go.sum b/engine-debug-tools/dump2png/go.sum new file mode 100644 index 0000000..f6ec887 --- /dev/null +++ b/engine-debug-tools/dump2png/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/engine-debug-tools/dump2png/main.go b/engine-debug-tools/dump2png/main.go new file mode 100644 index 0000000..1a86cf7 --- /dev/null +++ b/engine-debug-tools/dump2png/main.go @@ -0,0 +1,105 @@ +// dump2png converts the raw indexed-pixel frame dumps written by the +// engine's debug dump hotkey (see ../engine-patches/ and the root +// README.md's "Debugging" section) into viewable PNGs, using the real game +// palette - see that same README section for the dump file format. +package main + +import ( + "encoding/binary" + "fmt" + "image" + "image/color" + "image/png" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/inkyblackness/hacked/ss1/content/bitmap" + "github.com/inkyblackness/hacked/ss1/resource" + "github.com/inkyblackness/hacked/ss1/world" +) + +func main() { + if len(os.Args) != 4 { + fmt.Fprintln(os.Stderr, "usage: dump2png ") + os.Exit(1) + } + dataDir, inDir, outDir := os.Args[1], os.Args[2], os.Args[3] + + entries, err := os.ReadDir(dataDir) + if err != nil { + panic(err) + } + merged := &world.ManifestEntry{Origin: []string{dataDir}, ID: dataDir} + for _, e := range entries { + if e.IsDir() { + continue + } + ext := strings.ToLower(filepath.Ext(e.Name())) + if ext != ".res" && ext != ".dat" { + continue + } + fe, err := world.NewManifestEntryFrom([]string{filepath.Join(dataDir, e.Name())}) + if err != nil { + continue + } + merged.Resources = append(merged.Resources, fe.Resources...) + } + sort.Slice(merged.Resources, func(i, j int) bool { return merged.Resources[i].ID < merged.Resources[j].ID }) + mod := world.NewMod(func([]resource.ID, []resource.ID) {}, func() {}) + if err := mod.World().InsertEntry(0, merged); err != nil { + panic(err) + } + pal, err := bitmap.NewPaletteCache(mod).Palette(resource.KeyOf(0x02BC, resource.LangDefault, 0)) + if err != nil { + panic(err) + } + colors := pal.ColorPalette(false) + + if err := os.MkdirAll(outDir, 0o755); err != nil { + panic(err) + } + files, err := os.ReadDir(inDir) + if err != nil { + panic(err) + } + for _, fi := range files { + if !strings.HasSuffix(fi.Name(), ".raw") { + continue + } + raw, err := os.ReadFile(filepath.Join(inDir, fi.Name())) + if err != nil { + fmt.Println("skip", fi.Name(), err) + continue + } + if len(raw) < 4 { + continue + } + w := int(binary.LittleEndian.Uint16(raw[0:2])) + h := int(binary.LittleEndian.Uint16(raw[2:4])) + pixels := raw[4:] + if len(pixels) < w*h { + fmt.Println("truncated", fi.Name()) + continue + } + img := image.NewRGBA(image.Rect(0, 0, w, h)) + for i := 0; i < w*h; i++ { + r, g, b, _ := colors[pixels[i]].RGBA() + img.Set(i%w, i/w, color.RGBA{R: byte(r >> 8), G: byte(g >> 8), B: byte(b >> 8), A: 0xff}) + } + outPath := filepath.Join(outDir, strings.TrimSuffix(fi.Name(), ".raw")+".png") + of, err := os.Create(outPath) + if err != nil { + fmt.Println("create failed", outPath, err) + continue + } + err = png.Encode(of, img) + of.Close() + if err != nil { + fmt.Println("encode failed", outPath, err) + continue + } + fmt.Println("wrote", outPath, w, h) + } +} diff --git a/engine-debug-tools/engine-patches/01-desktop-debug-dump-hotkey.patch b/engine-debug-tools/engine-patches/01-desktop-debug-dump-hotkey.patch new file mode 100644 index 0000000..7404f0e --- /dev/null +++ b/engine-debug-tools/engine-patches/01-desktop-debug-dump-hotkey.patch @@ -0,0 +1,153 @@ +--- a/src/GameSrc/objsim.c ++++ b/src/GameSrc/objsim.c +@@ -26,8 +26,10 @@ + // Object simulator code for Citadel + #define __OBJSIM_SRC + +-#include ++#include + #include ++#include ++#include + + #include "Shock.h" + +@@ -1924,6 +1926,93 @@ + *h = s_critterNativeH[id][frame]; + } + ++// Resource id of the last-drawn critter animation, updated every frame from ++// gameobj.c's FAUBJ_CRIT case - read by debug_dump_critter_animation() on ++// the debug dump hotkey (see engine-debug-tools/, root README.md's ++// "Debugging" section). Debug-build only: see ++// engine-debug-tools/engine-patches/. ++static Id s_lastCritterId = 0; ++ ++void debug_track_critter_draw(Id id) { s_lastCritterId = id; } ++ ++// Dumps every frame of the last-drawn critter's current animation (the ++// whole resource id, not just the currently-visible frame) as raw ++// palette-index bytes, one file per frame, independent of the renderer's ++// own texture/GL state - lets a corrupted frame be told apart from ++// corruption introduced later, in rendering. Format: 2-byte LE width, ++// 2-byte LE height, then width*height index bytes - converted to a viewable ++// image by engine-debug-tools/dump2png, not here, to avoid this debug path ++// depending on the renderer's own palette/SDL state. ++void debug_dump_critter_animation(void) { ++ RefTable *prt; ++ int numFrames, frame; ++ char path[256]; ++ FILE *f; ++ ++ if (s_lastCritterId == 0) { ++ fprintf(stderr, "[debug dump] no critter drawn yet\n"); ++ return; ++ } ++ ++ mkdir("dumps", 0755); // ignore EEXIST - fopen below reports any real failure ++ ++ prt = (RefTable *)ResLock(s_lastCritterId); ++ if (prt == NULL) { ++ fprintf(stderr, "[debug dump] ResLock(%d) failed\n", (int)s_lastCritterId); ++ return; ++ } ++ numFrames = prt->numRefs; ++ ResUnlock(s_lastCritterId); ++ ++ fprintf(stderr, "[debug dump] id=%d numFrames=%d\n", (int)s_lastCritterId, numFrames); ++ ++ for (frame = 0; frame < numFrames; frame++) { ++ FrameDesc *fd; ++ grs_bitmap decoded; ++ uchar *bits; ++ short w, h; ++ ++ fd = (FrameDesc *)RefLock(MKREF(s_lastCritterId, frame)); ++ if (fd == NULL) { ++ fprintf(stderr, "[debug dump] RefLock frame %d failed\n", frame); ++ continue; ++ } ++ ++ if (fd->bm.type == BMT_RSD8) { ++ if (gr_rsd8_convert(&fd->bm, &decoded) != 0 /* GR_UNPACK_RSD8_OK */) { ++ fprintf(stderr, "[debug dump] gr_rsd8_convert frame %d failed\n", frame); ++ RefUnlock(MKREF(s_lastCritterId, frame)); ++ continue; ++ } ++ bits = decoded.bits; ++ w = decoded.w; ++ h = decoded.h; ++ } else { ++ bits = fd->bm.bits; ++ w = fd->bm.w; ++ h = fd->bm.h; ++ } ++ ++ snprintf(path, sizeof(path), "dumps/id%d_frame%d.raw", (int)s_lastCritterId, frame); ++ f = fopen(path, "wb"); ++ if (f != NULL) { ++ uchar hdr[4]; ++ hdr[0] = (uchar)(w & 0xFF); ++ hdr[1] = (uchar)((w >> 8) & 0xFF); ++ hdr[2] = (uchar)(h & 0xFF); ++ hdr[3] = (uchar)((h >> 8) & 0xFF); ++ fwrite(hdr, 1, 4, f); ++ fwrite(bits, 1, (size_t)w * (size_t)h, f); ++ fclose(f); ++ fprintf(stderr, "[debug dump] wrote %s (%dx%d)\n", path, (int)w, (int)h); ++ } else { ++ fprintf(stderr, "[debug dump] failed to open %s for writing\n", path); ++ } ++ ++ RefUnlock(MKREF(s_lastCritterId, frame)); ++ } ++} ++ + errtype obj_set_secondary_properties() { + char i, j; + RefTable *prt; +--- a/src/GameSrc/Headers/objsim.h ++++ b/src/GameSrc/Headers/objsim.h +@@ -109,6 +109,10 @@ + 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 frame, short *w, short *h); ++// Debug-build only - see engine-debug-tools/, root README.md's "Debugging" ++// section. ++void debug_track_critter_draw(Id id); ++void debug_dump_critter_animation(void); + 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 ++++ b/src/GameSrc/gameobj.c +@@ -1019,6 +1019,7 @@ + { + short native_w = 0, native_h = 0; + get_critter_native_size(REFID(ref), REFINDEX(ref), &native_w, &native_h); ++ debug_track_critter_draw(REFID(ref)); + _fr_draw_bitmap(tpdata, _fdt_dist, 0, anch.ul.x, anch.ul.y, native_w, native_h); + } + release_critter_bitmap_fast(ref); +--- a/src/Libraries/INPUT/Source/sdl_events.c ++++ b/src/Libraries/INPUT/Source/sdl_events.c +@@ -481,6 +481,19 @@ + SDL_Event ev; + + while (SDL_PollEvent(&ev)) { ++ // Debug-build only (engine-debug-tools/, root README.md's ++ // "Debugging" section): K dumps every frame of the last-drawn ++ // critter's current animation to dumps/. Checked here, before the ++ // switch below dispatches on event type at all, so it can never ++ // collide with an existing game keybinding - a plain letter rather ++ // than Insert, which some keyboards don't send at all (compact ++ // layouts, or routed through Fn). ++ if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_k) { ++ extern void debug_dump_critter_animation(void); ++ debug_dump_critter_animation(); ++ continue; ++ } ++ + switch (ev.type) { + case SDL_QUIT: + // a bit hacky at this place, but this would allow exiting the game via the window's [x] button diff --git a/engine-debug-tools/engine-patches/02-desktop-debug-rsd8-decode-log.patch b/engine-debug-tools/engine-patches/02-desktop-debug-rsd8-decode-log.patch new file mode 100644 index 0000000..3e4b12f --- /dev/null +++ b/engine-debug-tools/engine-patches/02-desktop-debug-rsd8-decode-log.patch @@ -0,0 +1,166 @@ +--- a/src/Libraries/2D/Source/RSD/rsdcvt.c ++++ b/src/Libraries/2D/Source/RSD/rsdcvt.c +@@ -37,6 +37,7 @@ + * + */ + #include ++#include + #include + #include "grs.h" + #include "bitmap.h" +@@ -47,6 +48,14 @@ + + uchar *grd_unpack_buf=NULL; + ++// Set by debug_dump_critter_animation() (engine-debug-tools/, root ++// README.md's "Debugging" section) around its own gr_rsd8_convert() calls, ++// so a trace of this decode's internals - which path ran, whether it wrote ++// fewer bytes than the bitmap's own area, and (gr_rsd8_unpack(), ++// RSDUnpack.c) each opcode consumed - prints only for those calls, not ++// every RSD8 decode in the game. Debug-build only. ++int qs_debug_rsd8_trace = 0; ++ + /*************************************************/ + /* Puts 0's in place of skips and pads with 0's. */ + /* i.e., grd_unpack_buf is entirely overwritten. */ +@@ -138,6 +147,15 @@ + } + } + rsd_done: ++ if (qs_debug_rsd8_trace) { ++ long written = (long)(p_dst - dbm->bits); ++ long expected = (long)dbm->row * (long)dbm->h; ++ fprintf(stderr, ++ "[debug rsd8] path=%s sbm(w=%d h=%d row=%d) dbm(row=%d h=%d) " ++ "written=%ld expected=%ld short_by=%ld\n", ++ (dbm->w == dbm->row) ? "fast" : "slow", sbm->w, sbm->h, sbm->row, ++ dbm->row, dbm->h, written, expected, expected - written); ++ } + if ((over_run = (dbm->bits+dbm->row*dbm->h)-p_dst)) { + assert(over_run > 0); + LG_memset (p_dst, kSkipColor, over_run); +--- a/src/Libraries/2D/Source/RSD/RSDUnpack.c ++++ b/src/Libraries/2D/Source/RSD/RSDUnpack.c +@@ -22,6 +22,7 @@ + // + + #include "grs.h" ++#include + #include + + //---------------------------------------------------------------------------- +@@ -34,6 +35,10 @@ + short count,count2; + ushort longcode; + uint32_t longval, *longdest, *longsrc; ++ ++ extern int qs_debug_rsd8_trace; // rsdcvt.c - engine-debug-tools/, root README.md's "Debugging" section ++ uchar *src_start = src, *dest_start = dest; ++ long op_index = 0; + + do + { +@@ -42,6 +47,7 @@ + { + count = *(src++); // get count + val = *(src++); // get val ++ if (qs_debug_rsd8_trace) fprintf(stderr, "[debug rsd8 op] #%ld src_off=%ld dest_off=%ld type=run count=%d val=%d\n", op_index++, (long)(src-src_start), (long)(dest-dest_start), (int)count, (int)val); + + if (count>=kMinLongLoop) // if at least kMinLongLoop bytes, do long word stuff + { +@@ -63,6 +69,7 @@ + else if (code<0x80) // dump (copy) bytes + { + count = code; ++ if (qs_debug_rsd8_trace) fprintf(stderr, "[debug rsd8 op] #%ld src_off=%ld dest_off=%ld type=dump count=%d\n", op_index++, (long)(src-src_start), (long)(dest-dest_start), (int)count); + if (code>=kMinLongLoop) // if at least kMinLongLoop bytes, do long word stuff + { + count2 = count>>2; +@@ -84,6 +91,7 @@ + { + count = code & 0x007f; // clear high byte + val = longval = 0L; ++ if (qs_debug_rsd8_trace) fprintf(stderr, "[debug rsd8 op] #%ld src_off=%ld dest_off=%ld type=skip count=%d\n", op_index++, (long)(src-src_start), (long)(dest-dest_start), (int)count); + + if (count>=kMinLongLoop) // if at least kMinLongLoop bytes, do long word stuff + { +@@ -105,11 +113,15 @@ + longcode = * (ushort *) src; + src += 2L; + +- if (!longcode) break; // done? ++ if (!longcode) { ++ if (qs_debug_rsd8_trace) fprintf(stderr, "[debug rsd8 op] #%ld src_off=%ld dest_off=%ld type=terminator\n", op_index++, (long)(src-src_start), (long)(dest-dest_start)); ++ break; // done? ++ } + else if (longcode<0x8000) // skip (zero) + { + count = longcode; + val = longval = 0L; ++ if (qs_debug_rsd8_trace) fprintf(stderr, "[debug rsd8 op] #%ld src_off=%ld dest_off=%ld type=longskip count=%d\n", op_index++, (long)(src-src_start), (long)(dest-dest_start), (int)count); + + if (count>=kMinLongLoop) // if at least kMinLongLoop bytes, do long word stuff + { +@@ -129,6 +141,7 @@ + else if (longcode<0xC000) // dump (copy) + { + count = longcode & 0x7fff; // clear high bit ++ if (qs_debug_rsd8_trace) fprintf(stderr, "[debug rsd8 op] #%ld src_off=%ld dest_off=%ld type=longdump count=%d\n", op_index++, (long)(src-src_start), (long)(dest-dest_start), (int)count); + + if (count>=kMinLongLoop) // if at least kMinLongLoop bytes, do long word stuff + { +@@ -151,6 +164,7 @@ + { + count = longcode & 0x3fff; + val = *(src++); // get val ++ if (qs_debug_rsd8_trace) fprintf(stderr, "[debug rsd8 op] #%ld src_off=%ld dest_off=%ld type=longrun count=%d val=%d\n", op_index++, (long)(src-src_start), (long)(dest-dest_start), (int)count, (int)val); + + if (count>=kMinLongLoop) // if at least kMinLongLoop bytes, do long word stuff + { +--- a/src/GameSrc/objsim.c ++++ b/src/GameSrc/objsim.c +@@ -1979,11 +1979,43 @@ + } + + if (fd->bm.type == BMT_RSD8) { +- if (gr_rsd8_convert(&fd->bm, &decoded) != 0 /* GR_UNPACK_RSD8_OK */) { ++ extern int qs_debug_rsd8_trace; // rsdcvt.c - engine-debug-tools/, root README.md's "Debugging" section ++ uchar *first_copy; ++ size_t nbytes; ++ int rsd8_ok; ++ ++ qs_debug_rsd8_trace = 1; ++ rsd8_ok = gr_rsd8_convert(&fd->bm, &decoded) == 0 /* GR_UNPACK_RSD8_OK */; ++ qs_debug_rsd8_trace = 0; ++ if (!rsd8_ok) { + fprintf(stderr, "[debug dump] gr_rsd8_convert frame %d failed\n", frame); + RefUnlock(MKREF(s_lastCritterId, frame)); + continue; + } ++ ++ // Re-decode the same compressed source (fd->bm.bits, unchanged ++ // by the first call - only grd_unpack_buf, the shared ++ // decode-output buffer, gets overwritten) a second time and ++ // compare: decode runs back to back on this single thread, with ++ // nothing else able to touch fd->bm.bits or grd_unpack_buf in ++ // between, so a byte-for-byte match here means any garbled ++ // pixels are a deterministic property of this exact compressed ++ // stream (bad source data, or a decoder bug it triggers), not a ++ // one-off from a resource-cache slot being reused mid-decode. ++ nbytes = (size_t)decoded.w * (size_t)decoded.h; ++ first_copy = malloc(nbytes); ++ if (first_copy != NULL) { ++ memcpy(first_copy, decoded.bits, nbytes); ++ ++ qs_debug_rsd8_trace = 1; ++ gr_rsd8_convert(&fd->bm, &decoded); ++ qs_debug_rsd8_trace = 0; ++ ++ fprintf(stderr, "[debug dump] frame %d repeat-decode of bm.bits=%p %s\n", frame, ++ (void *)fd->bm.bits, memcmp(first_copy, decoded.bits, nbytes) == 0 ? "identical" : "DIFFERS"); ++ free(first_copy); ++ } ++ + bits = decoded.bits; + w = decoded.w; + h = decoded.h; diff --git a/engine-patches/04-desktop-fix-critter-sprite-scale.patch b/engine-patches/04-desktop-fix-critter-sprite-scale.patch index fd01d36..424b604 100644 --- a/engine-patches/04-desktop-fix-critter-sprite-scale.patch +++ b/engine-patches/04-desktop-fix-critter-sprite-scale.patch @@ -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 diff --git a/engine-patches/05-desktop-fix-texture-cache-collision.patch b/engine-patches/05-desktop-fix-texture-cache-collision.patch new file mode 100644 index 0000000..cc95f67 --- /dev/null +++ b/engine-patches/05-desktop-fix-texture-cache-collision.patch @@ -0,0 +1,84 @@ +--- a/src/MacSrc/OpenGL.cc ++++ b/src/MacSrc/OpenGL.cc +@@ -41,6 +41,7 @@ + + #include + #include ++#include + + 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 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, 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); diff --git a/modding/README.md b/modding/README.md index 1479a36..88dde7e 100644 --- a/modding/README.md +++ b/modding/README.md @@ -192,6 +192,8 @@ mods/ messages/