From c7e25e323a7a0a87f5a669c0f84e94de7e3cb5bd Mon Sep 17 00:00:00 2001 From: Irmen de Jong Date: Sun, 9 Aug 2026 01:24:01 +0200 Subject: [PATCH] some script fixes and izx0 image format --- .agents/skills/prog8-coder/SKILL.md | 6 +- docs/source/todo.rst | 3 +- examples/m68k/amigaiff.p8 | 2 +- examples/m68k/izx0reader.p8 | 37 ++- ideas/scaled-indexing-IR.md | 302 ++++++++++++++++++ .../src/prog8/intermediate/IRInstructions.kt | 6 +- scripts/amiga-create-floppy.sh | 60 ++++ ...ga_export_izx0.py => amiga-export-izx0.py} | 23 +- scripts/cx16images.py | 0 9 files changed, 406 insertions(+), 33 deletions(-) create mode 100644 ideas/scaled-indexing-IR.md create mode 100755 scripts/amiga-create-floppy.sh rename scripts/{amiga_export_izx0.py => amiga-export-izx0.py} (94%) mode change 100644 => 100755 mode change 100644 => 100755 scripts/cx16images.py diff --git a/.agents/skills/prog8-coder/SKILL.md b/.agents/skills/prog8-coder/SKILL.md index 13ea4b69d..40b94d20c 100644 --- a/.agents/skills/prog8-coder/SKILL.md +++ b/.agents/skills/prog8-coder/SKILL.md @@ -35,7 +35,7 @@ Follow ALL the rules below carefully. - `ubyte`/`uword` = unsigned; `long` = signed 4-byte; `float` = 5-byte MS format; `str` = 0-terminated ubytes (max 255 chars) - `pointer` = address-sized unsigned integer. **Auto-adapts to the target**: a 16-bit `uword` on the 8-bit 6502 targets (C64, CX16, C128, PET32, virtual), a 32-bit `long` on the 32-bit M68000 targets (amiga500, qemu68k). Use it for addresses/pointers that must be portable across targets instead of hardcoding `uword` or `long`. - **float requires `%import floats`** at top of file, else compiler errors -- Arrays: max 256 bytes (512 for split word arrays). For larger data, use `memory()` + pointers +- Arrays: the size limit is a **byte budget** on all targets. On 6502 targets: max 256 bytes total (512 for split word arrays), so element counts are 256/4=64 for long arrays, 256/2=128 for word arrays, 256 for byte arrays. On M68000 targets (amiga500, qemu68k): max 32768 bytes total (16384 for word arrays, 8192 for long arrays). For larger data, use `memory()` + pointers - `memory(name, size)` returns the address to a statically reserved block of memory. The result is `pointer`-sized: a `uword` (16-bit) on 6502 targets, a `long` (32-bit) on M68000 targets. - To point a typed pointer at a `memory()` block, assign the uword directly: `^^MyStruct ptr = memory("name", size)`. The `^^Type:expression` syntax (below) only works with array literals, not general uword expressions. - Struct initialization: `^^StructType ptr = ^^StructType:[val1,val2,...]` (the `^^StructType:` can be omitted if inferable). This `^^Type:[...]` syntax does NOT work with variables or `memory()` — only literal arrays. @@ -72,8 +72,8 @@ No call stack for variable storage — recursion overwrites locals. To handle it ## Strings, Arrays & Pointers - String escapes: `\\`, `\"`, `\'`, `\n`, `\r`, plus hex/unicode: `\xHH` (raw byte, no encoding applied) and `\uHHHH` (unicode codepoint) -- `str` / array: max 256 bytes. `long[]` limited to 64 entries (64x4=256). `str[]` for string arrays: `str[5] names = ["a","b","c","d","e"]` -- 2D arrays: `type[rows][cols] name`, access `name[r][c]`. Flat init list only (no nested `[[...]]`). Total size still ≤ 256 bytes +- `str` / array: the limit is a **byte budget** on all targets. On 6502 targets: max 256 bytes (so `long[]` limited to 64 entries, 64x4=256; word arrays 128 entries, 128x2=256). On M68000 targets (amiga500, qemu68k): max 32768 bytes (16384 for word arrays, 8192 for long arrays). `str[]` for string arrays: `str[5] names = ["a","b","c","d","e"]` +- 2D arrays: `type[rows][cols] name`, access `name[r][c]`. Flat init list only (no nested `[[...]]`). Total size is a byte budget: ≤ 256 bytes on 6502 targets; max 32768 bytes on M68000 targets. - str/array passed as pointer to subroutine (receiving subroutine gets `^^ubyte` or `^^element`) - **No const pointers** or pointer-to-pointer currently supported - **Parsing limitation**: `pointer[index].field` as assignment target needs `^^`: `pointer[index]^^.field = value` diff --git a/docs/source/todo.rst b/docs/source/todo.rst index f00528457..96a832eab 100644 --- a/docs/source/todo.rst +++ b/docs/source/todo.rst @@ -55,7 +55,8 @@ Romable (%option romable) IR/VM ^^^^^ -- make long shifts with values >= 32 consistent across targets: virtual wraps the shift count mod 32 (``5 << 33`` becomes ``5 << 1`` = 10), but qemu68k saturates to 0. uword shifts with values > 15 are an error. Pick one behavior and apply uniformly (also affects the ``examples/test.p8`` bit-shift tests, which currently have to omit 33 for long to stay target-agnostic). +- encode indexed scaling into IR (so that m68k codegen can use scale factor addressing) see ideas/scaled-indexing-IR.md +- make long shifts with values >= 32 consistent across targets: virtual wraps the shift count mod 32 (``5 << 33`` becomes ``5 << 1`` = 10), but qemu68k saturates to 0. uword shifts with values > 15 are an error. - maybe change all branch instructions to have 2 exits (label if branch condition true, and label if false) instead of 1, and get rid of the implicit "next code chunk" link between chunks. - implement more TODOs in AssignmentGen? - add more optimizations in IRPeepholeOptimizer? diff --git a/examples/m68k/amigaiff.p8 b/examples/m68k/amigaiff.p8 index cbef0770c..e9517e8dd 100644 --- a/examples/m68k/amigaiff.p8 +++ b/examples/m68k/amigaiff.p8 @@ -41,7 +41,7 @@ main { sys.exit(1) } - str imagefile = "PROGDIR:psygnosis.iff" + str imagefile = "PROGDIR:cara.iff" ; open custom screen ^^intuition.Screen myScreen = intuition.OpenScreenTagList(0, screentags) diff --git a/examples/m68k/izx0reader.p8 b/examples/m68k/izx0reader.p8 index edf4ca016..d13a4c2b8 100644 --- a/examples/m68k/izx0reader.p8 +++ b/examples/m68k/izx0reader.p8 @@ -10,6 +10,7 @@ main { sub start() { + ; open a 320x256, 5-plane screen long[] screentags = [ intuition.SA_Left, 0, intuition.SA_Top, 0, @@ -63,7 +64,9 @@ main { izx0 { - const long HEADER_SIZE = 48 + ; IZX0 header, palette, then compressed planes. + const long HEADER_SIZE = 52 + const long TOTAL_COMPRESSED_SIZE_OFFSET = 48 const long MAX_PALETTE_SIZE = 256 * 4 uword imageWidth @@ -93,34 +96,34 @@ izx0 { numColors = peekw(header + 12) uword paletteSize = peekw(header + 14) + ; palette follows the header clearPalette() bytesRead = dos.Read(file, palette, paletteSize) if bytesRead == paletteSize { - long largestPlane = 0 + ; buffer all compressed planes before decoding + long compressedSize = peekl(header + TOTAL_COMPRESSED_SIZE_OFFSET) uword plane - for plane in 0 to numPlanes - 1 { - long planeSize = peekl(header + 16 + plane * 4) - if planeSize > largestPlane - largestPlane = planeSize - } - if largestPlane > 0 and bitplanepointers!=0 { - pointer planeBuffer = exec.AllocVec(largestPlane, exec.MEMF_PUBLIC) - if planeBuffer != 0 { - for plane in 0 to numPlanes - 1 { - long streamSize = peekl(header + 16 + plane * 4) - bytesRead = dos.Read(file, planeBuffer, streamSize) - if bytesRead == streamSize { + if compressedSize > 0 and bitplanepointers!=0 { + pointer compressedBuffer = exec.AllocVec(compressedSize, exec.MEMF_PUBLIC) + if compressedBuffer != 0 { + bytesRead = dos.Read(file, compressedBuffer, compressedSize) + if bytesRead == compressedSize { + pointer compressedPlane = compressedBuffer + for plane in 0 to numPlanes - 1 { + long streamSize = peekl(header + 16 + plane * 4) long bitplane = peekp(bitplanepointers) + ; decode one plane into CHIP RAM if bitplane!=0 - compression.unZX0(planeBuffer, bitplane) + compression.unZX0(compressedPlane, bitplane) + compressedPlane += streamSize bitplanepointers++ } + success = true } - exec.FreeVec(planeBuffer) + exec.FreeVec(compressedBuffer) } } - success = true } } } diff --git a/ideas/scaled-indexing-IR.md b/ideas/scaled-indexing-IR.md new file mode 100644 index 000000000..c65a52e52 --- /dev/null +++ b/ideas/scaled-indexing-IR.md @@ -0,0 +1,302 @@ +# Scaled Indexing for IR LOADX / STOREX / STOREZX + +This document describes the design to move array index scaling out of the IR +generator and into the code generation backends, by giving the IR instructions +`LOADX`, `STOREX`, and `STOREZX` an explicit `scale` operand. + +The goal is to let the m68k backend use the 68020 hardware scaled-index +addressing mode (`(An, Xn*scale)`), while keeping the IR identical for all +targets and fixing the same problem uniformly for every backend. + +--- + +## 1. Current State (background) + +The IR generator pre-scales the array index: + +- `loadIndexReg` (`codeGenIntermediate/IRCodeGen.kt:2100-2128`) loads the + element index and immediately multiplies it by the element size + (`itemsize`), both on the 16-bit path (`:2115`, `multiplyByConst(UWORD, ...)`) + and on the 8-bit path (`:2126`, `multiplyByConst(UBYTE, ...)`). +- Therefore every consumer of `LOADX` / `STOREX` / `STOREZX` assumes the index + register already holds a **byte offset** into the array: + - The VM adds the raw index to the base address (`InsLOADX`/`InsSTOREX`/ + `InsSTOREZX` in `VirtualMachine.kt:576/646/693`). + - new6502 uses the raw index as a byte offset into the array + (`indexedLoad`/`storeExchange`/`zeroMemoryIndexed` in + `InstrLoadStore.kt:675/753/845`). + - m68k uses the raw index as `d0` in `(a0,d0.w)` / `(0,a0,d0.l)` forms + (`codeGenM68k/InstrLoadStore.kt`), with comments noting "index pre-scaled". + +Because the index is pre-scaled, the 68020's hardware scaled addressing mode +`(An, Xn*2)` / `(An, Xn*4)` cannot be used: the value in `Xn` would already be +scaled, so the hardware scaling would double-apply. + +--- + +## 2. Design Decision + +**The IR never pre-scales the index, regardless of compilation target.** + +- The index register in `LOADX` / `STOREX` / `STOREZX` always holds the + **element index**. +- Each instruction carries an explicit **`scale`** operand: the element size in + bytes. +- The effective address is: + + base + index*scale + labelSymbolOffset + +- `labelSymbolOffset` already exists on IR instructions and is already resolved + everywhere, including in the VM program loader + (`VmProgramLoader.kt:177-201`), so the intra-element byte offsets needed by + `setlsb` / `setmsb` ride along for free. + +### Why an explicit operand instead of deriving it from the type + +`setlsb` / `setmsb` store a single byte (`.b`) into word/long/float arrays where +the element size (2/4/5/8) differs from the size of the value being stored +(1). Only an explicit operand can express "store a byte at element index i of an +array whose elements are N bytes wide". Deriving the scale from the value type +would give 1 for these stores and break them. + +### Struct fields are unaffected + +Struct field access never goes through the scaled index register. Field offsets +are always constant byte offsets: + +- Pointer field access: `LOADI` / `STOREI` with `immediate = fieldOffset` + (`ExpressionGen.kt:1886-1897`, `AssignmentGen.kt:295-315`). +- Struct-variable field access: `LOADM` / `STOREM` with `symbolOffset`. +- Array of structs: index is multiplied by the struct size and added to the + pointer via `ADDR` (genuine pointer arithmetic, `ExpressionGen.kt:1866-1868`). + +So the "absolute fixed byte index" of a struct field never appears in a scaled +`LOADX`/`STOREX`/`STOREZX` index register. The `scale` operand applies only to +the element-index convention of these three opcodes. + +### Backward compatibility + +No backward compatibility is needed for existing `.p8ir` files; the format +changes freely. The serialized `scale` is always written for the three opcodes +(see Section 3), so files are self-describing. + +--- + +## 3. IR Representation + +### 3.1 The `scale` field + +- Add `val scale: Int = 1` to `IRInstruction` + (`intermediate/IRInstructions.kt`, constructor around `:902`). +- Validation: + - `scale >= 1`. + - `scale != 1` is only allowed on `LOADX`, `STOREX`, `STOREZX`. + +### 3.2 Serialization + +Format: trailing `,S=N`, always written for the three opcodes. + +``` +loadx.w r5,r2,arr,S=2 +storx.b r1,r2,floatarray,S=5 +storezx.b r3,arr,S=1 +``` + +Parsing: `parseIRCodeLine` (`intermediate/Utils.kt:87`) strips an optional +trailing `,S=N`; defaults to 1 when absent. The existing label + offset parsing +(`arr+1`, `Utils.kt:242-253`) is unchanged and composes with it: + +``` +storx.b r1,r2,arr+1,S=4 +``` + +`IRFileReader` / `IRFileWriter` need no changes; they round-trip through +`toString()` / `parseIRCodeLine`. + +### 3.3 Documentation updates + +- `instructionFormats` header comment (`IRInstructions.kt`, around `:710`). +- The 32-bit word-index note in `IRProgram.kt:315`. + +--- + +## 4. Scale Values by Target + +| Element type | 6502 (cx16/c64/c128/pet32) | m68k (amiga500/qemu68k) | VM | +|---------------|----------------------------|-------------------------|------| +| byte / bool | 1 | 1 | 1 | +| split word | 1 (accesses lsb/msb byte sub-arrays) | 1 | 1 | +| word | 2 | 2 | 2 | +| pointer | 2 (non-split), 1 (split) | 4 | 4 | +| long | 4 | 4 | 4 | +| float | 5 (Mflpt5) | 4 | 8 | + +Constraints that make everything fit: + +- 6502: `ARRAY_SIZE_LIMIT = 256` is a **byte budget** (`AstChecker.kt:1080-1099`): + word arrays <= 128 elements, long <= 64, float <= 51. A scaled byte offset + therefore always fits in a byte (max 50*5 + 4 = 254), so 8-bit index + arithmetic suffices. +- 32-bit targets: all IR registers are physically 32-bit; element index + <= 16383 (word) / 8191 (long/float), scaled value <= 32767, which fits in a + signed 16-bit `.w` index register. +- 68020 scaled addressing supports `*1`, `*2`, `*4` (and `*8` with `.l`); m68k + only ever sees scales 1/2/4. + +--- + +## 5. Changes by Module + +### 5.1 `intermediate` + +1. Add `scale: Int = 1` to `IRInstruction` with the validation from Section 3.1. +2. Serialize `,S=N` in `toString()` for `LOADX`/`STOREX`/`STOREZX`; parse it in + `Utils.parseIRCodeLine`. +3. Update the comments in `IRInstructions.kt` and `IRProgram.kt:315`. + +### 5.2 `codeGenIntermediate` - stop pre-scaling + +**`loadIndexReg`** (`IRCodeGen.kt:2100`): + +- Delete the two `multiplyByConst` calls (`:2115` and `:2126`). +- Drop the now-unused `itemsize` and `arrayIsSplitWords` parameters. +- Keep the byte-to-word `EXT` so 32-bit targets get a word-sized index. + +**Array-indexing sites - emit `scale = eltSize` on the instruction:** + +- `ExpressionGen.kt:646` (`indexByExpression`). +- `AssignmentGen.kt:746/766/787` (`translateRegularAssignArrayIndexed`). +- Regular-array `for` loop (`IRCodeGen.kt:752-769`): LOADX gets + `scale = elementSize`; the loop increment changes from + `addConstByteToReg(indexReg, elementSize)` to `+1`; the loop compare changes + from `lengthBytes` (= length*elementSize) to `iterableLength`. +- `setlsb`/`setmsb`, variable-index case (`BuiltinFuncGen.kt:931/952`): + `scale = eltSize`, `labelSymbolOffset = byteOffset(eltSize)`, and drop the + `ADD indexReg, byteOffset` that is no longer needed. The const-index branches + (`:923-940`, `:944-950`) keep baking the full byte offset into the `LOAD` + immediate with default `scale = 1`. + +**Pointer-arithmetic sites - keep the explicit multiply:** + +These sites use `loadIndexReg` to compute a real address (element offset +combined with a pointer via `ADDR` or `LOADI`/`STOREI`). They must multiply by +the element size themselves after `loadIndexReg`: + +- `ExpressionGen.kt:681` (`translatePointerIndexing`). +- `ExpressionGen.kt:1866` (array-of-structs pointer math, struct size). +- `AssignmentGen.kt:703/713/835` (pointer store paths). +- `BuiltinFuncGen.kt:867` (`setlsb`/`setmsb` on a pointer target). + +These paths do **not** use the `scale` operand; they keep genuine pointer +arithmetic, unchanged in behavior. + +### 5.3 `virtualmachine` + +`InsLOADX` (`:576`), `InsSTOREX` (`:646`), `InsSTOREZX` (`:693`): multiply the +index register by `i.scale` before adding it to the base address. The index +register is: + +- `reg2` for integer `LOADX` / `STOREX`, +- `reg1` for float `LOADX` / `STOREX` and for all `STOREZX` + (matches `instructionFormats`, `IRInstructions.kt:714/721/725`). + +`labelSymbolOffset` is already folded into the base address by +`VmProgramLoader.kt:177-201`, so no change is needed there. + +### 5.4 `codeGenM68k` + +In `InstrLoadStore.kt` (including the float variants at `:198/252/282`), after +the index has been loaded into `d0` (`loadIndexToD0`, `AsmGen.kt:162`): + +- **M68020 (qemu68k):** use the hardware scaled-index mode: + - `(a0,d0.w*N)` for the integer forms, + - `(0,a0,d0.w*N)` for the float forms (the index is a word value, always + <= 32767, so `.w*N` is correct). + - `labelSymbolOffset` folds into the displacement via `resolveAddress` + (`AsmGen.kt:212`), producing `(B,a0,d0.w*N)`. +- **M68000 (amiga500):** there is no scaled-index mode, so shift explicitly: + - `lsl.w #1,d0` for scale 2, + - `lsl.l #2,d0` for scale 4, + then keep the existing `(a0,d0.w)` / `(0,a0,d0.l)` forms. +- Refresh the stale "index pre-scaled" comments. +- `scale = 1` paths are unchanged. + +Gate on `AsmGen.cpu` (`AsmGen.kt:55`); only the 68020 path uses the scaled +addressing mode. + +### 5.5 `codeGenNew6502` + +The index register is always a byte on the 6502, and the array byte budget +(Section 4) guarantees the scaled offset fits in a byte. + +A small helper multiplies the byte index by `insn.scale`: + +- `scale` 1: no-op (byte and split arrays). +- `scale` 2: `asl`. +- `scale` 4: `asl` twice. +- any other scale (notably 5, the Mflpt5 float size): `TODO` - to be + implemented together with float array support. This leaves + `setlsb`/`setmsb` on float arrays unimplemented in this backend for now. + +The helper is used by `indexedLoad` (`:675`, byte path), `storeExchange` +(`:753`), and `zeroMemoryIndexed` (`:845`). `labelSymbolOffset` folds into the +base address via `resolveAddress`, so setlsb/setmsb needs no extra handling. + +Float `LOADX`/`STOREX`/`STOREZX` remain `TODO` in this backend +(`:711/861-862`), unrelated to this change. + +--- + +## 6. Out of Scope / Notes + +- The old SimpleAST 6502 backend (`codeGenCpu6502`) does not consume IR and is + unaffected. +- On the 6502, byte-size-budgeted arrays (Section 4) mean scaled offsets always + fit in a byte; no 16-bit carry propagation is required in the scale helper. +- The m68k regular-array `for` loop uses a BYTE-typed `INC`/`CMPI` on the loop + index. Verify during implementation that on m68k these operate on the full + 32-bit register so comparing against `iterableLength` works for arrays with + more than 255 elements; if not, widen the loop index register to WORD on + 32-bit targets only (the VM is unaffected). This addresses the known latent + m68k loop-index issue; 6502 loops are fine because of the byte budget. +- Pointer indexing keeps an explicit multiply (it is real pointer arithmetic, + not array indexing) and does not use the `scale` operand. + +--- + +## 7. Test Coverage + +- `intermediate`: `.p8ir` round-trip with `,S=N`; defaults to 1 when absent. +- qemu68k: compile word/long array loads and stores, assert the generated + `.asm` contains `(a0,d0.w*2)` / `(a0,d0.w*4)`; setlsb/setmsb variant asserts + `(B,a0,d0.w*N)`. +- amiga500: same programs, assert `lsl.w #1,d0` / `lsl.l #2,d0` are present and + that no `(a0,d0.w*...)` scaled forms appear. +- VM (`TestCompilerVirtual`, extend the existing regression at `:930`): word / + long / float element access with a variable index (scale 2/4/8), plus a + `setlsb`/`setmsb` case (scale + `labelSymbolOffset`). +- cx16 (new6502): word/long indexed access, assert the shift sequences (scale + 2/4); optional ksim65 execution test (`prog8tests.codegeneration.TestExecution6502` + pattern). +- Use the compile-and-grep-`.asm` test pattern from + `TestAmigaChipramOption.kt:96-103`. + +--- + +## 8. File Reference Map + +| Concern | File | +|---------|------| +| IR instruction definition, formats, validation, serialization | `intermediate/src/prog8/intermediate/IRInstructions.kt` | +| IR text parsing (`,S=N`) | `intermediate/src/prog8/intermediate/Utils.kt` | +| IR comment (32-bit word index note) | `intermediate/src/prog8/intermediate/IRProgram.kt:315` | +| IR code generator, `loadIndexReg`, for loop | `codeGenIntermediate/src/prog8/codegen/intermediate/IRCodeGen.kt` | +| Scaled store sites | `codeGenIntermediate/src/prog8/codegen/intermediate/AssignmentGen.kt` | +| Indexed loads, pointer math | `codeGenIntermediate/src/prog8/codegen/intermediate/ExpressionGen.kt` | +| `setlsb`/`setmsb` | `codeGenIntermediate/src/prog8/codegen/intermediate/BuiltinFuncGen.kt` | +| VM execution of the three opcodes | `virtualmachine/src/prog8/vm/VirtualMachine.kt` | +| VM label + offset resolution | `virtualmachine/src/prog8/vm/VmProgramLoader.kt:177-201` | +| m68k load/store emission | `codeGenM68k/src/prog8/codegen/m68k/InstrLoadStore.kt` | +| m68k `cpu`, `loadIndexToD0`, `resolveAddress` | `codeGenM68k/src/prog8/codegen/m68k/AsmGen.kt` | +| new6502 load/store emission | `codeGenNew6502/src/prog8/codegen/new6502/InstrLoadStore.kt` | +| Array byte budget limits | `compiler/src/prog8/compiler/astprocessing/AstChecker.kt:1080-1099` | diff --git a/intermediate/src/prog8/intermediate/IRInstructions.kt b/intermediate/src/prog8/intermediate/IRInstructions.kt index eca34e89a..571134d6b 100644 --- a/intermediate/src/prog8/intermediate/IRInstructions.kt +++ b/intermediate/src/prog8/intermediate/IRInstructions.kt @@ -107,7 +107,7 @@ All have type b or w or l or f. load reg1, value - load immediate value into register. If you supply a symbol, loads the *address* of the symbol! (variable values are loaded from memory via the loadm instruction) loadm reg1, address - load reg1 with value at memory address loadi reg1, reg2, value - load reg1 with value in memory indirect, pointed to by reg2 + offsetvalue 0-65535 (often used to read a field from a pointer to a struct, or with offset=0 just straight from the pointer) -loadx reg1, reg2, address - load reg1 with value at memory address indexed by value in reg2 (0-255, a byte) +loadx reg1, reg2, address - load reg1 with value at memory address indexed by value in reg2 (a byte 0-255 on 8-bit targets, a word 0-32767 on 32-bit targets) loadr reg1, reg2 - load reg1 with value in register reg2, "reg1 = reg2" loadhr reg1, slot - load cpu hardware register from calling convention slot (s0=A, s1=X, s2=Y, s3=AX, s4=AY, s5=XY, s6=FAC1, s7=FAC2) into reg1 loadhfaczero fpreg1 - load "cpu hardware register" fac0 into freg1.f @@ -116,9 +116,9 @@ storem reg1, address - store reg1 at memory address storei reg1, reg2, value - store reg1 in memory indirect, pointed to by reg2 + offsetvalue 0-65535 (often used to write a field from a pointer to a struct, or with offset=0 just straight to the pointer) storeim value, address - store an immediate value (constant) at memory address (the constant goes in the value field, NOT via a register). storezi reg1, value - store zero at memory pointed to by reg1 + offsetvalue 0-65535 (just like storei, but a shorthand to store a constant 0) -storex reg1, reg2, address - store reg1 at memory address, indexed by value in reg2 (0-255, a byte) +storex reg1, reg2, address - store reg1 at memory address, indexed by value in reg2 (a byte 0-255 on 8-bit targets, a word 0-32767 on 32-bit targets) storezm address - store zero at memory address -storezx reg1, address - store zero at memory address, indexed by value in reg1 (0-255, a byte) +storezx reg1, address - store zero at memory address, indexed by value in reg1 (a byte 0-255 on 8-bit targets, a word 0-32767 on 32-bit targets) storehr reg1, slot - store reg1 into cpu hardware register for calling convention slot (s0=A, s1=X, s2=Y, s3=AX, s4=AY, s5=XY, s6=FAC1, s7=FAC2) storehfaczero fpreg1 - store fpreg1.f into "cpu register" fac0 storehfacone fpreg1 - store fpreg1.f into "cpu register" fac1 diff --git a/scripts/amiga-create-floppy.sh b/scripts/amiga-create-floppy.sh new file mode 100755 index 000000000..b08997511 --- /dev/null +++ b/scripts/amiga-create-floppy.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euo pipefail + +root_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +output="${1:-${root_dir}/amigaiff.adf}" +program="${AMIGA_PROGRAM:-${root_dir}/amigaiff}" +library="${IFFPARSE_LIBRARY:-${root_dir}/Libs/iffparse.library}" +image="${PSYGNOSIS_IMAGE:-${root_dir}/psygnosis.iff}" +cara_image="${CARA_IMAGE:-${root_dir}/cara.iff}" +izx0_image="${IZX0_IMAGE:-${root_dir}/cara.izx0}" +izx0_reader="${IZX0_READER:-${root_dir}/izx0reader}" + +if ! command -v xdftool >/dev/null 2>&1; then + printf '%s\n' "error: xdftool from amitools is not installed or not on PATH" >&2 + exit 1 +fi + +if [[ ! -f "$program" ]]; then + printf '%s\n' "error: Amiga program not found: $program" >&2 + exit 1 +fi + +if [[ ! -f "$library" ]]; then + printf '%s\n' "error: iffparse.library not found: $library" >&2 + exit 1 +fi + +if [[ ! -f "$image" ]]; then + printf '%s\n' "error: IFF image not found: $image" >&2 + exit 1 +fi + +if [[ ! -f "$cara_image" ]]; then + printf '%s\n' "error: cara IFF image not found: $cara_image" >&2 + exit 1 +fi + +if [[ ! -f "$izx0_image" ]]; then + printf '%s\n' "error: IZX0 image not found: $izx0_image" >&2 + exit 1 +fi + +if [[ ! -f "$izx0_reader" ]]; then + printf '%s\n' "error: IZX0 reader not found: $izx0_reader" >&2 + exit 1 +fi + +xdftool -f "$output" create + \ + format PROG8 ofs + \ + boot install + \ + makedir Libs + \ + write "$library" Libs/iffparse.library + \ + write "$program" amigaiff + \ + write "$image" psygnosis.iff + \ + write "$cara_image" cara.iff + \ + write "$izx0_image" cara.izx0 + \ + write "$izx0_reader" izx0reader + +printf 'Created FFS Amiga floppy image: %s\n' "$output" +xdftool "$output" list / all info diff --git a/scripts/amiga_export_izx0.py b/scripts/amiga-export-izx0.py old mode 100644 new mode 100755 similarity index 94% rename from scripts/amiga_export_izx0.py rename to scripts/amiga-export-izx0.py index f8e86c4a9..fbea3b09b --- a/scripts/amiga_export_izx0.py +++ b/scripts/amiga-export-izx0.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 """ IZX0 Image File Format Specification & Exporter Script ====================================================== @@ -11,7 +12,7 @@ compressed bitplane streams using the ZX0 compression algorithm (via Salvador). File Layout: ------------ +-----------------------------------------------------------------------------+ -| Fixed Binary Header (48 Bytes, Big-Endian) | +| Fixed Binary Header (52 Bytes, Big-Endian) | +-----------------------------------------------------------------------------+ | Palette Data (Variable length = palette_data_size) | | - ECS Mode (is_aga=0): Array of UWORD ($0RGB, 2 bytes/color) | @@ -24,7 +25,7 @@ File Layout: | - Plane N Stream (Size = plane_sizes[N-1]) | +-----------------------------------------------------------------------------+ -Fixed Binary Header Memory Map (48 Bytes Total): +Fixed Binary Header Memory Map (52 Bytes Total): ------------------------------------------------ Offset (Hex) Offset (Dec) Type Description ------------------------------------------------------------------------------- @@ -37,8 +38,9 @@ Offset (Hex) Offset (Dec) Type Description 0x0C 12 UWORD Number of Palette Entries (2^nPlanes) 0x0E 14 UWORD Palette Data Size in Bytes 0x10 16 ULONG[8] Array of Compressed Sizes (Planes 0..7) +0x30 48 ULONG Total compressed bitmap data size ------------------------------------------------------------------------------- -Total Header Size: 48 Bytes (0x30) +Total Header Size: 52 Bytes (0x34) Palette Entry Specifications: ----------------------------- @@ -216,13 +218,14 @@ def export_izx0(input_path, output_path, force_aga=False, max_colors=256): palette_data_size = len(palette_bytes) - # 5. Build Fixed 48-Byte Binary Header (Big-Endian) + # 5. Build Fixed 52-Byte Binary Header (Big-Endian) plane_sizes = [0] * 8 for i in range(num_planes): plane_sizes[i] = len(compressed_planes[i]) + compressed_payload_size = sum(plane_sizes) header = struct.pack( - ">4sHHHBB HH 8I", + ">4sHHHBB HH 9I", b"IZX0", aligned_width, height, @@ -232,6 +235,7 @@ def export_izx0(input_path, output_path, force_aga=False, max_colors=256): num_palette_entries, # 2^num_planes palette_data_size, # Byte count of palette block *plane_sizes, + compressed_payload_size, ) # 6. Write Output File @@ -241,17 +245,16 @@ def export_izx0(input_path, output_path, force_aga=False, max_colors=256): for c_data in compressed_planes: f.write(c_data) - compressed_payload_size = sum(len(c) for c in compressed_planes) file_size = os.path.getsize(output_path) print(f"\nSaved '{output_path}' ({file_size} bytes total)") print( - f"Header: 48B | Palette: {palette_data_size}B ({num_palette_entries} entries) | Streams: {compressed_payload_size}B" + f"Header: 52B | Palette: {palette_data_size}B ({num_palette_entries} entries) | Streams: {compressed_payload_size}B" ) def main(): parser = argparse.ArgumentParser( - prog="export_izx0", + prog="amiga-export-izx0", description="Convert image files (PNG/BMP/etc.) into the IZX0 planar compressed Amiga format.", formatter_class=argparse.RawDescriptionHelpFormatter, ) @@ -285,6 +288,10 @@ def main(): help="Max colors for quantization palette reduction (default: 256)", ) + if len(sys.argv) == 1: + parser.print_help() + return + args = parser.parse_args() if not os.path.exists(args.input): diff --git a/scripts/cx16images.py b/scripts/cx16images.py old mode 100644 new mode 100755