From ff68d1e23d364e40352d52c61a4feb1ebd3d4935 Mon Sep 17 00:00:00 2001 From: Henri Asseily Date: Sun, 31 Aug 2025 12:36:00 +0300 Subject: [PATCH] patterns: Added Apple IIGS SHR + SHR 3200 + SHR PWA Animation pattern (#432) * Added SHR pattern * Added IIGS SHR animation test file * Added pattern to readme * Added description and author --------- Co-authored-by: Nik --- README.md | 1 + patterns/SHR.hexpat | 189 ++++++++++++++++++ tests/patterns/test_data/SHR_animation#C20000 | Bin 0 -> 34892 bytes 3 files changed, 190 insertions(+) create mode 100644 patterns/SHR.hexpat create mode 100644 tests/patterns/test_data/SHR_animation#C20000 diff --git a/README.md b/README.md index c670472..5d18b36 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,7 @@ Everything will immediately show up in ImHex's Content Store and gets bundled wi | SDB | | [`patterns/sdb.hexpat`](patterns/sdb.hexpat) | [Shim DataBase](https://learn.microsoft.com/en-us/windows/win32/devnotes/application-compatibility-database) file format | | Shell Link | `application/x-ms-shortcut` | [`patterns/lnk.hexpat`](patterns/lnk.hexpat) | Windows Shell Link file format | | shp | | [`patterns/shp.hexpat`](patterns/shp.hexpat) | ESRI shape file | +| SHR | | [`patterns/SHR.hexpat`](patterns/SHR.hexpat) | Apple IIgs Super Hi-Res (SHR) + PaintWorks Animation (ANI) | | shx | | [`patterns/shx.hexpat`](patterns/shx.hexpat) | ESRI index file | | smk | | [`patterns/smk.hexpat`](patterns/smk.hexpat) | Smacker video file | | sup | | [`patterns/sup.hexpat`](patterns/sup.hexpat) | PGS Subtitle | diff --git a/patterns/SHR.hexpat b/patterns/SHR.hexpat new file mode 100644 index 0000000..0c4386f --- /dev/null +++ b/patterns/SHR.hexpat @@ -0,0 +1,189 @@ +/*! + Apple IIgs Super Hi-Res (SHR) + PaintWorks Animation (ANI) — ImHex pattern + + Supports: + • PIC $C1/$0000 — 32 KB uncompressed SHR screen image + • PIC $C1/$0002 — 3200-color (“Brooks”) per-scanline palettes + • ANI $C2/$0000 — PaintWorks animation: + 0x0000..0x7FFF : base SHR $C1/$0000 image + 0x8000.. : animation header + chunks + + PaintWorks animation structure (per reversed docs): + - Base image: uncompressed SHR ($C1/$0000) + - +0x8000 u32: total animation data length after header (file_len - 0x8008) + - +0x8004 u16: global frame delay in VBLs + - +0x8006 u16: flag/? (commonly 0x00C0 or 0x00C1) + - +0x8008 ...: one or more animation chunks: + chunk: + u32 chunk_len (includes this length field) + repeated { u16 offset; u16 value; } pairs + offset==0x0000 marks End-of-Frame (value is ignored) + - Offsets may target any byte in the 32 KB SHR space (pixels, SCBs, palettes). + This enables palette-cycling and SCB effects. + + References: + - CiderPress2 PaintWorks Animation notes (file structure, fields, semantics). +*/ + +import std.core; +import std.sys; +import std.math; + +#pragma endian little +#include + +#pragma description Apple IIgs Super Hi-Res (SHR) + PaintWorks Animation (ANI) +#pragma author hasseily + + +// ------------------------------ Constants ------------------------------ + +const u32 SHR_ROWS = 200; +const u32 SHR_BYTES_PER_ROW = 160; +const u32 SHR_PIXEL_DATA_SIZE = SHR_ROWS * SHR_BYTES_PER_ROW; // 32000 (0x7D00) + +const u32 PIC0000_FILE_SIZE = 0x8000; // 32768 +const u32 PIC0002_FILE_SIZE = 0x9600; // 38400 (32000 + 200*32) + +const u32 PIC0000_OFF_PIXELS = 0x0000; +const u32 PIC0000_OFF_SCB = 0x7D00; +const u32 PIC0000_OFF_RESERVED = 0x7DC8; +const u32 PIC0000_OFF_PALETTES = 0x7E00; + +const u32 PALETTE_COUNT = 16; +const u32 PALETTE_COLORS = 16; + +const u32 ANI_BASE_SHR_SIZE = 0x8000; // First 32 KB is a $C1/$0000 image +const u32 ANI_HDR_OFF = 0x8000; // Animation header starts here +const u32 ANI_MIN_TOTAL_SIZE = 0x8008; // base + header (no chunks) + +// ------------------------------ Types: SHR core ------------------------------ + +struct Row160 { u8 data[SHR_BYTES_PER_ROW]; }; + +// Scanline Control Byte +bitfield ShrSCB { + palette : 4; // 0..15 + reserved : 1; + color_fill : 1; + interrupt : 1; + mode_640 : 1; // 0=320, 1=640 +}; + +// helper: expand a 4-bit channel to 8-bit (0x0..0xF -> 0x00..0xFF) +fn expand4(u8 v) { return (v << 4) | v; }; + +bitfield Colors_gb { + blue : 4; // Blue (B3..B0) + green : 4; // Green (G3..G0) +}; + +bitfield Colors_r0 { + red : 4; // Red (R3..R0) + unused : 4; // Unused / reserved +}; + +// RGB444 stored as 0RGB +struct Rgb444_0RGB { + Colors_gb gb; + Colors_r0 r0; +} [[color(std::format("{:02X}{:02X}{:02X}", expand4(r0.red), expand4(gb.green), expand4(gb.blue)))]]; + +struct Palette16 { Rgb444_0RGB color[PALETTE_COLORS]; }; + +// $C1/$0000 raw 32 KB screen dump +struct SHR_PIC0000 { + Row160 pixels[SHR_ROWS] @ PIC0000_OFF_PIXELS; + ShrSCB scb[SHR_ROWS] @ PIC0000_OFF_SCB; + u8 reserved[56] @ PIC0000_OFF_RESERVED; + Palette16 palettes[PALETTE_COUNT] @ PIC0000_OFF_PALETTES; +}; + +// “Brooks” 3200-color: pixels + 200 per-line palettes (no SCBs) +struct BrooksLinePalette { Rgb444_0RGB color[PALETTE_COLORS]; }; + +struct SHR_PIC0002 { + Row160 pixels[SHR_ROWS] @ 0x0000; + BrooksLinePalette line_palettes[SHR_ROWS] @ SHR_PIXEL_DATA_SIZE; // 0x7D00 +}; + +// ------------------------------ Types: PaintWorks ANI ($C2/$0000) ------------------------------ + +/* Each operation modifies 1 word at an absolute offset in the 32 KB SHR area. + End-of-frame marker: offset == 0x0000 (value is ignored). */ +struct AniOp { + u16 offset [[color("0000AA")]]; // 0x0000..0x7FFE valid; 0x0000 = End-of-Frame + u16 value [[color("00AAAA")]]; // word to store at [offset] + // For convenience in the sidebar: + bool is_eof = (offset == 0x0000); +}; + +// A contiguous animation chunk: length + packed AniOp pairs. +// Most files have exactly one chunk that spans all frames. +struct AniChunk { + u32 chunk_len; // includes this field + + // ops_count = (chunk_len - 4)/4, unless chunk_len == 4 + // in which case: (__file_size - 0x8000 - 12)/4 + u64 ops_count64 = + (chunk_len == 4) + ? ( (__file_size > (0x8000 + 12)) ? ((__file_size - 0x8000 - 12) / 4) : 0 ) + : ( (chunk_len >= 4) ? (u64(chunk_len - 4) / 4) : 0 ); + + u32 ops_count = u32(ops_count64); + + // ops start immediately after chunk_len (offset +4) + AniOp ops[ops_count]; +}; + +// Header located at 0x8000 after the base 32 KB image +struct AniHeader { + u32 anim_data_len [[color("660000")]]; // total bytes of animation data after header + u16 frame_delay_vbl [[color("CC0000")]]; // global per-frame delay in VBLs (NTSC/PAL differ) + u16 flag_unknown; // usually 0x00C0 or 0x00C1 +}; + +// Full PaintWorks animation container +struct ANI_PaintWorks { + // Base frame: a normal uncompressed SHR image + SHR_PIC0000 base @ 0x0000; + + // Global animation header + AniHeader hdr @ ANI_HDR_OFF; + + // One or more chunks, typically exactly one: + AniChunk chunks[ std::math::min( + u32(16), // cap to keep ImHex happy in pathological cases + u32((__file_size - (ANI_HDR_OFF + sizeof(AniHeader))) > 3 ? + 1 + u32((__file_size - (ANI_HDR_OFF + sizeof(AniHeader))) / 0x10000000) : + 1) + ) ] @ (ANI_HDR_OFF + sizeof(AniHeader)); + + // Helpful computed values for inspection: + u64 file_len = __file_size; + u64 expected_anim_end = ANI_HDR_OFF + sizeof(AniHeader) + u64(hdr.anim_data_len); +}; + +// ------------------------------ Dispatcher ------------------------------ + +u64 __file_size = std::mem::size(); + +if (__file_size == PIC0000_FILE_SIZE) { + // Plain SHR dump + SHR_PIC0000 pic0000 @ 0x0000; + +} else if (__file_size == PIC0002_FILE_SIZE) { + // Brooks 3200-color + SHR_PIC0002 pic0002 @ 0x0000; + +} else if (__file_size >= ANI_MIN_TOTAL_SIZE) { + // Heuristic: treat as PaintWorks ANI if there’s room for base+header. + // (Many PW ANI files use ProDOS type $C2/$0000.) + ANI_PaintWorks ani @ 0x0000; + +} else if (__file_size >= SHR_PIXEL_DATA_SIZE) { + // Fallback: show pixels only for odd dumps + Row160 pixels_only[SHR_ROWS] @ 0x0000; +} + +ANI_PaintWorks ani_paintworks_at_0 @ 0; \ No newline at end of file diff --git a/tests/patterns/test_data/SHR_animation#C20000 b/tests/patterns/test_data/SHR_animation#C20000 new file mode 100644 index 0000000000000000000000000000000000000000..bcc964d540064d025efc1ef6a741b8877001d142 GIT binary patch literal 34892 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 x%wFR(aQ%X=0|rKnS4TwSlTlm;fQuJ~BgQN6y