some script fixes and izx0 image format

This commit is contained in:
Irmen de Jong committed 2026-08-09 12:26:59 +02:00
1 parent 1e2fc4d6f9
commit c7e25e323a
9 files changed
+406 -33

No files matched your search

+3 -3
View File
@@ -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`
+2 -1
View File
@@ -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?
+1 -1
View File
@@ -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)
+20 -17
View File
@@ -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
}
}
}
+302
View File
@@ -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` |
@@ -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
+60
View File
@@ -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
+15 -8
View File
@@ -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):
Regular → Executable
View File
File mode changed.