Migrating from ORCA/C to Clang for the IIgs.
This commit is contained in:
parent
087bc77a6b
commit
de280b7a32
85 changed files with 7646 additions and 7410 deletions
|
|
@ -189,7 +189,7 @@ The user should pick which to prioritize.
|
|||
|
||||
The repo's auto-memory at
|
||||
`~/.claude/projects/-home-scott-claude-joeylib/memory/` includes
|
||||
JoeyLib-wide context (CMake quirks, ORCA-C gotchas, perf
|
||||
JoeyLib-wide context (CMake quirks, IIgs toolchain gotchas, perf
|
||||
constraints) plus `project_joeylib_agi_plan.md` which describes the
|
||||
overall AGI port plan. All persist into the next session
|
||||
automatically; no need to reload.
|
||||
|
|
@ -197,9 +197,8 @@ automatically; no need to reload.
|
|||
Specifically relevant to the AGI port:
|
||||
|
||||
- `project_joeylib_agi_plan.md` — overall plan, 3-4 month estimate
|
||||
- `feedback_orca_c_atoi_segment.md` — don't use atoi in AGI code
|
||||
(it pushes the IIgs ORCA-C stdio cluster past 64 KB; we use a
|
||||
manual parseArgU8 instead)
|
||||
- AGI code uses a manual `parseArgU8` instead of atoi to keep the
|
||||
IIgs link budget small.
|
||||
- `project_perf_directive.md` — IIgs is the perf floor; every
|
||||
other target must match or beat it
|
||||
- `feedback_iigs_speed.md` — IIgs must be absolute fastest
|
||||
|
|
@ -213,6 +212,6 @@ Specifically relevant to the AGI port:
|
|||
`feedback_git.md`).
|
||||
- Always `source toolchains/env.sh` before any `make` -- the user
|
||||
has added that to .claude/settings.local.json allow list.
|
||||
- DJGPP / ORCA / vasm / m68k-atari-mint paths come from env.sh.
|
||||
- DJGPP / llvm-mos / vasm / m68k-atari-mint paths come from env.sh.
|
||||
- The user does NOT need hand-holding -- they're an expert C dev.
|
||||
Skip excessive explanation; describe what was done and move on.
|
||||
|
|
|
|||
294
IIGS_PERF_PLAN.md
Normal file
294
IIGS_PERF_PLAN.md
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
# IIgs Performance Fix Plan
|
||||
|
||||
Derived from the 2026-06 IIgs render-path audit (per-op 65816 cycle analysis +
|
||||
adversarial verification). Numbers below are verifier-confirmed unless marked
|
||||
"est." Ordered by dependency, then by realistic payoff.
|
||||
|
||||
## Implementation status (2026-06-23)
|
||||
|
||||
All phases implemented and compile/link clean via the clang/llvm-mos w65816
|
||||
toolchain. Every change is
|
||||
correct-by-construction from the verified analysis; the hot-path 65816 asm
|
||||
(Phases 3, 4, 5.1) still needs **pixel validation on hardware/MAME** -- the
|
||||
headless emulator here can't load the 1.6 MB disk image, so I could not observe
|
||||
output. Two micro-opts are left as marked residuals where the phase's value is
|
||||
already captured.
|
||||
|
||||
- **Phase 0 -- DONE.** Removed the vestigial `SEI` + chunk-CLI from
|
||||
`iigsFillRectInner`; added the ~29 ops/sec ceiling + SEI-inflation note to
|
||||
`PERF.md` and the `uber-perf-table` generator.
|
||||
- **Phase 1 -- DONE.** `drawShowcase()` in `examples/uber/uber.c` renders one of
|
||||
each primitive into a labelled grid; the timed benchmark is unchanged (op
|
||||
sequence/args identical, so cross-port hashes still compare); ends green. No
|
||||
live progress bar (it would clobber the deliberately-seeded per-op input
|
||||
states; frozen showcase + joeylog is the fidelity-preserving choice).
|
||||
- **Phase 2 -- DONE.** Two parts: (a) `surfaceMarkDirtyRect` widens single-row
|
||||
(`h==1`) marks inline in C, dropping the cross-segment `iigsMarkDirtyRowsInner`
|
||||
JSL; (b) **`jlDrawPixel` fully inlined on IIgs** (draw.c) -- no `plotPixelNoMark`
|
||||
/ `surfaceMarkDirtyRect` call layers around the single asm plot, mark widened
|
||||
inline via `jlStageGet()` guard. This is the 5-10x path. (The plan's
|
||||
fold-into-asm form is UNSAFE -- `iigsDrawPixelInner` can't see `s`, would mark
|
||||
`gStage` for heap-surface draws; the C inline keeps the guard.)
|
||||
- **Phase 3 -- DONE.** Computed-jump slam for ALL middle widths (`friMidSlamTbl`
|
||||
via an **RTS computed-goto** + X bias -- ascending table kept, no bank-0 vector
|
||||
hazard; `nWords==0` lands on the odd-byte writer). Partial fills go 7 -> 3
|
||||
cyc/byte. Plus the `friMidMvn` X-reuse cleanup (now dead code path removed).
|
||||
- **Phase 4 -- DONE (4.1 residual).** 4.2: removed the per-plot `dcSavedCol`
|
||||
round-trip (parity via free X). 4.3: the 4 octant row bases are maintained
|
||||
incrementally (+/-160 per step) instead of 4 LUT lookups/iteration. 4.1 (move
|
||||
remaining `dc*` scratch to direct page) is the **residual**: ~4% more after
|
||||
4.2+4.3, but needs a stack-frame-locals refactor to resolve the `D=SP+8` arg
|
||||
conflict -- disproportionate blind-refactor risk for the gain.
|
||||
- **Phase 5 -- DONE.** 5.1: `iigsSurfaceClearFastInner` -- post-init PHA stack-slam
|
||||
(2 cyc/byte, ~1.5x) with AUXWRITE redirect to bank $01; SHR shadow already
|
||||
inhibited by halInit; SP restored before CLI; the C macro dispatches here and
|
||||
keeps the STA `iigsSurfaceClearInner` as the init-safe fallback. (Non-chunked
|
||||
SEI ~23 ms; chunk like the present if audio glitches.) 5.2: `surfaceMarkDirtyAll`
|
||||
is now two `memset`s. 5.3 subsumed by 5.1.
|
||||
- **Phase 6 -- DONE (deep residual).** `jlSpriteSaveAndDraw` already shares
|
||||
`shift`, dodges the MUL4 2D-index, and marks once (the C-level win). The deeper
|
||||
dest-address sharing threaded through `spriteCompiledSaveUnder`/`Draw` is
|
||||
codegen-level surgery for ~1.05x -- left as the **residual**.
|
||||
- **Phase 7 -- DONE.** `docs/DESIGN.md` documents the deliberate double-buffer
|
||||
cost and why single-buffering the IIgs was rejected.
|
||||
|
||||
### Highest hardware-validation priority (new asm, pixel-validate first)
|
||||
|
||||
1. `jlFillRect` at assorted x/w (NOT just 0/320): the computed-goto slam must
|
||||
fill exactly `[x..x+w)` with no over/underrun. Check odd widths + odd x.
|
||||
2. `iigsSurfaceClearFastInner`: the screen must clear to the right color with no
|
||||
stray writes; watch for audio glitches (the ~23 ms SEI window).
|
||||
3. `jlDrawCircle`: shape unchanged after the incremental-row-base rewrite.
|
||||
4. `jlDrawPixel`: visually correct + ops/sec jump.
|
||||
|
||||
### Hardware-validation checklist (run on your MAME/GSplus + the 4 ports)
|
||||
|
||||
1. Build + run `examples/uber` on IIgs; confirm the showcase grid renders all
|
||||
primitives legibly and the benchmark still completes (green exit).
|
||||
2. Re-capture `joeylog.txt` on all four ports; regenerate `PERF.md` via
|
||||
`tools/uber-perf-table`. Expect IIgs `fillRect 320x200` and `stagePresent`
|
||||
to drop toward ~25-28 (SEI removal de-inflates fillRect).
|
||||
3. `tools/diff-uber-hashes` IIgs-vs-{amiga,st,dos}: all ops must still match
|
||||
(the C dirty-mark + memset changes are portable and must not alter pixels).
|
||||
If any op diverges, the dirty-mark fold changed a present extent -- inspect.
|
||||
4. Eyeball the circle (`jlDrawCircle`) output unchanged after the dcSavedCol
|
||||
change; confirm `jlDrawPixel` ops/sec rose.
|
||||
|
||||
|
||||
|
||||
## Ground truth / framing
|
||||
|
||||
- **Hard floor:** any full-screen 32000-byte move = 16000 word-stores x 6 cyc
|
||||
(`STA long,X $9F` or `PEI`, both 3 cyc/byte) / 2.8 MHz = ~34 ms = **~29 ops/sec
|
||||
max**. Clear, full-width fillRect, and present are all pinned to this; you
|
||||
cannot beat it without moving fewer bytes (the dirty-row system already does
|
||||
that for sparse frames).
|
||||
- **The benchmark lies for SEI ops.** `PERF.md` `fillRect 320x200`=60 and
|
||||
`stagePresent`=42 exceed/approach the 29-ops/sec floor and are inflated; clear=28
|
||||
is honest. Fix measurement FIRST (Phase 0) or you cannot tell wins from clock noise.
|
||||
- **Two architectural realities that are NOT bugs:** (a) the back-buffer ->
|
||||
`$E1` design pays the screen twice on full-redraw frames, but the single-buffer
|
||||
"fix" is refuted (slow `$E1`, overdraw inverts the win, tearing) -- keep it; the
|
||||
dirty-row system is the right mitigation. (b) The circle OUTLINE trails the ST
|
||||
due to 65816 register pressure and may never fully win; bank the partial gain.
|
||||
|
||||
## Cross-cutting rules (apply to every asm change)
|
||||
|
||||
- Keep the assembler's register-width state correct after every `REP`/`SEP` so
|
||||
immediates are sized correctly.
|
||||
- Keep each asm routine in the SAME load segment as its C caller (DRAWPRIMS for
|
||||
the draw primitives); a separate segment silently produces calls-to-nowhere.
|
||||
- `gStageMinWord` / `gStageMaxWord` must stay `uint8_t[]` so the asm that walks
|
||||
them per byte stays correctly aligned with the C-side element size.
|
||||
- PHA/PEI stack-slam paths REQUIRE `SEI` and are unsafe during `joeyInit` -- gate
|
||||
them post-init (`gModeSet`).
|
||||
- 4bpp word stores need even byte index (`x/2` even, i.e. `x % 4 == 0`) for the
|
||||
computed-jump slam entry.
|
||||
- Build hygiene: `source toolchains/env.sh`; DELETE the example binary before
|
||||
rebuild (make leaves IIgs binaries stale); pass `spriteEmitIigs.c` first in
|
||||
LIB_SRCS (linker bank-packing order).
|
||||
- **Validation after every change:** re-run `examples/uber`, confirm
|
||||
`jlSurfaceHash` per op is UNCHANGED (pixel-identical to the IIgs golden) and
|
||||
ops/sec moved the right way. Verify visually in MAME (checkpoint poll or
|
||||
snapshot) -- joeyLog alone misses wrong-pixel-but-right-control-flow bugs.
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 -- Make measurement trustworthy (PREREQUISITE)
|
||||
|
||||
Everything downstream is judged against the benchmark; fix it first.
|
||||
|
||||
- **0.1 Audit & likely remove `iigsFillRectInner`'s `SEI`** (joeyDraw.asm:315,
|
||||
chunk-CLI machinery :585-614). The body does only long-mode writes to the
|
||||
bank-$01 back buffer with no SP/shadow hijack (per its own comment :284-292),
|
||||
so the SEI appears vestigial. Confirm nothing in the fill needs interrupt
|
||||
atomicity (MVN is interruptible/resumable by design), then remove SEI + the
|
||||
FRI_CHUNK_ROWS CLI logic. Expected: fillRect-full de-inflates 60 -> ~27
|
||||
(matching clear), AND the audio-starvation risk goes away. If some atomicity IS
|
||||
required, document exactly what and keep it.
|
||||
- **0.2 Add a cycle-ceiling sanity check / annotation.** Document the ~29 ops/sec
|
||||
full-screen floor in `PERF.md`; flag any full-screen op reading above it as
|
||||
suspect. Optional: assert in the harness.
|
||||
- **0.3 Reconcile `present`=42** (its SEI is REAL -- stack/shadow hijack -- so it
|
||||
can't just be removed). Capture an honest present rate via an SEI-immune time
|
||||
source or a diagnostic no-SEI measurement build; expect ~25 ops/sec. Update the
|
||||
table and the cross-port comparison accordingly.
|
||||
- **Re-baseline `PERF.md`** with honest numbers before Phase 2+.
|
||||
|
||||
Exit criterion: clear, fillRect-full, and present all read within ~10% of each
|
||||
other (~25-28 ops/sec), consistent with the cycle model.
|
||||
|
||||
## Phase 1 -- Make the uber demo legible (show, don't smear)
|
||||
|
||||
Right now `examples/uber` shows the user a solid colored screen and teaches them
|
||||
nothing. Cause, concretely:
|
||||
- `runAllTests()` (uber.c:222-285) runs each op in a 16-frame tight loop and
|
||||
NEVER calls `jlStagePresent()` during or between the timed ops. The screen is
|
||||
frozen on the last present before the run (the red-bar-on-black at uber.c:379)
|
||||
for the whole ~80 s run, then ends on a bare `jlSurfaceClear(gStage, 2)`
|
||||
(uber.c:390) -> solid green.
|
||||
- Even if it did present, the timed ops are full-screen or fixed-coordinate
|
||||
(`fillRect 320x200`, `surfaceClear`, pixel@100,100, line 0,50->319,50, ...), so
|
||||
they overwrite each other into one solid color. There is no spatial separation.
|
||||
|
||||
Goal: the demo should visibly demonstrate what each primitive does AND run the
|
||||
benchmark without leaving a meaningless screen. Separate "show the user" from
|
||||
"measure throughput" -- they are different jobs.
|
||||
|
||||
- **1.1 Visual showcase pass (new), runs once before the benchmark, UNTIMED.**
|
||||
Lay out a labeled grid (e.g. 4 cols x 5 rows of ~72x36 cells with a gutter) and
|
||||
render each primitive ONCE into its own cell so nothing overlaps: pixel, line
|
||||
H/V/diag, rect outline, circle (small), fillRect (small), fillCircle, a tile,
|
||||
tileCopyMasked result, the compiled sprite, a 16-entry palette ramp swatch, an
|
||||
SCB per-line strip, a sample-pixel readout. Full-screen ops (surfaceClear,
|
||||
fillRect 320x200, present) cannot be shown at native size in a cell -- represent
|
||||
each with a small labeled swatch and say so. `jlStagePresent()` once, then hold
|
||||
for a readable beat (frame-counted ~2-3 s, or `jlWaitForAnyKey` -> "press a key
|
||||
to begin benchmark"). THIS is the part that makes sense to a user.
|
||||
- **1.2 Benchmark pass: keep timing/hash fidelity, show a sane status screen.**
|
||||
Keep the `timeOp` loop and every op's args + call order EXACTLY as-is -- the
|
||||
cross-port `jlSurfaceHash` golden values depend on the precise op sequence, so
|
||||
changing them invalidates the validation harness. Stop presenting the timing
|
||||
garbage: draw a static "BENCHMARKING..." screen plus a progress indicator (op
|
||||
counter / bar, optionally each op's just-measured ops/sec) updated BETWEEN
|
||||
`timeOp` calls -- never inside the timed loop. End on an explicit "DONE -- press
|
||||
any key" screen, not a bare solid clear.
|
||||
- **1.3 Labels.** `jlDrawText` exists (tile.h:119) but needs a font surface +
|
||||
asciiMap, which uber does not currently load. Either add a small font surface +
|
||||
asciiMap and label each cell, or (fallback) use fixed spatial layout + a short
|
||||
on-screen color-key strip and a legend printed via `jlLogF`.
|
||||
|
||||
Cautions:
|
||||
- All showcase drawing AND the progress-UI updates must be OUTSIDE any timed
|
||||
window -- zero effect on ops/sec.
|
||||
- Do NOT change the timed ops' arguments or sequence (golden-hash stability).
|
||||
- Keep it port-agnostic: uber is shared example code that also runs on
|
||||
Amiga/ST/DOS -- no IIgs-only assumptions in the showcase.
|
||||
- Do this AFTER Phase 0 so any per-op ops/sec shown on the status screen is the
|
||||
corrected (honest) number, not the inflated one.
|
||||
|
||||
Exit criterion: a first-time user watching the demo can see and identify each
|
||||
primitive's output, and the benchmark phase shows readable progress + a clear
|
||||
finish -- never a bare solid color.
|
||||
|
||||
## Phase 2 -- Kill the per-primitive C+JSL tax (biggest real-world win)
|
||||
|
||||
`jlDrawPixel` costs **~1595 cyc/call** (2.8e6 / 1755), ~94% overhead; the actual
|
||||
nibble RMW is ~30 cyc. Every marking primitive pays a second cross-segment JSL.
|
||||
|
||||
- **2.1 Fold dirty-marking into the asm inner loops.** `jlDrawPixel` (draw.c:95),
|
||||
`jlFillRect` (draw.c:448-ish via fillRectOnSurface), line, and circle each call
|
||||
`surfaceMarkDirtyRect` AFTER the plot JSL -- a second JSL into
|
||||
`iigsMarkDirtyRowsInner` (~49 cyc plumbing x2 + the row loop). The inner loops
|
||||
already know the row(s) and extent (fillRect has `midStart`/`trailingByte` per
|
||||
row; pixel has `y` and the byte). Update `gStageMinWord[y]`/`gStageMaxWord[y]`
|
||||
inline and make `surfaceMarkDirtyRect` a no-op on the IIgs fast paths.
|
||||
- Confirmed win: removes ~120 cyc from EVERY marking primitive; **5-10x on
|
||||
`jlDrawPixel`**.
|
||||
- **2.2 Trim the asm prologue/epilogue for the tiny ops.** `iigsDrawPixelInner`
|
||||
(joeyDraw.asm:1158-1227) keeps scratch in 6-cyc long-absolute (`dpxlTmp`,
|
||||
`dpxlNibPart`) and pays full PHP/PHB/PHD ... PLD/PLB/PLP framing around a ~50-cyc
|
||||
plot. Use registers/DP for the scratch; drop framing not needed when DBR/D are
|
||||
already correct. (est ~1.2-1.4x on the inner once 2.1 lands.)
|
||||
- **2.3 Batch/inline entry for pixel-heavy callers (optional).** A span/row-run
|
||||
API (or inlining the in-bounds nibble RMW into the C via macro, the way fillRect
|
||||
was de-wrappered) amortizes the JSL across many pixels for plotting-heavy code.
|
||||
|
||||
Exit criterion: `jlDrawPixel` >> 1755; line/rect call cost drops by the second-JSL
|
||||
amount; hashes unchanged.
|
||||
|
||||
## Phase 3 -- fillRect partial-width slam gate
|
||||
|
||||
The 3-cyc/byte slam fires ONLY for exactly `x==0 && w==320` (`friFullWidth`,
|
||||
joeyDraw.asm:406-420). Everything else falls to seed+`MVN` at **7 cyc/byte**
|
||||
(2.33x slower), confirmed.
|
||||
|
||||
- **3.1 Computed jump into the unrolled `STA long,X` table.** Set `X =
|
||||
curRow + midStart`, then `JMP (addr)` into the 80-store run at store
|
||||
`#(80 - midBytes)`. Gives 3 cyc/byte for ANY width. Keep MVN only for tiny odd
|
||||
remainders. Drop-in safe (bank-$01 long stores, no SEI/shadow). Respect the
|
||||
4bpp even-byte-index rule for the entry. **1.8-2.3x on all partial-width fills.**
|
||||
- **3.2 Cleanups (minor):** cache `curRow+midStart` instead of recomputing it
|
||||
(joeyDraw.asm:542-565); hoist loop-invariant edge offsets (`leadingByte`/
|
||||
`trailingByte` relative to curRow, += 160/row); one `sep`/`rep` window per row
|
||||
for both edge RMWs.
|
||||
|
||||
## Phase 4 -- circle / line register pressure (~1.27x, partial)
|
||||
|
||||
- **4.1 Move Bresenham scratch to direct page.** `dcX/dcY/dcErr/dcRowYP/dcRowYN/
|
||||
dcRowXP/dcRowXN` (circle, joeyDraw.asm:1500+) and the line's per-pixel state
|
||||
(joeyDraw.asm:1253+) are 6-cyc long-absolute; DP is 5 cyc here (DL != 0 because
|
||||
D=SP+8). Set up a DP scratch page for the hot state.
|
||||
- **4.2 Remove the per-plot parity round-trip.** `sta dcSavedCol` / reload /
|
||||
`and #1` (joeyDraw.asm:1603-1610) recovers a bit `lsr a` already left in carry
|
||||
-- branch on carry instead. ~15 cyc/plot x 8 plots/step.
|
||||
- **4.3 Maintain the 4 row bases incrementally** (+/-160 per Bresenham step)
|
||||
instead of 4 LUT lookups/step.
|
||||
- Net ~1.27x (0.65x -> ~0.83x vs ST). Stop there -- the architecture won't give a
|
||||
full win on the outline. The fill-circle already beats the ST; leave it.
|
||||
|
||||
## Phase 5 -- faster clear (post-Phase-0; lower priority)
|
||||
|
||||
- **5.1 Post-init PHA-slam clear path.** `STA long,X` = 3 cyc/byte; SP-into-`$01`
|
||||
PHA-slam = 2 cyc/byte. ~1.5x (37.6 -> ~24 ms). Gate on `gModeSet` (SEI unsafe at
|
||||
init); keep the `STA long,X` path as the init-safe fallback. Chunk the SEI like
|
||||
present to avoid audio starvation. NOTE: only meaningful to measure once Phase 0
|
||||
fixes the clock.
|
||||
- **5.2 Route `surfaceMarkDirtyAll` through `iigsMarkDirtyRowsInner`** (it's a
|
||||
200-iter C loop today; surface.c:233-243) -- one source of truth with the
|
||||
fillRect marker. ~7600 cyc/clear.
|
||||
- **5.3 Hold `fillWord` in a register** across the clear loop instead of
|
||||
`lda 9,s` per iteration (joeyDraw.asm:137). Subsumed if 5.1 lands.
|
||||
|
||||
## Phase 6 -- sprite dispatch overhead
|
||||
|
||||
Body store mode (`LDA #imm16`/`STA abs,Y`) is already near-optimal -- DO NOT touch
|
||||
it. ~3000-4000 cyc/call of C dispatch wraps ~1000-1500 cyc of real blit.
|
||||
|
||||
- **6.1 Share address/shift/fnAddr across `SaveAndDraw`'s two halves** (sprite.c)
|
||||
-- it recomputes dest address, shift, fnAddr, and re-patches the self-modifying
|
||||
stub twice. (est ~1.05x SaveAndDraw.)
|
||||
- **6.2 Trim per-call C dispatch:** cache fnAddr, dodge the 2D-index MUL4, do
|
||||
`isFullyOnSurface` in 8-bit. (est ~1.10-1.15x per sprite op.)
|
||||
|
||||
## Phase 7 -- architecture (decision, no code)
|
||||
|
||||
Keep the back-buffer + dirty-row model. Document in `docs/DESIGN.md` that
|
||||
full-redraw frames inherently pay the screen cost ~2x and that sparse-update
|
||||
(sprite/HUD) games should lean on partial updates. Do NOT pursue single-buffer
|
||||
direct-to-`$E1` (refuted on slow-bank/overdraw/tearing grounds).
|
||||
|
||||
---
|
||||
|
||||
## Suggested order of execution
|
||||
|
||||
1. Phase 0 (0.1 fillRect SEI, 0.2 annotate, 0.3 present) -> re-baseline PERF.md.
|
||||
2. Phase 1 (make the demo legible) -- so every later change can be eyeballed, not
|
||||
just hashed; cheap and unblocks visual validation.
|
||||
3. Phase 2.1 (fold dirty-marking) -- highest real-world payoff, touches all primitives.
|
||||
4. Phase 3.1 (fillRect computed-jump slam).
|
||||
5. Phase 2.2, Phase 4 (circle/line DP scratch + parity).
|
||||
6. Phase 5, Phase 6 (clear PHA-slam, sprite dispatch) as time permits.
|
||||
7. Phase 7 doc.
|
||||
|
||||
Each step: re-run uber, assert hashes identical, record ops/sec delta in PERF.md.
|
||||
46
LICENSES.md
46
LICENSES.md
|
|
@ -56,44 +56,18 @@ These run on the developer's machine to compile / assemble / link
|
|||
JoeyLib for each target. They are not redistributed inside the
|
||||
produced game binaries.
|
||||
|
||||
### Golden Gate (host tools: iix, profuse, mkfs-profuse, dumpobj)
|
||||
### llvm-mos (clang / llvm-mc / link816 w65816 toolchain)
|
||||
|
||||
- **Used on:** Apple IIgs
|
||||
- **URL:** <https://golden-gate.ksherlock.com/>
|
||||
(publisher / store: <https://juiced.gs/store/golden-gate/>)
|
||||
- **License:** **Commercial** software by Kelvin Sherlock,
|
||||
distributed through Juiced.GS / Gamebits. End-user purchase
|
||||
required; redistribution **not** permitted.
|
||||
- **Role:** Cross-host harness that runs ORCA-style IIgs tools on a
|
||||
modern OS. Provides the `iix` shell, the OMF linker, `profuse`
|
||||
(ProDOS FUSE mount), `mkfs-profuse`, `dumpobj`, and the IIgs-side
|
||||
filesystem image (`$GOLDEN_GATE`) that the harness emulates.
|
||||
- **Distribution:** **Not redistributed by JoeyLib.** Each developer
|
||||
must bring their own purchased copy. `toolchains/install.sh`
|
||||
builds host tools from a developer-supplied source checkout under
|
||||
`stuff/GoldenGate/` (the `stuff/` tree is gitignored) and stages
|
||||
the IIgs filesystem from a developer-supplied `gg-linux.tgz`. The
|
||||
resulting `toolchains/iigs/gg-tools/` and `toolchains/iigs/goldengate/`
|
||||
trees are gitignored.
|
||||
|
||||
### ORCA/C compiler + ORCA libraries
|
||||
|
||||
- **Used on:** Apple IIgs
|
||||
- **URL:** <https://github.com/byteworksinc/ORCA-C>
|
||||
(commercial distribution: <https://juiced.gs/store/opus-ii/>)
|
||||
- **License:** **Commercial** product by The Byte Works, Inc.
|
||||
(Mike Westerfield). Source code is published on GitHub under a
|
||||
custom license that grants only **private use, fork, and
|
||||
modification**; redistribution in any form other than submission
|
||||
back to the upstream repo requires written permission. Commercial
|
||||
rights remain with Byte Works; the compiler is sold today as part
|
||||
of the **Opus ][** collection via Juiced.GS.
|
||||
- **Role:** ANSI C compiler for the 65816 + Apple IIgs runtime
|
||||
libraries. Run via Golden Gate's `iix` harness.
|
||||
- **Distribution:** **Not redistributed by JoeyLib.** ORCA support
|
||||
files arrive as part of the developer-supplied Golden Gate /
|
||||
Opus ][ install bundle and land inside the gitignored
|
||||
`toolchains/iigs/goldengate/` tree.
|
||||
- **URL:** <https://github.com/llvm-mos/llvm-mos>
|
||||
- **License:** Apache License 2.0 with LLVM Exceptions (the upstream
|
||||
LLVM license).
|
||||
- **Role:** clang C compiler targeting the 65816, plus llvm-mc for
|
||||
assembling hand-written `.s` files and link816 for producing OMF
|
||||
S16 executables. Driven by `toolchains/iigs/clang-build.sh`; the
|
||||
toolchain root is located via the `LLVM816_ROOT` environment
|
||||
variable.
|
||||
- **Vendored at:** `toolchains/iigs/llvm-mos/`
|
||||
|
||||
### Merlin32
|
||||
|
||||
|
|
|
|||
12
PERF.md
12
PERF.md
|
|
@ -34,3 +34,15 @@
|
|||
| jlAudioIsPlayingMod | 3536 | 9.28x | 4.33x | 26.78x |
|
||||
| surfaceMarkDirtyRect (via jlFillRect 32x32) | 240 | 1.38x | 1.26x | 58.95x |
|
||||
| c2p full screen | - | - | - | - |
|
||||
|
||||
> **IIgs full-screen ceiling (~29 ops/sec).** A full-screen op moves
|
||||
> 32000 bytes = 16000 word-stores x 6 cyc (STA long,X / PEI, both
|
||||
> 3 cyc/byte) / 2.8 MHz ~= 34 ms ~= 29 ops/sec maximum. So
|
||||
> `jlSurfaceClear`, `jlFillRect 320x200`, and `jlStagePresent full`
|
||||
> cannot legitimately exceed ~29; a higher reading means the benchmark
|
||||
> clock was perturbed. The clock is `jlFrameCount` = the VBL-interrupt-
|
||||
> driven GetTick, so an op that holds `SEI` while it runs can over-
|
||||
> report its ops/sec. Treat full-screen IIgs numbers above ~29 as
|
||||
> inflated and cross-check against the cycle model. (`jlFillRect 320x200`
|
||||
> = 60 and `jlStagePresent full` = 42 in the table above predate the
|
||||
> Phase 0 SEI removal and are inflated; expect ~25-28 after re-measure.)
|
||||
|
|
|
|||
|
|
@ -270,15 +270,10 @@ static uint16_t rgb(uint8_t r, uint8_t g, uint8_t b) {
|
|||
}
|
||||
|
||||
|
||||
/* ORCA-C's auto-segmenter splits code at function boundaries but
|
||||
* places everything from one TU into one code bank by default. This
|
||||
* single-file example easily blows the 64 KB code-bank limit, so we
|
||||
* sprinkle `segment "NAME";` directives at section boundaries to
|
||||
* force the linker to place each region in its own code segment.
|
||||
* The directive is ORCA-specific; gcc-targeted ports ignore it via
|
||||
* the platform guard. */
|
||||
/* This single-file example can blow the 65816's 64 KB code-bank
|
||||
* limit, so on the IIgs we split code across segments at section
|
||||
* boundaries to spread the load across multiple banks. */
|
||||
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||
segment "ADVART";
|
||||
#endif
|
||||
|
||||
|
||||
|
|
@ -1103,7 +1098,6 @@ static bool buildAllSprites(void) {
|
|||
|
||||
|
||||
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||
segment "ADVROOM";
|
||||
#endif
|
||||
|
||||
|
||||
|
|
@ -1603,7 +1597,6 @@ static bool findPath(RoomE room, int16_t sx, int16_t sy, int16_t tx, int16_t ty)
|
|||
|
||||
|
||||
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||
segment "ADVMOVE";
|
||||
#endif
|
||||
|
||||
|
||||
|
|
@ -1869,7 +1862,6 @@ static void drawInventoryBar(void) {
|
|||
|
||||
|
||||
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||
segment "ADVUI";
|
||||
#endif
|
||||
|
||||
|
||||
|
|
@ -1935,7 +1927,6 @@ static void composeFrame(int16_t cursorX, int16_t cursorY) {
|
|||
|
||||
|
||||
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||
segment "ADVVERB";
|
||||
#endif
|
||||
|
||||
|
||||
|
|
@ -2213,7 +2204,6 @@ static bool buildBackgrounds(void) {
|
|||
|
||||
|
||||
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||
segment "";
|
||||
#endif
|
||||
|
||||
|
||||
|
|
@ -2231,7 +2221,6 @@ int main(void) {
|
|||
* ~1.5 KB each, so we ask for 256 KB to be safe. Chunky ports
|
||||
* need ~16 KB. */
|
||||
config.codegenBytes = 256UL * 1024;
|
||||
config.maxSurfaces = 6;
|
||||
config.audioBytes = 64UL * 1024;
|
||||
|
||||
if (!jlInit(&config)) {
|
||||
|
|
|
|||
|
|
@ -235,12 +235,10 @@ static bool isTrunkChar(char c) {
|
|||
}
|
||||
|
||||
|
||||
/* ORCA-C's auto-segmenter splits at function boundaries; one TU's
|
||||
* code can still overflow the 65816's 64 KB code bank if it lands
|
||||
* in a single segment. Sprinkle ORCA-only segment directives at the
|
||||
* heavier sections to spread the load across multiple banks. */
|
||||
/* One TU's code can overflow the 65816's 64 KB code bank if it lands
|
||||
* in a single segment. Split the heavier sections across segments to
|
||||
* spread the load across multiple banks. */
|
||||
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||
segment "ADV2ART";
|
||||
#endif
|
||||
|
||||
|
||||
|
|
@ -723,7 +721,6 @@ static void programScbForView(DebugViewE view) {
|
|||
|
||||
|
||||
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||
segment "ADV2PAINT";
|
||||
#endif
|
||||
|
||||
|
||||
|
|
@ -930,7 +927,6 @@ static void drawEgoMasked(int16_t x, int16_t y, DirE dir, uint8_t frame) {
|
|||
|
||||
|
||||
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||
segment "ADV2PATH";
|
||||
#endif
|
||||
|
||||
|
||||
|
|
@ -1214,7 +1210,6 @@ static void drawInventoryBar(void) {
|
|||
|
||||
|
||||
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||
segment "ADV2COMP";
|
||||
#endif
|
||||
|
||||
|
||||
|
|
@ -1328,7 +1323,6 @@ static void handleClick(int16_t mx, int16_t my, bool leftClick, bool rightClick)
|
|||
|
||||
|
||||
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||
segment "";
|
||||
#endif
|
||||
|
||||
|
||||
|
|
@ -1346,7 +1340,6 @@ int main(void) {
|
|||
* be small. We need 4 surfaces (stage + visual + priority +
|
||||
* control). */
|
||||
config.codegenBytes = 4UL * 1024;
|
||||
config.maxSurfaces = 8;
|
||||
config.audioBytes = 0;
|
||||
|
||||
if (!jlInit(&config)) {
|
||||
|
|
|
|||
|
|
@ -307,7 +307,6 @@ static uint32_t gAgiV11SubTickAccum; // counts AGI ticks toward next
|
|||
|
||||
|
||||
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||
segment "AGIMAIN";
|
||||
#endif
|
||||
|
||||
// ----- Internal helpers (alphabetical) -----
|
||||
|
|
@ -1687,7 +1686,6 @@ static void updateEgoFromInput(void) {
|
|||
|
||||
|
||||
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||
segment "";
|
||||
#endif
|
||||
|
||||
// ----- Entry point -----
|
||||
|
|
@ -1703,7 +1701,6 @@ int main(int argc, char **argv) {
|
|||
bool vmReady;
|
||||
|
||||
cfg.codegenBytes = 0u; // no sprites needed -- backdrop-restore path is direct memcpy
|
||||
cfg.maxSurfaces = 4u; // stage + backdrop (2 used)
|
||||
// AGI sound is software-synthesized PSG squarewaves pushed
|
||||
// through the Sound Blaster auto-init DMA path (see agiSynthFill
|
||||
// and src/port/dos/audio.c). The audio pool covers the 4 KB
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@
|
|||
|
||||
|
||||
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||
segment "AGIOBJ";
|
||||
#endif
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -81,7 +81,6 @@ static void plot(AgiPicT *pic, const PicStateT *state, int16_t x, int16_t y)
|
|||
|
||||
|
||||
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||
segment "AGIPIC";
|
||||
#endif
|
||||
|
||||
// ----- Internal helpers (alphabetical) -----
|
||||
|
|
@ -122,7 +121,7 @@ static void drawCorner(AgiPicT *pic, const PicStateT *state, const uint8_t *data
|
|||
// the minor axis is linearly interpolated with round-half-away-from-
|
||||
// zero. All math stays in 16-bit -- AGI coordinates are 8-bit so the
|
||||
// worst product (dy * i = 167 * 167 = 27889) fits in int16, and the
|
||||
// 65816 build avoids ORCA-C's long-arithmetic helpers entirely.
|
||||
// 65816 build avoids long-arithmetic helpers entirely.
|
||||
static void drawLine(AgiPicT *pic, const PicStateT *state, int16_t x0, int16_t y0, int16_t x1, int16_t y1) {
|
||||
int16_t dx;
|
||||
int16_t dy;
|
||||
|
|
|
|||
|
|
@ -48,9 +48,8 @@
|
|||
|
||||
// IIgs 64 KB code-bank rule: park this TU in its own load segment
|
||||
// so it doesn't pile onto _ROOT (which still holds main() and the
|
||||
// ORCA-C startup). Cross-platform builds ignore this pragma.
|
||||
// runtime startup). Cross-platform builds ignore this pragma.
|
||||
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||
segment "AGIRES";
|
||||
#endif
|
||||
|
||||
|
||||
|
|
@ -154,7 +153,7 @@ static bool loadDirectory(AgiGameT *game, AgiResTypeE type, const char *gameDir,
|
|||
}
|
||||
|
||||
volNibble = (uint8_t)(buf[0] >> 4);
|
||||
// 20-bit offset, cast to uint32_t before shift so ORCA-C's
|
||||
// 20-bit offset, cast to uint32_t before shift so a
|
||||
// 16-bit int doesn't truncate the (& 0x0F) << 16 term.
|
||||
offset = ((uint32_t)(buf[0] & 0x0F) << 16) | ((uint32_t)buf[1] << 8) | (uint32_t)buf[2];
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@
|
|||
|
||||
|
||||
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||
segment "AGITXT";
|
||||
#endif
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@
|
|||
|
||||
|
||||
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||
segment "AGIVIEW";
|
||||
#endif
|
||||
|
||||
|
||||
|
|
@ -143,7 +142,6 @@ uint8_t agiActorPriorityForY(int16_t y) {
|
|||
|
||||
|
||||
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||
segment "AGIDRAW";
|
||||
#endif
|
||||
|
||||
void agiViewDraw(const AgiViewT *view, uint8_t loopIdx, uint8_t celIdx,
|
||||
|
|
|
|||
|
|
@ -273,7 +273,6 @@
|
|||
|
||||
|
||||
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||
segment "AGIVM";
|
||||
#endif
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -118,7 +118,6 @@ int main(void) {
|
|||
uint32_t sfxLen;
|
||||
|
||||
config.codegenBytes = 8 * 1024;
|
||||
config.maxSurfaces = 4;
|
||||
config.audioBytes = 64UL * 1024;
|
||||
|
||||
if (!jlInit(&config)) {
|
||||
|
|
|
|||
|
|
@ -243,7 +243,6 @@ int main(void) {
|
|||
jlSurfaceT *screen;
|
||||
|
||||
config.codegenBytes = 8 * 1024;
|
||||
config.maxSurfaces = 4;
|
||||
config.audioBytes = 64UL * 1024;
|
||||
|
||||
if (!jlInit(&config)) {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ int main(void) {
|
|||
jlConfigT config;
|
||||
|
||||
config.codegenBytes = 8 * 1024;
|
||||
config.maxSurfaces = 4;
|
||||
config.audioBytes = 64UL * 1024;
|
||||
|
||||
if (!jlInit(&config)) {
|
||||
|
|
|
|||
|
|
@ -218,7 +218,6 @@ int main(void) {
|
|||
jlSurfaceT *screen;
|
||||
|
||||
config.codegenBytes = 8 * 1024;
|
||||
config.maxSurfaces = 4;
|
||||
config.audioBytes = 64UL * 1024;
|
||||
|
||||
if (!jlInit(&config)) {
|
||||
|
|
|
|||
|
|
@ -218,7 +218,6 @@ int main(void) {
|
|||
int16_t cursorRow;
|
||||
|
||||
config.codegenBytes = 8 * 1024;
|
||||
config.maxSurfaces = 4;
|
||||
config.audioBytes = 64UL * 1024;
|
||||
|
||||
if (!jlInit(&config)) {
|
||||
|
|
|
|||
|
|
@ -106,7 +106,6 @@ int main(void) {
|
|||
jlSurfaceT *screen;
|
||||
|
||||
config.codegenBytes = 8 * 1024;
|
||||
config.maxSurfaces = 4;
|
||||
config.audioBytes = 64UL * 1024;
|
||||
|
||||
if (!jlInit(&config)) {
|
||||
|
|
|
|||
|
|
@ -111,7 +111,6 @@ int main(void) {
|
|||
// halSpriteSavePlanes / halSpriteRestorePlanes as static lib code.
|
||||
// 32 KB is plenty for whatever other codegen needs are around.
|
||||
config.codegenBytes = 32UL * 1024;
|
||||
config.maxSurfaces = 4; // stage + work
|
||||
config.audioBytes = 32UL * 1024; // music + SFX
|
||||
|
||||
if (!jlInit(&config)) {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@
|
|||
|
||||
#include "spacetaxi.h"
|
||||
|
||||
JOEYLIB_SEGMENT("STAXI")
|
||||
|
||||
#define ST_SFX_VOICE 2u
|
||||
#define ST_THRUST_VOICE 1u
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@
|
|||
|
||||
#include "spacetaxi.h"
|
||||
|
||||
JOEYLIB_SEGMENT("STAXI")
|
||||
|
||||
// Velocity safety clamp. With ST_SUBPIXEL = 256, ST_MAX_VX = 512 caps
|
||||
// the cab at ~2 px/frame which is in line with the C64 max effective
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@
|
|||
|
||||
#include "spacetaxi.h"
|
||||
|
||||
JOEYLIB_SEGMENT("STAXI")
|
||||
|
||||
|
||||
void stHudDraw(jlSurfaceT *stage, const StGameT *game) {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
|
||||
#include "spacetaxi.h"
|
||||
|
||||
JOEYLIB_SEGMENT("STAXI")
|
||||
|
||||
// STL2 adds the per-level physics templates (xAccel/yAccel/xGrav/yGrav)
|
||||
// and the full VIC color block ($7D00-$7D08). Drops the patience byte
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@
|
|||
|
||||
#include "spacetaxi.h"
|
||||
|
||||
JOEYLIB_SEGMENT("STAXI")
|
||||
|
||||
#define ST_PASSENGER_W_PX 16
|
||||
#define ST_PASSENGER_H_PX 16
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@
|
|||
|
||||
// All STAXI example sources share the STAXI load segment so the
|
||||
// IIgs binary's _ROOT bank stays under 64 KB. No-op on other ports.
|
||||
JOEYLIB_SEGMENT("STAXI")
|
||||
|
||||
// Tile bank files are per-level: each level's .dat references a
|
||||
// numeric bank id, and the host loads tiles/tbankN.tbk on demand
|
||||
|
|
|
|||
|
|
@ -107,7 +107,6 @@ int main(void) {
|
|||
* the chunky two-shift case asks for. 32 KB fits a 16x16 ball
|
||||
* with all variants. */
|
||||
config.codegenBytes = 32UL * 1024;
|
||||
config.maxSurfaces = 4;
|
||||
config.audioBytes = 64UL * 1024;
|
||||
|
||||
if (!jlInit(&config)) {
|
||||
|
|
|
|||
|
|
@ -217,6 +217,195 @@ static void buildBallSprite(void) {
|
|||
}
|
||||
|
||||
|
||||
// ----- Visual showcase -----
|
||||
//
|
||||
// Before the (visually meaningless) timed benchmark, draw one example
|
||||
// of each primitive into its own grid cell so a viewer can actually SEE
|
||||
// what the library renders instead of a single solid color. The
|
||||
// benchmark that follows never presents, so this showcase stays on
|
||||
// screen for the whole timed run; results go to joeylog.txt. A legend
|
||||
// mapping cell index -> primitive is also logged.
|
||||
|
||||
#define SC_SCREEN_W 320
|
||||
#define SC_SCREEN_H 200
|
||||
#define SC_PAL_COUNT 16
|
||||
#define SC_PAL_SWATCH_W (SC_SCREEN_W / SC_PAL_COUNT)
|
||||
#define SC_PAL_STRIP_H 12
|
||||
#define SC_GRID_TOP 16
|
||||
#define SC_COLS 4
|
||||
#define SC_ROWS 3
|
||||
#define SC_CELLS (SC_COLS * SC_ROWS)
|
||||
#define SC_GUTTER 4
|
||||
#define SC_INSET 5
|
||||
#define SC_BG_COLOR 10
|
||||
#define SC_BORDER_COLOR 1
|
||||
#define SC_HOLD_FRAMES 210
|
||||
|
||||
|
||||
static const uint16_t gShowcasePal[SC_PAL_COUNT] = {
|
||||
0x000, 0xFFF, 0xF00, 0x0F0, 0x00F, 0xFF0, 0x0FF, 0xF0F,
|
||||
0xF80, 0xAAA, 0x555, 0x8AF, 0x8F8, 0xF8C, 0x840, 0xACE
|
||||
};
|
||||
|
||||
|
||||
static void showcaseCellRect(uint16_t index, int16_t *outX, int16_t *outY, int16_t *outW, int16_t *outH) {
|
||||
uint16_t col;
|
||||
uint16_t row;
|
||||
int16_t cellW;
|
||||
int16_t cellH;
|
||||
|
||||
col = (uint16_t)(index % SC_COLS);
|
||||
row = (uint16_t)(index / SC_COLS);
|
||||
cellW = (int16_t)((SC_SCREEN_W - (SC_COLS + 1) * SC_GUTTER) / SC_COLS);
|
||||
cellH = (int16_t)((SC_SCREEN_H - SC_GRID_TOP - (SC_ROWS + 1) * SC_GUTTER) / SC_ROWS);
|
||||
*outX = (int16_t)(SC_GUTTER + (int16_t)col * (cellW + SC_GUTTER));
|
||||
*outY = (int16_t)(SC_GRID_TOP + SC_GUTTER + (int16_t)row * (cellH + SC_GUTTER));
|
||||
*outW = cellW;
|
||||
*outH = cellH;
|
||||
}
|
||||
|
||||
|
||||
static void drawShowcase(void) {
|
||||
uint16_t cell;
|
||||
int16_t x;
|
||||
int16_t y;
|
||||
int16_t w;
|
||||
int16_t h;
|
||||
int16_t ix;
|
||||
int16_t iy;
|
||||
int16_t iw;
|
||||
int16_t ih;
|
||||
int16_t cx;
|
||||
int16_t cy;
|
||||
int16_t r;
|
||||
int16_t k;
|
||||
int16_t px;
|
||||
int16_t py;
|
||||
int16_t minDim;
|
||||
uint8_t bx;
|
||||
uint8_t by;
|
||||
uint8_t bxStart;
|
||||
uint8_t bxEnd;
|
||||
uint8_t byStart;
|
||||
uint8_t byEnd;
|
||||
uint16_t held;
|
||||
|
||||
jlPaletteSet(gStage, 0, gShowcasePal);
|
||||
jlScbSetRange(gStage, 0, 199, 0);
|
||||
jlSurfaceClear(gStage, SC_BG_COLOR);
|
||||
|
||||
// Top strip: all 16 palette entries as swatches.
|
||||
for (k = 0; k < SC_PAL_COUNT; k++) {
|
||||
jlFillRect(gStage, (int16_t)(k * SC_PAL_SWATCH_W), 0, SC_PAL_SWATCH_W, SC_PAL_STRIP_H, (uint8_t)k);
|
||||
}
|
||||
|
||||
for (cell = 0; cell < SC_CELLS; cell++) {
|
||||
showcaseCellRect(cell, &x, &y, &w, &h);
|
||||
jlDrawRect(gStage, x, y, (uint16_t)w, (uint16_t)h, SC_BORDER_COLOR);
|
||||
ix = (int16_t)(x + SC_INSET);
|
||||
iy = (int16_t)(y + SC_INSET);
|
||||
iw = (int16_t)(w - 2 * SC_INSET);
|
||||
ih = (int16_t)(h - 2 * SC_INSET);
|
||||
cx = (int16_t)(ix + iw / 2);
|
||||
cy = (int16_t)(iy + ih / 2);
|
||||
minDim = (iw < ih) ? iw : ih;
|
||||
|
||||
switch (cell) {
|
||||
case 0:
|
||||
// Pixels: a scatter of single plotted pixels.
|
||||
for (py = iy; py < iy + ih; py += 3) {
|
||||
for (px = ix; px < ix + iw; px += 3) {
|
||||
jlDrawPixel(gStage, px, py, (uint8_t)(2 + ((px + py) % 14)));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
// Horizontal lines.
|
||||
for (k = 0; k < ih; k += 4) {
|
||||
jlDrawLine(gStage, ix, (int16_t)(iy + k), (int16_t)(ix + iw - 1), (int16_t)(iy + k), (uint8_t)(2 + (k / 4) % 14));
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
// Vertical lines.
|
||||
for (k = 0; k < iw; k += 4) {
|
||||
jlDrawLine(gStage, (int16_t)(ix + k), iy, (int16_t)(ix + k), (int16_t)(iy + ih - 1), (uint8_t)(2 + (k / 4) % 14));
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
// Diagonals: an X plus a fan from the center.
|
||||
jlDrawLine(gStage, ix, iy, (int16_t)(ix + iw - 1), (int16_t)(iy + ih - 1), 5);
|
||||
jlDrawLine(gStage, ix, (int16_t)(iy + ih - 1), (int16_t)(ix + iw - 1), iy, 6);
|
||||
for (k = 0; k < iw; k += 8) {
|
||||
jlDrawLine(gStage, cx, cy, (int16_t)(ix + k), iy, (uint8_t)(8 + (k / 8) % 8));
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
// Rectangle outlines, concentric.
|
||||
for (k = 0; 2 * k < minDim - 4; k += 5) {
|
||||
jlDrawRect(gStage, (int16_t)(ix + k), (int16_t)(iy + k), (uint16_t)(iw - 2 * k), (uint16_t)(ih - 2 * k), (uint8_t)(2 + (k / 5) % 14));
|
||||
}
|
||||
break;
|
||||
case 5:
|
||||
// Filled rectangles, overlapping.
|
||||
jlFillRect(gStage, ix, iy, (uint16_t)(iw * 2 / 3), (uint16_t)(ih * 2 / 3), 2);
|
||||
jlFillRect(gStage, (int16_t)(ix + iw / 3), (int16_t)(iy + ih / 3), (uint16_t)(iw * 2 / 3), (uint16_t)(ih * 2 / 3), 4);
|
||||
break;
|
||||
case 6:
|
||||
// Circle outlines, concentric.
|
||||
for (r = (int16_t)(minDim / 2); r > 2; r -= 4) {
|
||||
jlDrawCircle(gStage, cx, cy, (uint16_t)r, (uint8_t)(2 + (r / 4) % 14));
|
||||
}
|
||||
break;
|
||||
case 7:
|
||||
// Filled circles.
|
||||
jlFillCircle(gStage, cx, cy, (uint16_t)(minDim / 2 - 1), 8);
|
||||
jlFillCircle(gStage, cx, cy, (uint16_t)(minDim / 4), 5);
|
||||
break;
|
||||
case 8:
|
||||
// Tiles: an 8x8-block checkerboard inside the cell.
|
||||
bxStart = (uint8_t)((ix + 7) / 8);
|
||||
bxEnd = (uint8_t)((ix + iw) / 8);
|
||||
byStart = (uint8_t)((iy + 7) / 8);
|
||||
byEnd = (uint8_t)((iy + ih) / 8);
|
||||
for (by = byStart; by < byEnd; by++) {
|
||||
for (bx = bxStart; bx < bxEnd; bx++) {
|
||||
jlTileFill(gStage, bx, by, (uint8_t)(((bx + by) & 1) ? 6 : 8));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 9:
|
||||
// Sprite: the compiled ball at a few positions.
|
||||
jlSpriteDraw(gStage, gSprite, ix, iy);
|
||||
jlSpriteDraw(gStage, gSprite, (int16_t)(ix + iw - 16), (int16_t)(iy + ih - 16));
|
||||
jlSpriteDraw(gStage, gSprite, (int16_t)(cx - 8), (int16_t)(cy - 8));
|
||||
break;
|
||||
case 10:
|
||||
// Flood fill: outline a circle, then flood its interior.
|
||||
jlDrawCircle(gStage, cx, cy, (uint16_t)(minDim / 2 - 2), 1);
|
||||
jlFloodFill(gStage, cx, cy, 12);
|
||||
break;
|
||||
default:
|
||||
// Mini scene: ground, sun, horizon, ball.
|
||||
jlFillRect(gStage, ix, (int16_t)(iy + ih / 2), (uint16_t)iw, (uint16_t)(ih - ih / 2), 4);
|
||||
jlFillCircle(gStage, cx, (int16_t)(iy + ih / 3), (uint16_t)(ih / 4), 5);
|
||||
jlDrawLine(gStage, ix, (int16_t)(iy + ih / 2), (int16_t)(ix + iw - 1), (int16_t)(iy + ih / 2), 1);
|
||||
jlSpriteDraw(gStage, gSprite, (int16_t)(cx - 8), (int16_t)(iy + ih / 2 - 16));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
jlLogF("UBER: showcase cells: 0=pixels 1=lineH 2=lineV 3=diag 4=rect 5=fillRect 6=circle 7=fillCircle 8=tiles 9=sprite 10=flood 11=scene\n");
|
||||
jlStagePresent();
|
||||
|
||||
// Hold the showcase on screen, then auto-advance to the benchmark so
|
||||
// the headless perf-capture run still completes without a keypress.
|
||||
held = jlFrameCount();
|
||||
while ((uint16_t)(jlFrameCount() - held) < SC_HOLD_FRAMES) {
|
||||
/* hold */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ----- Main -----
|
||||
|
||||
static void runAllTests(void) {
|
||||
|
|
@ -296,9 +485,8 @@ int main(void) {
|
|||
|
||||
/* 32 KB fits the 8 pre-shifted DRAW variants the Amiga planar
|
||||
* compiled sprite emitter generates. UL on the multiply because
|
||||
* ORCA-C's 16-bit int overflows on 32 * 1024. */
|
||||
* a 16-bit int overflows on 32 * 1024. */
|
||||
config.codegenBytes = 32UL * 1024;
|
||||
config.maxSurfaces = 4;
|
||||
config.audioBytes = 64UL * 1024;
|
||||
|
||||
if (!jlInit(&config)) {
|
||||
|
|
@ -373,10 +561,11 @@ int main(void) {
|
|||
jlLogF("UBER: audioInit failed (skipping audio)\n");
|
||||
}
|
||||
|
||||
// Reset stage + run all per-frame timed tests.
|
||||
jlSurfaceClear(gStage, 0);
|
||||
jlFillRect(gStage, 0, 0, 320, 8, 1);
|
||||
jlStagePresent();
|
||||
// Visual showcase: render one of each primitive into its own grid
|
||||
// cell so the run shows something legible (the timed benchmark below
|
||||
// never presents, so this stays on screen throughout it). The first
|
||||
// timed op is jlSurfaceClear, so this leaves the benchmark untouched.
|
||||
drawShowcase();
|
||||
|
||||
runAllTests();
|
||||
|
||||
|
|
@ -391,6 +580,10 @@ int main(void) {
|
|||
jlStagePresent();
|
||||
|
||||
jlLogF("UBER: press any key to exit\n");
|
||||
// Flush the log to disk BEFORE the blocking key wait, so an automated
|
||||
// (headless) run that kills the process at this prompt still captures
|
||||
// the results -- joeyLog otherwise only flushes at the atexit fclose.
|
||||
jlLogFlush();
|
||||
jlWaitForAnyKey();
|
||||
|
||||
jlSpriteDestroy(gSprite);
|
||||
|
|
|
|||
|
|
@ -24,6 +24,11 @@
|
|||
|
||||
#define JOEY_AUDIO_SFX_SLOTS 4
|
||||
|
||||
// Number of PSG-style polyphonic tone voices (jlAudioVoice selects
|
||||
// voice 0/1/2). The core gate range-checks against this so each
|
||||
// per-port HAL can assume voice < JOEY_AUDIO_VOICES.
|
||||
#define JOEY_AUDIO_VOICES 3
|
||||
|
||||
// Initialize audio. Returns true if the platform has a working audio
|
||||
// engine and was able to start it. Returns false silently otherwise;
|
||||
// the rest of the API stays callable but produces no sound.
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@
|
|||
|
||||
typedef struct {
|
||||
uint32_t codegenBytes; // runtime compiled-sprite cache size
|
||||
uint16_t maxSurfaces; // maximum concurrent surfaces
|
||||
uint32_t audioBytes; // audio sample and module RAM pool
|
||||
uint32_t audioBytes; // reserved; not yet consulted by any audio engine
|
||||
} jlConfigT;
|
||||
|
||||
// Initialize the library. Returns true on success.
|
||||
|
|
@ -35,11 +34,10 @@ const char *jlVersionString(void);
|
|||
// always a hardware-level wait, not a software timer.
|
||||
void jlWaitVBL(void);
|
||||
|
||||
// Monotonic 16-bit frame counter. Polled by callers; ports detect
|
||||
// the rising edge inside this call (IIgs $C019, DOS $3DA, Amiga
|
||||
// VPOSR) or expose a counter maintained by a VBL ISR (Atari ST).
|
||||
// Caller must poll faster than 2 * jlFrameHz() so no edge is
|
||||
// missed. Used by benchmarks and frame-rate-independent animation.
|
||||
// Monotonic 16-bit frame counter. Caller must poll faster than
|
||||
// 2 * jlFrameHz() so no edge is missed. Used by benchmarks and
|
||||
// frame-rate-independent animation. (Per-port edge-detect / VBL-ISR
|
||||
// mechanism is documented at halFrameCount in src/core/hal.h.)
|
||||
uint16_t jlFrameCount(void);
|
||||
|
||||
// Nominal display frame rate in Hz: 50 (Amiga PAL), 60 (IIgs / ST
|
||||
|
|
@ -50,17 +48,14 @@ uint16_t jlFrameHz(void);
|
|||
|
||||
// Monotonic wall-clock millisecond counter since jlInit. Decoupled
|
||||
// from frame rate -- two consecutive calls separated by a long
|
||||
// render will report the true elapsed time, not the number of VBLs
|
||||
// that fired. Per-port implementation: DOS uses uclock() (PIT
|
||||
// counter 0, ~1 us resolution); ST reads the 200 Hz Timer C
|
||||
// counter at _hz_200; Amiga uses ReadEClock (~1.4 us PAL); IIgs
|
||||
// falls back to frame_count * 1000 / 60 since ORCA-C time() bursts
|
||||
// the stdio cluster bank.
|
||||
// render report the true elapsed time, not the number of VBLs that
|
||||
// fired. (Per-port millisecond timer sources are documented at
|
||||
// halMillisElapsed in src/core/hal.h.)
|
||||
uint32_t jlMillisElapsed(void);
|
||||
|
||||
// Deterministic, seedable pseudo-random generator. Bit-identical
|
||||
// output across every JoeyLib port for a given seed, since stdlib
|
||||
// rand() varies wildly between DJGPP, ORCA-C, libnix, and mintlib.
|
||||
// rand() varies wildly between platforms and C libraries.
|
||||
// The underlying algorithm is xorshift32 (Marsaglia): period 2^32-1,
|
||||
// no multiply, suitable for games but not for cryptography.
|
||||
uint32_t jlRandom(void);
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
#define JOEYLIB_PLATFORM_AMIGA
|
||||
#elif defined(__atarist__) || defined(__MINT__) || defined(__TOS__)
|
||||
#define JOEYLIB_PLATFORM_ATARIST
|
||||
#elif defined(__ORCAC__) || defined(__APPLE2GS__) || defined(__GNO__)
|
||||
#elif defined(__APPLE2GS__) || defined(__GNO__)
|
||||
#define JOEYLIB_PLATFORM_IIGS
|
||||
#else
|
||||
#error "JoeyLib: unknown platform; define JOEYLIB_PLATFORM_<TARGET> explicitly via -D"
|
||||
|
|
@ -63,20 +63,6 @@
|
|||
#define JOEYLIB_PLATFORM_NAME "MS-DOS"
|
||||
#endif
|
||||
|
||||
// ----- ORCA-C named load segments -----
|
||||
//
|
||||
// On the IIgs the ORCA Linker fits each load segment in its own bank,
|
||||
// so spilling cross-platform .c files into named segments is the way
|
||||
// to keep monolithic IIgs binaries under the 64 KB-per-bank _ROOT
|
||||
// limit. The `segment "name";` statement is ORCA-C-specific syntax
|
||||
// (see ORCA/C ch. 30); other ports' compilers don't recognize it, so
|
||||
// the macro evaluates to nothing on Amiga/ST/DOS.
|
||||
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||
#define JOEYLIB_SEGMENT(name) segment name;
|
||||
#else
|
||||
#define JOEYLIB_SEGMENT(name)
|
||||
#endif
|
||||
|
||||
// ----- Library version -----
|
||||
|
||||
#define JOEYLIB_VERSION_MAJOR 1
|
||||
|
|
|
|||
|
|
@ -15,10 +15,11 @@
|
|||
#include "types.h"
|
||||
|
||||
// Flip the dirty regions of the stage to the display, then clear the
|
||||
// dirty state. Cheap when nothing has changed since the last call
|
||||
// (gStageAnyDirty short-circuit). Drawing primitives mark dirty as
|
||||
// a side effect, so callers only need to call jlStagePresent at the
|
||||
// end of a frame -- everything they drew shows up.
|
||||
// dirty state. Drawing primitives mark dirty as a side effect, so
|
||||
// callers only need to call jlStagePresent at the end of a frame --
|
||||
// everything they drew shows up. Rows that nothing touched since the
|
||||
// last present are skipped by the per-port present using the per-row
|
||||
// dirty bands.
|
||||
//
|
||||
// To present a region you didn't draw with the standard primitives
|
||||
// (e.g. direct framebuffer poking), call surfaceMarkDirtyRect on
|
||||
|
|
|
|||
|
|
@ -112,9 +112,15 @@ void jlSpriteDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y);
|
|||
// Capture the destination region a subsequent jlSpriteDraw at the same
|
||||
// (x,y) would write to. backup->bytes must have at least
|
||||
// (widthTiles*4) * (heightTiles*8) bytes of capacity for fully
|
||||
// in-bounds draws; for clipped draws only the visible bytes are
|
||||
// stored. The captured region's exact size is reported in
|
||||
// backup->sizeBytes.
|
||||
// in-bounds draws on chunky ports (DOS, IIgs); for clipped draws only
|
||||
// the visible bytes are stored. The captured region's exact size is
|
||||
// reported in backup->sizeBytes.
|
||||
//
|
||||
// Planar ports (Amiga, Atari ST) round x DOWN to an 8-pixel plane
|
||||
// boundary and width UP, so a sprite drawn at an odd-of-8 x captures
|
||||
// one extra plane-byte column per row. Size backup->bytes for the
|
||||
// planar worst case ((widthTiles*4) + 1) * (heightTiles*8) bytes so an
|
||||
// odd-x save cannot overrun the buffer.
|
||||
void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup);
|
||||
|
||||
// Repaint the destination region from a jlSpriteBackupT captured by a
|
||||
|
|
@ -126,7 +132,7 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup);
|
|||
// captures the destination bytes about to be overwritten, then draws
|
||||
// the sprite. Both ops share validation, the destination ptr is
|
||||
// computed once, and a single dirty-rect mark covers both. Saves
|
||||
// roughly one full dispatcher chain (~150 cyc on IIgs ORCA-C) per
|
||||
// roughly one full dispatcher chain (~150 cyc on IIgs) per
|
||||
// frame versus calling jlSpriteSaveUnder + jlSpriteDraw separately.
|
||||
//
|
||||
// Identical semantics to:
|
||||
|
|
|
|||
|
|
@ -6,10 +6,7 @@
|
|||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef enum {
|
||||
VIDEO_REGION_NTSC,
|
||||
VIDEO_REGION_PAL,
|
||||
VIDEO_REGION_VGA
|
||||
} jlVideoRegionE;
|
||||
// This header is the shared include point for stdint / stdbool across
|
||||
// every public JoeyLib header (pulled in transitively via joey.h).
|
||||
|
||||
#endif
|
||||
|
|
|
|||
278
make/iigs.mk
278
make/iigs.mk
|
|
@ -1,262 +1,134 @@
|
|||
# Apple IIgs build rules.
|
||||
# Apple IIgs build rules (llvm-mos w65816: clang + llvm-mc + link816).
|
||||
#
|
||||
# Uses GoldenGate's iix to drive ORCA/C 2.1.0 + ORCA Linker. The
|
||||
# toolchains/iigs/iix-build.sh wrapper handles ORCA's case-sensitivity
|
||||
# quirks (lowercase .a/.root/.sym vs linker demands for uppercase),
|
||||
# multi-source compile/link, and pragma injection for include paths
|
||||
# and the stdint/stdbool shim headers.
|
||||
# toolchains/iigs/clang-build.sh drives compile (clang) / assemble
|
||||
# (llvm-mc) / link (link816) against the prebuilt w65816 runtime.
|
||||
# Hand-rolled asm lives in src/port/iigs/*.s (llvm-mc GAS syntax);
|
||||
# bank placement is handled by -ffunction-sections + link816
|
||||
# dead-stripping.
|
||||
#
|
||||
# STATUS: the launchable-binary pipeline (multi-segment OMF emission +
|
||||
# disk packaging + GS/OS launch) is still being brought up. `iigs-lib`
|
||||
# compile-checks every library + example translation unit, and
|
||||
# `iigs-clang-smoke` validates the full clang -> llvm-mc -> link816 ->
|
||||
# MAME path end to end. Per-example binary targets build via
|
||||
# clang-build.sh; large examples need the multi-segment OMF wiring still
|
||||
# in progress.
|
||||
|
||||
include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk
|
||||
|
||||
PLATFORM := iigs
|
||||
BUILD := $(REPO_DIR)/build/$(PLATFORM)
|
||||
BINDIR := $(BUILD)/bin
|
||||
OBJDIR := $(BUILD)/obj
|
||||
|
||||
PORT_C_SRCS_ALL := $(wildcard $(SRC_PORT)/iigs/*.c)
|
||||
# Hand-rolled .asm sources go through ORCA's macro assembler via
|
||||
# iix-build.sh's `assemble` dispatch. Each .asm declares its target
|
||||
# load segment in the START operand (e.g. peislam.asm -> PEISLAMS)
|
||||
# so the linker places its bytes in a separate bank from _ROOT.
|
||||
# See ORCA/M for IIgs ch. 6 "Load Segments" for the mechanism.
|
||||
PORT_ASM_SRCS_ALL := $(wildcard $(SRC_PORT)/iigs/*.asm)
|
||||
CLANG := $(LLVM816_ROOT)/tools/llvm-mos-build/bin/clang
|
||||
RT_INC := $(LLVM816_ROOT)/runtime/include
|
||||
CFLAGS := --target=w65816 -O2 -ffreestanding -ffunction-sections
|
||||
INCLUDES := -I$(RT_INC) -I$(INCLUDE_DIR) -I$(INCLUDE_DIR)/joey -I$(SRC_CORE) -I$(REPO_DIR)/src/codegen
|
||||
|
||||
# audio_full.c declares its functions in the AUDIOIMPL load segment
|
||||
# (`segment "AUDIOIMPL"` at file scope, see ORCA/C ch. 30) so the
|
||||
# implementation code lives in its own bank, not _ROOT. That lets
|
||||
# the same source link into every binary, replacing the earlier
|
||||
# audio.c-stub vs audio_full.c-real split. The 34 KB NTP replayer
|
||||
# bytes still ride along via the xxd-baked header.
|
||||
PORT_C_SRCS := $(PORT_C_SRCS_ALL)
|
||||
PORT_C_SRCS := $(wildcard $(SRC_PORT)/iigs/*.c)
|
||||
# Hand-rolled asm, migrated to llvm-mc GAS .s.
|
||||
PORT_ASM_SRCS := $(wildcard $(SRC_PORT)/iigs/*.s)
|
||||
|
||||
# IIgs uses NTPstreamsound for SFX, not the libxmp+overlay combo that
|
||||
# DOS and ST share, so src/core/audioSfxMix.c is unused here. Filter
|
||||
# it out so the ORCA Linker doesn't pull its dead code into every
|
||||
# IIgs binary (the monolithic-link budget is tight).
|
||||
# DOS and ST share, so src/core/audioSfxMix.c is unused here.
|
||||
CORE_C_SRCS_IIGS := $(filter-out %/audioSfxMix.c, $(CORE_C_SRCS))
|
||||
|
||||
# Sprite codegen: 65816 emitter + cross-platform compile dispatch.
|
||||
CODEGEN_SRCS := $(REPO_DIR)/src/codegen/spriteEmitIigs.c \
|
||||
$(REPO_DIR)/src/codegen/spriteCompile.c
|
||||
|
||||
# NinjaTrackerPlus replayer. Assembled with Merlin32 from the staged
|
||||
# source at toolchains/iigs/ntp/ninjatrackerplus.s. Output is a 34 KB
|
||||
# raw 65816 binary that the IIgs audio HAL loads at runtime via
|
||||
# Memory Manager into a fixed-bank handle. NTP source is bank-internal
|
||||
# (no JSL/JML cross-bank jumps) so the player works at any bank-start
|
||||
# load address even though it was assembled with `org $0F0000`.
|
||||
NTP_SRC := $(REPO_DIR)/toolchains/iigs/ntp/ninjatrackerplus.s
|
||||
NTP_BIN := $(BUILD)/audio/ntpplayer.bin
|
||||
NTP_ASM := $(BUILD)/audio/ntpdata.asm
|
||||
IIGS_MERLIN := $(REPO_DIR)/toolchains/iigs/merlin32/bin/merlin32
|
||||
LIB_C_SRCS := $(CORE_C_SRCS_IIGS) $(PORT_C_SRCS) $(CODEGEN_SRCS)
|
||||
LIB_SRCS := $(LIB_C_SRCS) $(PORT_ASM_SRCS)
|
||||
|
||||
LIB_SRCS := $(CORE_C_SRCS_IIGS) $(PORT_C_SRCS) $(PORT_ASM_SRCS_ALL) $(NTP_ASM) $(CODEGEN_SRCS)
|
||||
|
||||
# HELLO is omitted from the disk because UBER exercises everything it
|
||||
# does and the disk was tight. PATTERN is included as the SCB / palette
|
||||
# golden-reference for cross-port debugging.
|
||||
# Example sources (single-TU unless noted).
|
||||
PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c
|
||||
PATTERN_BIN := $(BINDIR)/PATTERN
|
||||
DRAW_SRC := $(EXAMPLES)/draw/draw.c
|
||||
DRAW_BIN := $(BINDIR)/DRAW
|
||||
KEYS_SRC := $(EXAMPLES)/keys/keys.c
|
||||
KEYS_BIN := $(BINDIR)/KEYS
|
||||
JOY_SRC := $(EXAMPLES)/joy/joy.c
|
||||
JOY_BIN := $(BINDIR)/JOY
|
||||
SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c
|
||||
SPRITE_BIN := $(BINDIR)/SPRITE
|
||||
UBER_SRC := $(EXAMPLES)/uber/uber.c
|
||||
UBER_BIN := $(BINDIR)/UBER
|
||||
ADV_SRC := $(EXAMPLES)/adventure/adventure.c
|
||||
ADV_BIN := $(BINDIR)/ADV
|
||||
ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c
|
||||
ADV2_BIN := $(BINDIR)/ADV2
|
||||
AGI_SRCS := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c
|
||||
AGI_BIN := $(BINDIR)/AGI
|
||||
STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stEngine.c $(EXAMPLES)/spacetaxi/stPassenger.c $(EXAMPLES)/spacetaxi/stHud.c $(EXAMPLES)/spacetaxi/stAudio.c
|
||||
# IIgs convention follows the others: app dir under bin/ with a
|
||||
# DATA/ subdir for runtime assets. The disk packager will need to
|
||||
# carry both into the 2mg image (TODO).
|
||||
STAXI_INSTALL_DIR := $(BINDIR)/STAXI
|
||||
STAXI_BIN := $(STAXI_INSTALL_DIR)/STAXI
|
||||
AUDIO_SRC := $(EXAMPLES)/audio/audio.c
|
||||
AUDIO_BIN := $(BINDIR)/AUDIO
|
||||
AUDIO_MOD := $(REPO_DIR)/assets/test.mod
|
||||
AUDIO_SFX := $(REPO_DIR)/assets/test.sfx
|
||||
AUDIO_NTP := $(BUILD)/audio/test.ntp
|
||||
AUDIO_HEADER := $(BUILD)/audio/test_assets.h
|
||||
JOEYMOD := $(REPO_DIR)/tools/joeymod/joeymod
|
||||
DISK_IMG := $(BINDIR)/joey.2mg
|
||||
|
||||
IIGS_PACKAGE := $(REPO_DIR)/toolchains/iigs/package-disk.sh
|
||||
.PHONY: all iigs iigs-lib iigs-clang-smoke clean-iigs
|
||||
|
||||
IIX_INCLUDES := \
|
||||
-I $(IIGS_INCLUDE_SHIM) \
|
||||
-I $(INCLUDE_DIR) \
|
||||
-I $(INCLUDE_DIR)/joey \
|
||||
-I $(SRC_CORE) \
|
||||
-I $(REPO_DIR)/src/codegen
|
||||
# Default: compile-check the library + run the end-to-end smoke test.
|
||||
all iigs: iigs-lib iigs-clang-smoke
|
||||
|
||||
.PHONY: all iigs iigs-disk clean-iigs
|
||||
# Building the disk implicitly builds every binary it depends on, so
|
||||
# `make iigs` ends with a fresh joey.2mg on every change. Without this,
|
||||
# stale disk images would silently mask binary updates -- a surprise
|
||||
# when the run script always boots from joey.2mg.
|
||||
all iigs: $(DISK_IMG)
|
||||
# Compile-check every library and example translation unit with clang,
|
||||
# and assemble every .s with llvm-mc. This is the canonical "does the
|
||||
# IIgs source still build" gate while the launchable pipeline is wired.
|
||||
iigs-lib:
|
||||
@mkdir -p $(OBJDIR)
|
||||
@set -e; \
|
||||
for c in $(LIB_C_SRCS); do \
|
||||
echo " CC $$c"; \
|
||||
$(CLANG) $(CFLAGS) $(INCLUDES) -c $$c -o $(OBJDIR)/$$(echo $$c | sed 's#[/.]#_#g').o; \
|
||||
done; \
|
||||
for s in $(PORT_ASM_SRCS); do \
|
||||
echo " AS $$s"; \
|
||||
$(LLVM816_ROOT)/tools/llvm-mos-build/bin/llvm-mc -arch=w65816 -filetype=obj $$s -o $(OBJDIR)/$$(basename $$s .s).o; \
|
||||
done
|
||||
|
||||
$(NTP_BIN): $(NTP_SRC) $(IIGS_MERLIN)
|
||||
@mkdir -p $(dir $@)
|
||||
@cp $(NTP_SRC) $(BUILD)/audio/ninjatrackerplus.s
|
||||
cd $(BUILD)/audio && $(IIGS_MERLIN) . ninjatrackerplus.s
|
||||
mv $(BUILD)/audio/ntpplayer $@
|
||||
# End-to-end smoke test: clang -> llvm-mc -> link816 -> MAME, validating
|
||||
# the ported asm computes correctly on emulated hardware.
|
||||
iigs-clang-smoke:
|
||||
$(REPO_DIR)/toolchains/iigs/clang-smoke.sh
|
||||
|
||||
# Bake the NTP replayer bytes into an ORCA-M asm file. The asm declares
|
||||
# the bytes in a `data NTPDATA` segment; ORCA's linker groups same-
|
||||
# name object segments into one load segment, and the GS/OS loader
|
||||
# places it in its own bank. Net effect: the 34 KB of NTP bytes don't
|
||||
# crowd _ROOT in any binary, so audio_full.c can link into every demo
|
||||
# (vs the old audio.c-stub split). audio_full.c references
|
||||
# gNtpPlayerBytes / gNtpPlayerBytes_len as externs (case-sensitive
|
||||
# symbol match against the asm labels).
|
||||
$(NTP_ASM): $(NTP_BIN) $(REPO_DIR)/toolchains/iigs/bin-to-asm.sh
|
||||
@mkdir -p $(dir $@)
|
||||
$(REPO_DIR)/toolchains/iigs/bin-to-asm.sh $(NTP_BIN) $@ NTPDATA gNtpPlayerBytes gNtpPlayerBytes_len
|
||||
|
||||
# iix-build.sh takes MAIN.c first, then EXTRA sources (compiled with
|
||||
# #pragma noroot). The example source supplies main(); libjoey sources
|
||||
# are the extras. The chtyp post-step tags the output as GS/OS S16
|
||||
# ($B3) so GS/OS recognizes it as launchable; the file-type lives in
|
||||
# a user.com.apple.FinderInfo xattr that iix and profuse preserve.
|
||||
#
|
||||
# All binaries use ORCA-C large memory model (-b). Cost: slightly
|
||||
# larger / slower compiled C per the ORCA docs. Win: 32-bit pointers
|
||||
# everywhere, so library asm can take SurfaceT* args via one
|
||||
# consistent ABI (small-mm 16-bit pointers truncated bank bytes,
|
||||
# which broke any asm that wanted to address bank-1 stage memory).
|
||||
# Per-binary header dependency files. iix-build.sh -M emits one .d
|
||||
# alongside each binary covering every header transitively included
|
||||
# by the C sources in that binary's build. Pulled in via -include at
|
||||
# the bottom of this file so editing a shared header (e.g.
|
||||
# surfaceInternal.h) triggers a rebuild of every IIgs binary that
|
||||
# transitively depends on it.
|
||||
# Per-example binaries via clang-build.sh. clang-build.sh mirrors the
|
||||
# old driver's CLI (-I / -o / -M; -s and -b are accepted no-ops). These
|
||||
# link single-segment today; examples larger than one bank need the
|
||||
# multi-segment OMF wiring still in progress.
|
||||
DEP_DIR := $(BUILD)/dep
|
||||
PATTERN_DEP := $(DEP_DIR)/PATTERN.d
|
||||
DRAW_DEP := $(DEP_DIR)/DRAW.d
|
||||
KEYS_DEP := $(DEP_DIR)/KEYS.d
|
||||
JOY_DEP := $(DEP_DIR)/JOY.d
|
||||
SPRITE_DEP := $(DEP_DIR)/SPRITE.d
|
||||
UBER_DEP := $(DEP_DIR)/UBER.d
|
||||
AUDIO_DEP := $(DEP_DIR)/AUDIO.d
|
||||
ADV_DEP := $(DEP_DIR)/ADV.d
|
||||
ADV2_DEP := $(DEP_DIR)/ADV2.d
|
||||
AGI_DEP := $(DEP_DIR)/AGI.d
|
||||
STAXI_DEP := $(DEP_DIR)/STAXI.d
|
||||
|
||||
$(PATTERN_BIN): $(PATTERN_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
|
||||
$(BINDIR)/PATTERN: $(PATTERN_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
|
||||
@mkdir -p $(dir $@) $(DEP_DIR)
|
||||
$(IIGS_BUILD) -b -M $(PATTERN_DEP) $(IIX_INCLUDES) -o $@ $(PATTERN_SRC) $(LIB_SRCS)
|
||||
$(IIGS_IIX) chtyp -t S16 $@
|
||||
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/PATTERN.d $(INCLUDES) -o $@ $(PATTERN_SRC) $(LIB_SRCS)
|
||||
|
||||
$(DRAW_BIN): $(DRAW_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
|
||||
$(BINDIR)/DRAW: $(DRAW_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
|
||||
@mkdir -p $(dir $@) $(DEP_DIR)
|
||||
$(IIGS_BUILD) -b -M $(DRAW_DEP) $(IIX_INCLUDES) -o $@ $(DRAW_SRC) $(LIB_SRCS)
|
||||
$(IIGS_IIX) chtyp -t S16 $@
|
||||
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/DRAW.d $(INCLUDES) -o $@ $(DRAW_SRC) $(LIB_SRCS)
|
||||
|
||||
$(KEYS_BIN): $(KEYS_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
|
||||
$(BINDIR)/KEYS: $(KEYS_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
|
||||
@mkdir -p $(dir $@) $(DEP_DIR)
|
||||
$(IIGS_BUILD) -b -M $(KEYS_DEP) $(IIX_INCLUDES) -o $@ $(KEYS_SRC) $(LIB_SRCS)
|
||||
$(IIGS_IIX) chtyp -t S16 $@
|
||||
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/KEYS.d $(INCLUDES) -o $@ $(KEYS_SRC) $(LIB_SRCS)
|
||||
|
||||
$(JOY_BIN): $(JOY_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
|
||||
$(BINDIR)/JOY: $(JOY_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
|
||||
@mkdir -p $(dir $@) $(DEP_DIR)
|
||||
$(IIGS_BUILD) -b -M $(JOY_DEP) $(IIX_INCLUDES) -o $@ $(JOY_SRC) $(LIB_SRCS)
|
||||
$(IIGS_IIX) chtyp -t S16 $@
|
||||
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/JOY.d $(INCLUDES) -o $@ $(JOY_SRC) $(LIB_SRCS)
|
||||
|
||||
$(SPRITE_BIN): $(SPRITE_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
|
||||
$(BINDIR)/SPRITE: $(SPRITE_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
|
||||
@mkdir -p $(dir $@) $(DEP_DIR)
|
||||
$(IIGS_BUILD) -b -M $(SPRITE_DEP) $(IIX_INCLUDES) -o $@ $(SPRITE_SRC) $(LIB_SRCS)
|
||||
$(IIGS_IIX) chtyp -t S16 $@
|
||||
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/SPRITE.d $(INCLUDES) -o $@ $(SPRITE_SRC) $(LIB_SRCS)
|
||||
|
||||
# UBER bumps user stack to 16 KB. ORCA-C's default user stack is small
|
||||
# (~1 KB) and vfprintf's parsing buffer + the demo's own stack-local
|
||||
# format buffers were spilling past it -- the symptom was a crash to
|
||||
# monitor on the second varargs-style joeyLogF call. The hand-rolled
|
||||
# decimal formatter in uber.c also uses larger stack-local buffers
|
||||
# (line[96], num[16]) than typical demos. 16 KB is plenty of headroom.
|
||||
$(UBER_BIN): $(UBER_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
|
||||
$(BINDIR)/UBER: $(UBER_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
|
||||
@mkdir -p $(dir $@) $(DEP_DIR)
|
||||
$(IIGS_BUILD) -b -s 16384 -M $(UBER_DEP) $(IIX_INCLUDES) -o $@ $(UBER_SRC) $(LIB_SRCS)
|
||||
$(IIGS_IIX) chtyp -t S16 $@
|
||||
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/UBER.d $(INCLUDES) -o $@ $(UBER_SRC) $(LIB_SRCS)
|
||||
|
||||
# Adventure has a lot of static const sprite-row arrays and walk-mask
|
||||
# strings; the auto-segmenter sees a big main TU, so the 16 KB user
|
||||
# stack matches UBER's bump for the same reason (vfprintf + format
|
||||
# buffers stay within bounds).
|
||||
$(ADV_BIN): $(ADV_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
|
||||
$(BINDIR)/ADV: $(ADV_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
|
||||
@mkdir -p $(dir $@) $(DEP_DIR)
|
||||
$(IIGS_BUILD) -b -s 16384 -M $(ADV_DEP) $(IIX_INCLUDES) -o $@ $(ADV_SRC) $(LIB_SRCS)
|
||||
$(IIGS_IIX) chtyp -t S16 $@
|
||||
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/ADV.d $(INCLUDES) -o $@ $(ADV_SRC) $(LIB_SRCS)
|
||||
|
||||
$(ADV2_BIN): $(ADV2_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
|
||||
$(BINDIR)/ADV2: $(ADV2_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
|
||||
@mkdir -p $(dir $@) $(DEP_DIR)
|
||||
$(IIGS_BUILD) -b -s 16384 -M $(ADV2_DEP) $(IIX_INCLUDES) -o $@ $(ADV2_SRC) $(LIB_SRCS)
|
||||
$(IIGS_IIX) chtyp -t S16 $@
|
||||
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/ADV2.d $(INCLUDES) -o $@ $(ADV2_SRC) $(LIB_SRCS)
|
||||
|
||||
# AGI binary spans multiple .c files (agi.c is main; agiRes.c is the
|
||||
# resource loader). iix-build.sh takes the first .c as main and the
|
||||
# rest as extras (compiled #pragma noroot); ordering matters.
|
||||
$(AGI_BIN): $(AGI_SRCS) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
|
||||
$(BINDIR)/AGI: $(AGI_SRCS) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
|
||||
@mkdir -p $(dir $@) $(DEP_DIR)
|
||||
$(IIGS_BUILD) -b -s 16384 -M $(AGI_DEP) $(IIX_INCLUDES) -I $(EXAMPLES)/agi -o $@ $(AGI_SRCS) $(LIB_SRCS)
|
||||
$(IIGS_IIX) chtyp -t S16 $@
|
||||
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/AGI.d $(INCLUDES) -I$(EXAMPLES)/agi -o $@ $(AGI_SRCS) $(LIB_SRCS)
|
||||
|
||||
# Space Taxi: multi-source like AGI; 16 KB stack for headroom on the
|
||||
# placeholder render path (full tilemap repaint + snprintf buffers).
|
||||
# Note: asset files (level *.dat, *.jas) are NOT yet packaged onto
|
||||
# the disk image -- IIGS_PACKAGE only handles binaries today. The
|
||||
# game will fail to find its assets when run; wiring data files into
|
||||
# the packager is the next step for this port on IIgs (similar to
|
||||
# the open AGI/KQ3 disk-packaging task).
|
||||
$(STAXI_BIN): $(STAXI_SRCS) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
|
||||
$(BINDIR)/STAXI: $(STAXI_SRCS) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
|
||||
@mkdir -p $(dir $@) $(DEP_DIR)
|
||||
$(IIGS_BUILD) -b -s 16384 -M $(STAXI_DEP) $(IIX_INCLUDES) -I $(EXAMPLES)/spacetaxi -o $@ $(STAXI_SRCS) $(LIB_SRCS)
|
||||
$(IIGS_IIX) chtyp -t S16 $@
|
||||
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/STAXI.d $(INCLUDES) -I$(EXAMPLES)/spacetaxi -o $@ $(STAXI_SRCS) $(LIB_SRCS)
|
||||
|
||||
# Convert the cross-platform .MOD asset to NinjaTrackerPlus runtime
|
||||
# format via joeymod (which shells out to ntpconverter.php). Without
|
||||
# php-cli the conversion is skipped; in that case the IIgs disk just
|
||||
# won't carry a TEST.NTP and the audio demo will report "missing data
|
||||
# file" at runtime instead of crashing on bad MOD magic.
|
||||
HAVE_PHP := $(shell command -v php >/dev/null 2>&1 && echo 1)
|
||||
|
||||
ifeq ($(HAVE_PHP),1)
|
||||
$(AUDIO_NTP): $(AUDIO_MOD) $(JOEYMOD)
|
||||
@mkdir -p $(dir $@)
|
||||
$(JOEYMOD) $< $@
|
||||
|
||||
AUDIO_DATA_FILES := $(AUDIO_NTP) $(AUDIO_SFX)
|
||||
else
|
||||
$(info iigs: php-cli not installed -- AUDIO demo will ship without TEST.NTP; install php-cli for real IIgs audio playback)
|
||||
AUDIO_DATA_FILES := $(AUDIO_SFX)
|
||||
endif
|
||||
|
||||
$(AUDIO_BIN): $(AUDIO_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
|
||||
@mkdir -p $(dir $@) $(DEP_DIR)
|
||||
$(IIGS_BUILD) -b -M $(AUDIO_DEP) $(IIX_INCLUDES) -I $(EXAMPLES)/audio -o $@ $(AUDIO_SRC) $(LIB_SRCS)
|
||||
$(IIGS_IIX) chtyp -t S16 $@
|
||||
|
||||
# Assemble a ProDOS 2img containing the examples, ready to mount in
|
||||
# GSplus alongside a GS/OS boot volume.
|
||||
iigs-disk: $(DISK_IMG)
|
||||
|
||||
$(DISK_IMG): $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(AUDIO_DATA_FILES) $(IIGS_PACKAGE)
|
||||
@mkdir -p $(dir $@)
|
||||
$(IIGS_PACKAGE) $@ $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) -- $(AUDIO_DATA_FILES)
|
||||
# NOTE: the audio example is omitted until the NinjaTrackerPlus replayer
|
||||
# is wired into the clang build (a path for baking the replayer bytes
|
||||
# into a data segment via llvm-mc is pending).
|
||||
|
||||
clean-iigs:
|
||||
rm -rf $(BUILD)
|
||||
|
||||
# Pull in per-binary header-dependency files generated by iix-build.sh -M.
|
||||
# Without this, editing a header (e.g. surfaceInternal.h) doesn't rebuild
|
||||
# IIgs binaries that include it -- the IIgs's iix toolchain has no native
|
||||
# -MMD analog, so iix-build.sh shells out to host gcc for the scan.
|
||||
-include $(PATTERN_DEP) $(DRAW_DEP) $(KEYS_DEP) $(JOY_DEP) $(SPRITE_DEP) $(UBER_DEP) $(AUDIO_DEP) $(ADV_DEP) $(ADV2_DEP) $(AGI_DEP) $(STAXI_DEP)
|
||||
-include $(wildcard $(DEP_DIR)/*.d)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
# Outputs of a run land in /tmp/mame-iigs/:
|
||||
# - debug.log MAME's debugger console output (-debuglog)
|
||||
# - crash.txt Lua-hook crash dump (if anything halts the CPU)
|
||||
# - joeylog.txt Extracted from the post-run disk image
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
|
|
@ -232,7 +231,6 @@ On crash the MAME debugger halts and Lua dumps state to:
|
|||
$out/crash.txt
|
||||
|
||||
After exit:
|
||||
$out/joeylog.txt extracted from disk image
|
||||
$out/debug.log MAME debugger console output
|
||||
|
||||
EOF
|
||||
|
|
@ -248,23 +246,6 @@ cleanup() {
|
|||
rm -rf "$out/snap"
|
||||
mv "$work/snap" "$out/snap"
|
||||
fi
|
||||
local profuse=$repo/toolchains/iigs/gg-tools/bin/profuse
|
||||
local mnt=$work/_mnt
|
||||
if [[ -x $profuse && -f $work/joey.2mg ]]; then
|
||||
export GOLDEN_GATE="$repo/toolchains/iigs/goldengate"
|
||||
export ORCA_ROOT="$GOLDEN_GATE"
|
||||
mkdir -p "$mnt"
|
||||
if "$profuse" -oro "$work/joey.2mg" "$mnt" 2>/dev/null; then
|
||||
for name in JOEYLOG.TXT joeylog.txt; do
|
||||
if [[ -f $mnt/$name ]]; then
|
||||
cp "$mnt/$name" "$out/joeylog.txt"
|
||||
echo "extracted joeylog.txt -> $out/joeylog.txt" >&2
|
||||
break
|
||||
fi
|
||||
done
|
||||
fusermount -u "$mnt" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
rm -rf "$work"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
|
|
|||
|
|
@ -7,9 +7,7 @@
|
|||
#
|
||||
# No argument: GSplus has no way to dispatch a specific binary on boot
|
||||
# (Finder is interactive), so this script just stages the disk and
|
||||
# launches the emulator. The post-run trap below extracts joeylog.txt
|
||||
# from the data disk so demos that left a breadcrumb file are visible
|
||||
# from the host shell after the emulator exits.
|
||||
# launches the emulator.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
|
|
@ -21,12 +19,6 @@ fi
|
|||
|
||||
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
|
||||
# profuse looks up its FST helpers under $GOLDEN_GATE / $ORCA_ROOT and
|
||||
# falls back to /usr/share/orca which we don't install. Point both at
|
||||
# our staged GoldenGate tree.
|
||||
export GOLDEN_GATE="$repo/toolchains/iigs/goldengate"
|
||||
export ORCA_ROOT="$GOLDEN_GATE"
|
||||
|
||||
gsplus=$repo/toolchains/emulators/gsplus/bin/gsplus
|
||||
rom=$repo/toolchains/emulators/support/apple-iigs.rom
|
||||
sys_disk=$repo/toolchains/emulators/support/gsos-system.po
|
||||
|
|
@ -49,46 +41,15 @@ done
|
|||
# copies so repeated runs do not mutate the originals.
|
||||
work=$(mktemp -d -t joeylib-iigs.XXXXXX)
|
||||
|
||||
# After GSplus exits, mount the work copy of joey.2mg via profuse and
|
||||
# pull joeylog.txt out (if the demo wrote one) before tearing down the
|
||||
# scratch dir. This is a debug aid: the demos use joeyLog* to leave a
|
||||
# breadcrumb file on the JOEYLIB volume, but the volume only lives
|
||||
# inside the scratch dir while GSplus is running.
|
||||
log_out=$repo/build/iigs/bin/joeylog.txt
|
||||
extract_log() {
|
||||
local profuse=$repo/toolchains/iigs/gg-tools/bin/profuse
|
||||
local mnt=$work/_log_mnt
|
||||
cleanup() {
|
||||
# Stash the post-run disk image for manual inspection.
|
||||
if [[ -f $work/joey.2mg ]]; then
|
||||
cp "$work/joey.2mg" "$repo/build/iigs/bin/joey-after-run.2mg"
|
||||
echo "run-iigs: saved post-run disk -> $repo/build/iigs/bin/joey-after-run.2mg" >&2
|
||||
fi
|
||||
if [[ ! -x $profuse ]]; then
|
||||
echo "run-iigs: profuse not found at $profuse" >&2
|
||||
elif [[ ! -f $work/joey.2mg ]]; then
|
||||
echo "run-iigs: $work/joey.2mg missing" >&2
|
||||
else
|
||||
mkdir -p "$mnt"
|
||||
if "$profuse" -oro "$work/joey.2mg" "$mnt"; then
|
||||
echo "run-iigs: mounted $mnt; contents:" >&2
|
||||
ls -la "$mnt" >&2 || true
|
||||
if [[ -f $mnt/JOEYLOG.TXT ]]; then
|
||||
cp "$mnt/JOEYLOG.TXT" "$log_out"
|
||||
echo "run-iigs: extracted joeylog.txt -> $log_out" >&2
|
||||
elif [[ -f $mnt/joeylog.txt ]]; then
|
||||
cp "$mnt/joeylog.txt" "$log_out"
|
||||
echo "run-iigs: extracted joeylog.txt -> $log_out" >&2
|
||||
else
|
||||
echo "run-iigs: no JOEYLOG.TXT on disk" >&2
|
||||
fi
|
||||
fusermount -u "$mnt" 2>/dev/null || true
|
||||
else
|
||||
echo "run-iigs: profuse mount FAILED" >&2
|
||||
fi
|
||||
fi
|
||||
rm -rf "$work"
|
||||
}
|
||||
trap extract_log EXIT
|
||||
trap cleanup EXIT
|
||||
|
||||
cp "$sys_disk" "$work/boot.po"
|
||||
cp "$data_disk" "$work/joey.2mg"
|
||||
|
|
|
|||
|
|
@ -20,26 +20,35 @@
|
|||
#include "surfaceInternal.h"
|
||||
|
||||
|
||||
// Largest scratch buffer needed for any single emit call. 16 KB
|
||||
// covers a 32x32 sprite even on 68k (the biggest mixed-RMW byte-
|
||||
// emit at 16 bytes/byte * (16*17 dest bytes per shift) ~= 4.5 KB,
|
||||
// times shift count 2). Round up generously.
|
||||
#define SPRITE_EMIT_SCRATCH_BYTES (16u * 1024u)
|
||||
// Scratch buffer for a single emit call. 16 KB comfortably covers any
|
||||
// sprite up to a 32x32 worst-case all-mixed DRAW (16 rows*17 dest
|
||||
// bytes/shift * SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE ~= 4.5 KB on 68k,
|
||||
// the largest per-destbyte target). It is NOT trusted to bound every
|
||||
// legal input: a sprite near the create-time dimension cap
|
||||
// (widthTiles <= 40, heightTiles <= 25) can emit far more than 16 KB,
|
||||
// so every emitter ALSO bound-checks its write cursor against the
|
||||
// `cap` argument and returns SPRITE_EMIT_OVERFLOW when a routine would
|
||||
// not fit. jlSpriteCompile then bails cleanly (no arena alloc,
|
||||
// sp->slot stays NULL, the dispatcher falls back to the interpreted
|
||||
// halSpriteXxxPlanes path) instead of corrupting the heap. The
|
||||
// constant is a long literal so it never wraps the 16-bit int the
|
||||
// IIgs uses for plain int arithmetic.
|
||||
#define SPRITE_EMIT_SCRATCH_BYTES (16384ul)
|
||||
|
||||
|
||||
// Compile-time selection of the per-CPU emitter. One src/codegen/
|
||||
// spriteEmit*.c file is built per platform, but the dispatch lives
|
||||
// in this file so jlSpriteCompile + spriteCompiledDraw aren't
|
||||
// duplicated three times.
|
||||
static uint16_t emitDrawForTarget(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
||||
static uint32_t emitDrawForTarget(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
|
||||
#if defined(JOEYLIB_PLATFORM_DOS)
|
||||
return spriteEmitDrawX86(out, sp, shift);
|
||||
return spriteEmitDrawX86(out, cap, sp, shift);
|
||||
#elif defined(JOEYLIB_PLATFORM_AMIGA)
|
||||
return spriteEmitDrawPlanar68k(out, sp, shift);
|
||||
return spriteEmitDrawPlanar68k(out, cap, sp, shift);
|
||||
#elif defined(JOEYLIB_PLATFORM_ATARIST)
|
||||
return spriteEmitDrawInterleaved68k(out, sp, shift);
|
||||
return spriteEmitDrawInterleaved68k(out, cap, sp, shift);
|
||||
#elif defined(JOEYLIB_PLATFORM_IIGS)
|
||||
return spriteEmitDrawIigs(out, sp, shift);
|
||||
return spriteEmitDrawIigs(out, cap, sp, shift);
|
||||
#else
|
||||
# error "jlSpriteCompile: no emitter selected for this platform"
|
||||
#endif
|
||||
|
|
@ -51,33 +60,33 @@ static uint16_t emitDrawForTarget(uint8_t *out, const jlSpriteT *sp, uint8_t shi
|
|||
// src/core/sprite.c gates on routineOffsets[shift][SPRITE_OP_SAVE]
|
||||
// != SPRITE_NOT_COMPILED and falls back to the interpreted memcpy
|
||||
// path otherwise.
|
||||
static uint16_t emitSaveForTarget(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
||||
static uint32_t emitSaveForTarget(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
|
||||
#if defined(JOEYLIB_PLATFORM_DOS)
|
||||
return spriteEmitSaveX86(out, sp, shift);
|
||||
return spriteEmitSaveX86(out, cap, sp, shift);
|
||||
#elif defined(JOEYLIB_PLATFORM_AMIGA)
|
||||
return spriteEmitSavePlanar68k(out, sp, shift);
|
||||
return spriteEmitSavePlanar68k(out, cap, sp, shift);
|
||||
#elif defined(JOEYLIB_PLATFORM_ATARIST)
|
||||
return spriteEmitSaveInterleaved68k(out, sp, shift);
|
||||
return spriteEmitSaveInterleaved68k(out, cap, sp, shift);
|
||||
#elif defined(JOEYLIB_PLATFORM_IIGS)
|
||||
return spriteEmitSaveIigs(out, sp, shift);
|
||||
return spriteEmitSaveIigs(out, cap, sp, shift);
|
||||
#else
|
||||
(void)out; (void)sp; (void)shift;
|
||||
(void)out; (void)cap; (void)sp; (void)shift;
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
static uint16_t emitRestoreForTarget(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
||||
static uint32_t emitRestoreForTarget(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
|
||||
#if defined(JOEYLIB_PLATFORM_DOS)
|
||||
return spriteEmitRestoreX86(out, sp, shift);
|
||||
return spriteEmitRestoreX86(out, cap, sp, shift);
|
||||
#elif defined(JOEYLIB_PLATFORM_AMIGA)
|
||||
return spriteEmitRestorePlanar68k(out, sp, shift);
|
||||
return spriteEmitRestorePlanar68k(out, cap, sp, shift);
|
||||
#elif defined(JOEYLIB_PLATFORM_ATARIST)
|
||||
return spriteEmitRestoreInterleaved68k(out, sp, shift);
|
||||
return spriteEmitRestoreInterleaved68k(out, cap, sp, shift);
|
||||
#elif defined(JOEYLIB_PLATFORM_IIGS)
|
||||
return spriteEmitRestoreIigs(out, sp, shift);
|
||||
return spriteEmitRestoreIigs(out, cap, sp, shift);
|
||||
#else
|
||||
(void)out; (void)sp; (void)shift;
|
||||
(void)out; (void)cap; (void)sp; (void)shift;
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
|
@ -90,13 +99,24 @@ static uint16_t emitRestoreForTarget(uint8_t *out, const jlSpriteT *sp, uint8_t
|
|||
// land in the arena.
|
||||
static uint32_t emitTotalSize(uint8_t *scratch, const jlSpriteT *sp) {
|
||||
uint32_t total;
|
||||
uint32_t written;
|
||||
uint8_t shift;
|
||||
uint8_t op;
|
||||
|
||||
total = 0;
|
||||
for (shift = 0; shift < JOEY_SPRITE_SHIFT_COUNT; shift++) {
|
||||
total += emitDrawForTarget(scratch, sp, shift);
|
||||
total += emitSaveForTarget(scratch, sp, shift);
|
||||
total += emitRestoreForTarget(scratch, sp, shift);
|
||||
for (op = 0; op < SPRITE_OP_COUNT; op++) {
|
||||
switch (op) {
|
||||
case SPRITE_OP_DRAW: written = emitDrawForTarget (scratch, SPRITE_EMIT_SCRATCH_BYTES, sp, shift); break;
|
||||
case SPRITE_OP_SAVE: written = emitSaveForTarget (scratch, SPRITE_EMIT_SCRATCH_BYTES, sp, shift); break;
|
||||
case SPRITE_OP_RESTORE: written = emitRestoreForTarget(scratch, SPRITE_EMIT_SCRATCH_BYTES, sp, shift); break;
|
||||
default: written = 0; break;
|
||||
}
|
||||
if (written == SPRITE_EMIT_OVERFLOW) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
total += written;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
|
@ -109,8 +129,8 @@ bool jlSpriteCompile(jlSpriteT *sp) {
|
|||
uint8_t op;
|
||||
ArenaSlotT *slot;
|
||||
uint8_t *dst;
|
||||
uint16_t written;
|
||||
uint16_t offset;
|
||||
uint32_t written;
|
||||
uint32_t offset;
|
||||
|
||||
if (sp == NULL) {
|
||||
return false;
|
||||
|
|
@ -135,6 +155,8 @@ bool jlSpriteCompile(jlSpriteT *sp) {
|
|||
}
|
||||
|
||||
totalSize = emitTotalSize(scratch, sp);
|
||||
/* SPRITE_EMIT_OVERFLOW (0xFFFFFFFF) is > 0xFFFF too, so a single
|
||||
* routine that did not fit the scratch is rejected here as well. */
|
||||
if (totalSize > 0xFFFFu) {
|
||||
free(scratch);
|
||||
return false;
|
||||
|
|
@ -159,17 +181,32 @@ bool jlSpriteCompile(jlSpriteT *sp) {
|
|||
offset = 0;
|
||||
for (shift = 0; shift < JOEY_SPRITE_SHIFT_COUNT; shift++) {
|
||||
for (op = 0; op < SPRITE_OP_COUNT; op++) {
|
||||
uint32_t capLeft;
|
||||
|
||||
capLeft = (uint32_t)(totalSize - offset);
|
||||
switch (op) {
|
||||
case SPRITE_OP_DRAW: written = emitDrawForTarget (dst + offset, sp, shift); break;
|
||||
case SPRITE_OP_SAVE: written = emitSaveForTarget (dst + offset, sp, shift); break;
|
||||
case SPRITE_OP_RESTORE: written = emitRestoreForTarget(dst + offset, sp, shift); break;
|
||||
case SPRITE_OP_DRAW: written = emitDrawForTarget (dst + offset, capLeft, sp, shift); break;
|
||||
case SPRITE_OP_SAVE: written = emitSaveForTarget (dst + offset, capLeft, sp, shift); break;
|
||||
case SPRITE_OP_RESTORE: written = emitRestoreForTarget(dst + offset, capLeft, sp, shift); break;
|
||||
default: written = 0; break;
|
||||
}
|
||||
if (written == 0) {
|
||||
if (written == SPRITE_EMIT_OVERFLOW) {
|
||||
/* The second pass disagreed with the sizing pass (must
|
||||
* not happen, but never write past the slot). Roll back. */
|
||||
codegenArenaFree(slot);
|
||||
free(scratch);
|
||||
return false;
|
||||
}
|
||||
/* routineOffsets is uint16_t with 0xFFFF (SPRITE_NOT_COMPILED)
|
||||
* as the "absent" sentinel, so a real offset must stay below
|
||||
* it. totalSize <= 0xFFFF already guarantees this, but guard
|
||||
* explicitly so a future change can't store an offset that
|
||||
* collides with the sentinel. */
|
||||
if (written == 0 || offset >= SPRITE_NOT_COMPILED) {
|
||||
sp->routineOffsets[shift][op] = SPRITE_NOT_COMPILED;
|
||||
} else {
|
||||
sp->routineOffsets[shift][op] = offset;
|
||||
offset = (uint16_t)(offset + written);
|
||||
sp->routineOffsets[shift][op] = (uint16_t)offset;
|
||||
offset = offset + written;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -189,16 +226,37 @@ bool jlSpriteCompile(jlSpriteT *sp) {
|
|||
}
|
||||
|
||||
|
||||
#if defined(JOEYLIB_PLATFORM_IIGS) || (!defined(JOEYLIB_PLATFORM_AMIGA) && !defined(JOEYLIB_PLATFORM_ATARIST))
|
||||
|
||||
// Re-derive the shift a chunky SAVE used from its backup. The shift is
|
||||
// a property of the save (x & 1) and is NOT stored in the backup, so
|
||||
// every chunky restore path must re-infer it. Single source of truth
|
||||
// for the two chunky restore dispatchers (IIgs + x86/68k) so the
|
||||
// formula cannot drift between them: copyBytes == spriteBytesPerRow
|
||||
// means shift 0 (even x), otherwise the +1-byte shift-1 routine.
|
||||
#define SPRITE_CHUNKY_BYTES_PER_TILE_ROW 4u
|
||||
|
||||
static uint8_t spriteChunkyRestoreShift(const jlSpriteT *sp, const jlSpriteBackupT *backup) {
|
||||
uint16_t copyBytes;
|
||||
uint16_t spriteBytesPerRow;
|
||||
|
||||
copyBytes = (uint16_t)(backup->width >> 1);
|
||||
spriteBytesPerRow = (uint16_t)(sp->widthTiles * SPRITE_CHUNKY_BYTES_PER_TILE_ROW);
|
||||
return (uint8_t)((copyBytes == spriteBytesPerRow) ? 0u : 1u);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
#if defined(JOEYLIB_PLATFORM_IIGS)
|
||||
|
||||
// SURFACE_ROW_OFFSET dispatches to the gRowOffsetLut lookup on IIgs;
|
||||
// declared in surfaceInternal.h. Replaces ORCA-C's __mul16 JSL with a
|
||||
// single indexed long-mode read.
|
||||
// declared in surfaceInternal.h. Uses a single indexed long-mode read
|
||||
// for the multiply.
|
||||
|
||||
// IIgs uses inline asm + a self-modifying call stub instead of a C
|
||||
// function-pointer cast. The build uses ORCA-C large memory model
|
||||
// (-b for sprite demos) so pointers are 24-bit and JSL works
|
||||
// cross-bank.
|
||||
// function-pointer cast. The build uses a large memory model so
|
||||
// pointers are 24-bit and JSL works cross-bank.
|
||||
//
|
||||
// `sta abs,Y` on 65816 uses the data bank register (DBR) for the
|
||||
// high byte of the effective address, so we need DBR = dst's bank
|
||||
|
|
@ -235,23 +293,26 @@ static uint32_t gDrawStubLastFnAddr = 0xFFFFFFFFul;
|
|||
|
||||
void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y) {
|
||||
uint8_t shift;
|
||||
uint32_t destAddr;
|
||||
uint16_t destOffset;
|
||||
uint8_t destBank;
|
||||
uint32_t fnAddr;
|
||||
|
||||
{
|
||||
uint8_t *destPtr;
|
||||
uint32_t destAddr;
|
||||
const uint8_t *destB;
|
||||
shift = (uint8_t)(x & 1);
|
||||
destPtr = &dst->pixels[SURFACE_ROW_OFFSET(y) + ((uint16_t)x >> 1)];
|
||||
/* Byte-alias the 24-bit pointer to grab lo16 + bank without the
|
||||
* ~LSHR4 32-bit-shift helper, matching SPLIT_POINTER below. */
|
||||
destAddr = (uint32_t)destPtr;
|
||||
destOffset = (uint16_t)(destAddr & 0xFFFFu);
|
||||
destBank = (uint8_t)((destAddr >> 16) & 0xFFu);
|
||||
destB = (const uint8_t *)&destAddr;
|
||||
destOffset = (uint16_t)(destB[0] | ((uint16_t)destB[1] << 8));
|
||||
destBank = destB[2];
|
||||
fnAddr = codegenArenaBaseAddr()
|
||||
+ sp->slot->offset
|
||||
+ (uint32_t)sp->routineOffsets[shift][SPRITE_OP_DRAW];
|
||||
}
|
||||
(void)destAddr;
|
||||
|
||||
if (!gDrawStubInited) {
|
||||
gSpriteCallStub[ 0] = 0x8B;
|
||||
|
|
@ -284,8 +345,8 @@ void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t
|
|||
gDrawStubLastFnAddr = fnAddr;
|
||||
}
|
||||
|
||||
// ORCA-C compiles this function under `longa on / longi on`
|
||||
// (M=16, X=16) and emits the function epilogue assuming those
|
||||
// This function is compiled under `longa on / longi on`
|
||||
// (M=16, X=16) and the function epilogue assumes those
|
||||
// widths at exit -- the deallocation ADC takes a 2-byte immediate
|
||||
// and any LDX/LDY use 2-byte immediates. The byte writes to
|
||||
// gSpriteCallStub above leave M=8, and an earlier PHP/PLP-only
|
||||
|
|
@ -294,12 +355,13 @@ void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t
|
|||
// swallowed the TCS, S was never adjusted, RTL popped wrong
|
||||
// bytes, control fell into BSS, and the IIgs hit BRK on a zero
|
||||
// byte. Force M=16/X=16 before returning to compiled C.
|
||||
asm {
|
||||
rep #0x30
|
||||
sep #0x20
|
||||
jsl gSpriteCallStub
|
||||
rep #0x30
|
||||
}
|
||||
__asm__ volatile (
|
||||
"rep #0x30\n\t"
|
||||
"sep #0x20\n\t"
|
||||
"jsl gSpriteCallStub\n\t"
|
||||
"rep #0x30"
|
||||
::: "memory"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -360,12 +422,10 @@ static void patchMvnBanks(uint8_t *routine, uint16_t heightPx, uint8_t dstBank,
|
|||
|
||||
|
||||
|
||||
// Split a 24-bit pointer into its low 16 bits + bank byte. The
|
||||
// (uint32_t) cast works correctly in ORCA/C 2.2.1 (the 2.1.0 lossy-
|
||||
// bank-byte bug is fixed). To avoid invoking the ~LSHR4 32-bit-shift
|
||||
// helper for the `>> 16` to extract the bank byte, we cast to
|
||||
// uint32_t and then byte-alias the storage -- gets the same bytes
|
||||
// with simple loads.
|
||||
// Split a 24-bit pointer into its low 16 bits + bank byte. To avoid
|
||||
// invoking a 32-bit-shift helper for the `>> 16` to extract the bank
|
||||
// byte, we cast to uint32_t and then byte-alias the storage -- gets
|
||||
// the same bytes with simple loads.
|
||||
#define SPLIT_POINTER(_ptr, _outLo, _outBank) \
|
||||
do { \
|
||||
uint32_t spAddr_ = (uint32_t)(_ptr); \
|
||||
|
|
@ -418,7 +478,7 @@ void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, in
|
|||
clippedX = (int16_t)(x & ~1);
|
||||
widthPx = (uint16_t)(sp->widthTiles * 8);
|
||||
heightPx = (uint16_t)(sp->heightTiles * 8);
|
||||
copyBytes = (uint16_t)((widthPx >> 1) + (shift == 1 ? 1 : 0));
|
||||
copyBytes = (uint16_t)((widthPx >> 1) + shift); /* shift is x & 1, exactly 0 or 1 */
|
||||
|
||||
screenPtr = (uint8_t *)&src->pixels[SURFACE_ROW_OFFSET(y) + ((uint16_t)clippedX >> 1)];
|
||||
SPLIT_POINTER(screenPtr, &screenLo, &screenBank);
|
||||
|
|
@ -442,9 +502,9 @@ void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, in
|
|||
}
|
||||
|
||||
/* Compute the 1D index into the cached* / routineOffsets 2D arrays
|
||||
* once. ORCA-C 2.2.1 lowers `shift * SPRITE_OP_COUNT` (where
|
||||
* SPRITE_OP_COUNT==3) to a ~MUL4 helper call; (shift<<1)+shift
|
||||
* compiles to two ASLs and an ADC, no helper. */
|
||||
* once. `shift * SPRITE_OP_COUNT` (where SPRITE_OP_COUNT==3) can
|
||||
* lower to a multiply helper call; (shift<<1)+shift compiles to
|
||||
* two ASLs and an ADC, no helper. */
|
||||
cacheIdx = (uint16_t)(((uint16_t)shift << 1) + (uint16_t)shift + SPRITE_OP_SAVE);
|
||||
cachedDst = (uint8_t *)sp->cachedDstBank + cacheIdx;
|
||||
cachedSrc = (uint8_t *)sp->cachedSrcBank + cacheIdx;
|
||||
|
|
@ -495,20 +555,19 @@ void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, in
|
|||
}
|
||||
|
||||
// MVN-based routine: needs M=16 / X=16; restore M=16 on exit
|
||||
// matches ORCA-C `longa on` epilogue expectations.
|
||||
asm {
|
||||
rep #0x30
|
||||
jsl gSpriteSaveStub
|
||||
rep #0x30
|
||||
}
|
||||
// matches the `longa on` epilogue expectations.
|
||||
__asm__ volatile (
|
||||
"rep #0x30\n\t"
|
||||
"jsl gSpriteSaveStub\n\t"
|
||||
"rep #0x30"
|
||||
::: "memory"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup) {
|
||||
uint8_t shift;
|
||||
uint16_t heightPx;
|
||||
uint16_t copyBytes;
|
||||
uint16_t spriteBytesPerRow;
|
||||
uint16_t screenLo;
|
||||
uint16_t backupLo;
|
||||
uint8_t screenBank;
|
||||
|
|
@ -524,9 +583,7 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup)
|
|||
|
||||
sp = backup->sprite;
|
||||
heightPx = backup->height;
|
||||
copyBytes = (uint16_t)(backup->width >> 1);
|
||||
spriteBytesPerRow = (uint16_t)(sp->widthTiles * 4);
|
||||
shift = (copyBytes == spriteBytesPerRow) ? 0 : 1;
|
||||
shift = spriteChunkyRestoreShift(sp, backup);
|
||||
|
||||
screenPtr = (uint8_t *)&dst->pixels[SURFACE_ROW_OFFSET(backup->y) + ((uint16_t)backup->x >> 1)];
|
||||
SPLIT_POINTER(screenPtr, &screenLo, &screenBank);
|
||||
|
|
@ -580,11 +637,12 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup)
|
|||
*cachedSrc = backupBank;
|
||||
}
|
||||
|
||||
asm {
|
||||
rep #0x30
|
||||
jsl gSpriteRestoreStub
|
||||
rep #0x30
|
||||
}
|
||||
__asm__ volatile (
|
||||
"rep #0x30\n\t"
|
||||
"jsl gSpriteRestoreStub\n\t"
|
||||
"rep #0x30"
|
||||
::: "memory"
|
||||
);
|
||||
}
|
||||
|
||||
#elif defined(JOEYLIB_PLATFORM_AMIGA)
|
||||
|
|
@ -602,7 +660,10 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup)
|
|||
* interpreter, which handles arbitrary x via halSpriteDrawPlanes /
|
||||
* halSpriteSavePlanes / halSpriteRestorePlanes. */
|
||||
|
||||
#define AMIGA_BYTES_PER_ROW_LOCAL 40
|
||||
// Amiga per-plane row stride: 320 px / 8 px-per-plane-byte = 40.
|
||||
// Derived from surface geometry so it cannot drift if the fixed
|
||||
// surface width ever changes.
|
||||
#define AMIGA_PLANE_BYTES_PER_ROW (SURFACE_WIDTH / 8)
|
||||
|
||||
void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y) {
|
||||
typedef void (*DrawFn)(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3);
|
||||
|
|
@ -615,7 +676,7 @@ void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t
|
|||
DrawFn fn;
|
||||
|
||||
shift = (uint8_t)(x & 7);
|
||||
byteOff = (uint16_t)((uint16_t)y * AMIGA_BYTES_PER_ROW_LOCAL + ((uint16_t)x >> 3));
|
||||
byteOff = (uint16_t)((uint16_t)y * AMIGA_PLANE_BYTES_PER_ROW + ((uint16_t)x >> 3));
|
||||
p0 = halSurfacePlanePtr(dst, 0); if (p0 == NULL) return;
|
||||
p1 = halSurfacePlanePtr(dst, 1);
|
||||
p2 = halSurfacePlanePtr(dst, 2);
|
||||
|
|
@ -646,7 +707,7 @@ void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, in
|
|||
if (shift != 0u) {
|
||||
widthPx = (uint16_t)(widthPx + 8u);
|
||||
}
|
||||
byteOff = (uint16_t)((uint16_t)y * AMIGA_BYTES_PER_ROW_LOCAL + ((uint16_t)clippedX >> 3));
|
||||
byteOff = (uint16_t)((uint16_t)y * AMIGA_PLANE_BYTES_PER_ROW + ((uint16_t)clippedX >> 3));
|
||||
|
||||
backup->sprite = sp;
|
||||
backup->x = clippedX;
|
||||
|
|
@ -683,7 +744,7 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup)
|
|||
* Shifted slots 1..7 all alias to the same restore bytes, so
|
||||
* slot 1 stands in for any non-zero shift. */
|
||||
shift = (uint8_t)(backup->width > (uint16_t)(sp->widthTiles * 8) ? 1u : 0u);
|
||||
byteOff = (uint16_t)((uint16_t)backup->y * AMIGA_BYTES_PER_ROW_LOCAL + ((uint16_t)backup->x >> 3));
|
||||
byteOff = (uint16_t)((uint16_t)backup->y * AMIGA_PLANE_BYTES_PER_ROW + ((uint16_t)backup->x >> 3));
|
||||
|
||||
p0 = halSurfacePlanePtr(dst, 0); if (p0 == NULL) return;
|
||||
p1 = halSurfacePlanePtr(dst, 1);
|
||||
|
|
@ -708,6 +769,14 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup)
|
|||
* 2+ : non-byte-aligned x, never compiled (emitter returns 0); the
|
||||
* per-shift offset is SPRITE_NOT_COMPILED so the dispatcher
|
||||
* falls back to halSpriteDrawPlanes. */
|
||||
|
||||
// ST word-interleaved row stride: 320 px * 4 planes / 8 bits = 160
|
||||
// bytes/row. Numerically equal to SURFACE_BYTES_PER_ROW but a distinct
|
||||
// fact (group-interleaved layout, not chunky), so it gets its own
|
||||
// name. Each 16-pixel group is 4 plane words = 8 bytes.
|
||||
#define ST_GROUP_BYTES_PER_ROW (SURFACE_WIDTH * 4 / 8)
|
||||
#define ST_BYTES_PER_PLANE_GROUP 8u
|
||||
|
||||
void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y) {
|
||||
typedef void (*DrawFn)(uint8_t *groupBase);
|
||||
uint8_t shift;
|
||||
|
|
@ -734,8 +803,8 @@ void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t
|
|||
return;
|
||||
}
|
||||
groupBase = base
|
||||
+ (uint16_t)y * 160u
|
||||
+ (uint16_t)((uint16_t)x >> 4) * 8u;
|
||||
+ (uint16_t)y * ST_GROUP_BYTES_PER_ROW
|
||||
+ (uint16_t)((uint16_t)x >> 4) * ST_BYTES_PER_PLANE_GROUP;
|
||||
fn = (DrawFn)(codegenArenaBase() + sp->slot->offset + routeOffset);
|
||||
fn(groupBase);
|
||||
}
|
||||
|
|
@ -788,7 +857,7 @@ void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, in
|
|||
clippedX = (int16_t)(x & ~1);
|
||||
widthPx = (uint16_t)(sp->widthTiles * 8);
|
||||
heightPx = (uint16_t)(sp->heightTiles * 8);
|
||||
copyBytes = (uint16_t)((widthPx >> 1) + (shift == 1 ? 1 : 0));
|
||||
copyBytes = (uint16_t)((widthPx >> 1) + shift); /* shift is x & 1, exactly 0 or 1 */
|
||||
|
||||
screenPtr = (uint8_t *)&src->pixels[(uint16_t)y * SURFACE_BYTES_PER_ROW + ((uint16_t)clippedX >> 1)];
|
||||
|
||||
|
|
@ -812,15 +881,11 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup)
|
|||
typedef void (*CopyFn)(const uint8_t *src, uint8_t *dst);
|
||||
jlSpriteT *sp;
|
||||
uint8_t shift;
|
||||
uint16_t copyBytes;
|
||||
uint16_t spriteBytesPerRow;
|
||||
uint8_t *screenPtr;
|
||||
CopyFn fn;
|
||||
|
||||
sp = backup->sprite;
|
||||
copyBytes = (uint16_t)(backup->width >> 1);
|
||||
spriteBytesPerRow = (uint16_t)(sp->widthTiles * 4);
|
||||
shift = (copyBytes == spriteBytesPerRow) ? 0 : 1;
|
||||
shift = spriteChunkyRestoreShift(sp, backup);
|
||||
|
||||
screenPtr = (uint8_t *)&dst->pixels[(uint16_t)backup->y * SURFACE_BYTES_PER_ROW + ((uint16_t)backup->x >> 1)];
|
||||
|
||||
|
|
|
|||
|
|
@ -35,12 +35,25 @@
|
|||
#define TILE_BYTES_PER_ROW 4
|
||||
#define TRANSPARENT_NIBBLE 0
|
||||
|
||||
#define MAX_ROUTINE_BYTES 16384
|
||||
// Stack-arg displacements for the m68k cdecl calling convention used by
|
||||
// the emitted routines (args pushed right-to-left, return PC at 0(sp)):
|
||||
// draw(dst) -> dst at 4(sp)
|
||||
// save/restore(src, dst) -> src at 4(sp), dst at 8(sp)
|
||||
#define STACK_ARG0_DISP 0x0004u
|
||||
#define STACK_ARG1_DISP 0x0008u
|
||||
|
||||
// Worst-case emitted bytes for one draw dest byte (mixed RMW: move.b
|
||||
// d16(a0),d0; andi.b; ori.b; move.b d0,d16(a0) = 4 instrs * 4 bytes).
|
||||
#define DRAW_MAX_BYTES_PER_DESTBYTE SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_68K
|
||||
// Bytes for the per-row adda.w stride advance.
|
||||
#define ROW_STRIDE_ADVANCE_BYTES 4u
|
||||
// Bytes for the rts epilogue.
|
||||
#define EPILOGUE_BYTES 2u
|
||||
|
||||
|
||||
// ----- Prototypes -----
|
||||
|
||||
static uint16_t emitCopyBody68k(uint8_t *out, uint16_t cursor, uint16_t heightPx, uint16_t copyBytes, bool strideOnSrc);
|
||||
static uint32_t emitCopyBody68k(uint8_t *out, uint32_t cap, uint32_t cursor, uint16_t heightPx, uint16_t copyBytes, bool strideOnSrc);
|
||||
static void shiftedByteAt(const jlSpriteT *sp, uint16_t row, uint16_t col, uint8_t shift, uint16_t spriteBytesPerRow, uint8_t *outValue, uint8_t *outOpaqueMask);
|
||||
static uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col);
|
||||
static uint16_t writeBE16(uint8_t *out, uint16_t value);
|
||||
|
|
@ -58,14 +71,20 @@ static uint16_t writeBE16(uint8_t *out, uint16_t value);
|
|||
//
|
||||
// strideOnSrc=true -> source has the screen stride (SAVE)
|
||||
// strideOnSrc=false -> destination has the screen stride (RESTORE)
|
||||
static uint16_t emitCopyBody68k(uint8_t *out, uint16_t cursor, uint16_t heightPx, uint16_t copyBytes, bool strideOnSrc) {
|
||||
static uint32_t emitCopyBody68k(uint8_t *out, uint32_t cap, uint32_t cursor, uint16_t heightPx, uint16_t copyBytes, bool strideOnSrc) {
|
||||
uint16_t row;
|
||||
uint16_t col;
|
||||
uint16_t advance;
|
||||
uint32_t rowBytes;
|
||||
|
||||
advance = (uint16_t)(SURFACE_BYTES_PER_ROW - copyBytes);
|
||||
|
||||
for (row = 0; row < heightPx; row++) {
|
||||
// One row: copyBytes * 2 (move.b) + the per-row adda stride.
|
||||
rowBytes = (uint32_t)copyBytes * 2u + ROW_STRIDE_ADVANCE_BYTES;
|
||||
if (cursor + rowBytes > cap) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
// Unrolled: move.b (a0)+, (a1)+ -- 0x12D8.
|
||||
for (col = 0; col < copyBytes; col++) {
|
||||
cursor += writeBE16(out + cursor, 0x12D8u);
|
||||
|
|
@ -155,9 +174,10 @@ static uint16_t writeBE16(uint8_t *out, uint16_t value) {
|
|||
}
|
||||
|
||||
|
||||
// 68k draw emit. Returns bytes written.
|
||||
uint16_t spriteEmitDraw68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
||||
uint16_t cursor;
|
||||
// 68k draw emit. Returns bytes written, or SPRITE_EMIT_OVERFLOW if the
|
||||
// routine would exceed `cap`.
|
||||
uint32_t spriteEmitDraw68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
|
||||
uint32_t cursor;
|
||||
uint16_t row;
|
||||
uint16_t col;
|
||||
uint16_t heightPx;
|
||||
|
|
@ -176,15 +196,21 @@ uint16_t spriteEmitDraw68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
|||
cursor = 0;
|
||||
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
|
||||
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW);
|
||||
destBytesPerRow = (uint16_t)(spriteBytesPerRow + (shift == 1 ? 1 : 0));
|
||||
destBytesPerRow = (uint16_t)(spriteBytesPerRow + shift); /* shift is 0 or 1 */
|
||||
|
||||
// Prologue: movea.l 4(sp), a0
|
||||
if (cursor + 4u > cap) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
cursor += writeBE16(out + cursor, 0x206Fu);
|
||||
cursor += writeBE16(out + cursor, 0x0004u);
|
||||
cursor += writeBE16(out + cursor, STACK_ARG0_DISP);
|
||||
|
||||
for (row = 0; row < heightPx; row++) {
|
||||
if (row > 0) {
|
||||
// adda.w #SURFACE_BYTES_PER_ROW, a0
|
||||
if (cursor + ROW_STRIDE_ADVANCE_BYTES > cap) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
cursor += writeBE16(out + cursor, 0xD0FCu);
|
||||
cursor += writeBE16(out + cursor, (uint16_t)SURFACE_BYTES_PER_ROW);
|
||||
}
|
||||
|
|
@ -193,6 +219,11 @@ uint16_t spriteEmitDraw68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
|||
if (opaqueMask == 0x00) {
|
||||
continue;
|
||||
}
|
||||
// Reserve the worst case for one dest byte (16-byte mixed
|
||||
// RMW) plus the rts epilogue.
|
||||
if (cursor + DRAW_MAX_BYTES_PER_DESTBYTE + EPILOGUE_BYTES > cap) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
if (opaqueMask == 0xFFu) {
|
||||
// move.b #imm, d16(a0)
|
||||
// Opcode 0x117C: bits 11-9 = dst reg (0=a0), bits 8-6 =
|
||||
|
|
@ -221,14 +252,17 @@ uint16_t spriteEmitDraw68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
|||
}
|
||||
|
||||
// Epilogue: rts
|
||||
if (cursor + EPILOGUE_BYTES > cap) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
cursor += writeBE16(out + cursor, 0x4E75u);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
|
||||
// RESTORE: copy backup -> screen. Destination has the screen stride.
|
||||
uint16_t spriteEmitRestore68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
||||
uint16_t cursor;
|
||||
uint32_t spriteEmitRestore68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
|
||||
uint32_t cursor;
|
||||
uint16_t heightPx;
|
||||
uint16_t copyBytes;
|
||||
|
||||
|
|
@ -238,24 +272,33 @@ uint16_t spriteEmitRestore68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift)
|
|||
|
||||
cursor = 0;
|
||||
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
|
||||
copyBytes = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW + (shift == 1u ? 1u : 0u));
|
||||
copyBytes = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW + shift); /* shift is 0 or 1 */
|
||||
|
||||
// Prologue: movea.l 4(sp), a0 (src); movea.l 8(sp), a1 (dst).
|
||||
if (cursor + 8u > cap) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
cursor += writeBE16(out + cursor, 0x206Fu);
|
||||
cursor += writeBE16(out + cursor, 0x0004u);
|
||||
cursor += writeBE16(out + cursor, STACK_ARG0_DISP);
|
||||
cursor += writeBE16(out + cursor, 0x226Fu);
|
||||
cursor += writeBE16(out + cursor, 0x0008u);
|
||||
cursor += writeBE16(out + cursor, STACK_ARG1_DISP);
|
||||
|
||||
cursor = emitCopyBody68k(out, cursor, heightPx, copyBytes, false);
|
||||
cursor = emitCopyBody68k(out, cap, cursor, heightPx, copyBytes, false);
|
||||
if (cursor == SPRITE_EMIT_OVERFLOW) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
|
||||
if (cursor + EPILOGUE_BYTES > cap) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
cursor += writeBE16(out + cursor, 0x4E75u);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
|
||||
// SAVE: copy screen -> backup. Source has the screen stride.
|
||||
uint16_t spriteEmitSave68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
||||
uint16_t cursor;
|
||||
uint32_t spriteEmitSave68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
|
||||
uint32_t cursor;
|
||||
uint16_t heightPx;
|
||||
uint16_t copyBytes;
|
||||
|
||||
|
|
@ -265,15 +308,24 @@ uint16_t spriteEmitSave68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
|||
|
||||
cursor = 0;
|
||||
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
|
||||
copyBytes = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW + (shift == 1u ? 1u : 0u));
|
||||
copyBytes = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW + shift); /* shift is 0 or 1 */
|
||||
|
||||
if (cursor + 8u > cap) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
cursor += writeBE16(out + cursor, 0x206Fu);
|
||||
cursor += writeBE16(out + cursor, 0x0004u);
|
||||
cursor += writeBE16(out + cursor, STACK_ARG0_DISP);
|
||||
cursor += writeBE16(out + cursor, 0x226Fu);
|
||||
cursor += writeBE16(out + cursor, 0x0008u);
|
||||
cursor += writeBE16(out + cursor, STACK_ARG1_DISP);
|
||||
|
||||
cursor = emitCopyBody68k(out, cursor, heightPx, copyBytes, true);
|
||||
cursor = emitCopyBody68k(out, cap, cursor, heightPx, copyBytes, true);
|
||||
if (cursor == SPRITE_EMIT_OVERFLOW) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
|
||||
if (cursor + EPILOGUE_BYTES > cap) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
cursor += writeBE16(out + cursor, 0x4E75u);
|
||||
return cursor;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// IIgs (65816) sprite codegen. Emits PIC draw routines that write
|
||||
// 4bpp packed surface bytes via abs,Y indexed addressing.
|
||||
//
|
||||
// CALLING CONVENTION: NOT the ORCA-C fn-pointer convention. The
|
||||
// CALLING CONVENTION: NOT a C fn-pointer convention. The
|
||||
// runtime never calls these routines via a C cast -- instead,
|
||||
// spriteCompiledDraw (in jlSpriteCompile.c, gated on
|
||||
// JOEYLIB_PLATFORM_IIGS) builds a self-modifying JSL stub that
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
// - M = 8-bit (set by stub before JSL)
|
||||
// - X = 16-bit (set by stub)
|
||||
// - Y = destRow (loaded by stub from immediate)
|
||||
// - DBR = program bank (ORCA-C default)
|
||||
// - DBR = program bank
|
||||
// No stack arg, no prologue. Body executes directly.
|
||||
//
|
||||
// Routine shape (per-byte emit, no PEA optimization yet):
|
||||
|
|
@ -39,12 +39,10 @@
|
|||
#define TILE_BYTES_PER_ROW 4
|
||||
#define TRANSPARENT_NIBBLE 0
|
||||
|
||||
#define MAX_ROUTINE_BYTES 8192
|
||||
|
||||
|
||||
// ----- Prototypes -----
|
||||
|
||||
static uint16_t emitMvnCopyRoutine(uint8_t *out, uint16_t heightPx, uint16_t copyBytes, bool advanceX);
|
||||
static uint32_t emitMvnCopyRoutine(uint8_t *out, uint32_t cap, uint16_t heightPx, uint16_t copyBytes, bool advanceX);
|
||||
static void shiftedByteAt(const jlSpriteT *sp, uint16_t row, uint16_t col, uint8_t shift, uint16_t spriteBytesPerRow, uint8_t *outValue, uint8_t *outOpaqueMask);
|
||||
static uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col);
|
||||
|
||||
|
|
@ -116,8 +114,7 @@ static uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col)
|
|||
|
||||
|
||||
// writeLE16 was inlined at every call site. Inlining cuts a JSL/RTL
|
||||
// per emitted 16-bit immediate (4 instructions per byte * 12 sites)
|
||||
// and avoids ORCA-Linker bank-fragility around tiny-helper resolution.
|
||||
// per emitted 16-bit immediate (4 instructions per byte * 12 sites).
|
||||
|
||||
// Common backbone for save and restore. Both ops copy a byte-aligned
|
||||
// rectangle row-by-row using MVN; only the operand banks (which buffer
|
||||
|
|
@ -150,15 +147,36 @@ static uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col)
|
|||
//
|
||||
// The MVN at row R has its dstbk at routine offset (12*R + 4) and
|
||||
// srcbk at (12*R + 5).
|
||||
static uint16_t emitMvnCopyRoutine(uint8_t *out, uint16_t heightPx, uint16_t copyBytes, bool advanceX) {
|
||||
uint16_t cursor;
|
||||
// Worst-case bytes emitted per MVN copy row: row 0 = 6 bytes, any
|
||||
// later row = 12. Plus the trailing RTL.
|
||||
#define IIGS_MVN_MAX_BYTES_PER_ROW 12u
|
||||
|
||||
static uint32_t emitMvnCopyRoutine(uint8_t *out, uint32_t cap, uint16_t heightPx, uint16_t copyBytes, bool advanceX) {
|
||||
uint32_t cursor;
|
||||
uint16_t advance;
|
||||
uint16_t row;
|
||||
|
||||
// MVN copies (count + 1) bytes from the LDA #(copyBytes-1) operand.
|
||||
// A zero copyBytes would emit LDA #0xFFFF and MVN a full 64 KB --
|
||||
// a screen-trashing blit. widthTiles==0 is rejected at sprite
|
||||
// creation, so copyBytes>=4 in practice, but guard the helper so a
|
||||
// future caller cannot weaponize it: emit a bare RTL no-op instead.
|
||||
if (copyBytes == 0u) {
|
||||
if (cap < 1u) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
out[0] = 0x6B;
|
||||
return 1u;
|
||||
}
|
||||
|
||||
cursor = 0;
|
||||
advance = (uint16_t)(SURFACE_BYTES_PER_ROW - copyBytes);
|
||||
|
||||
for (row = 0; row < heightPx; row++) {
|
||||
// Reserve the row's worst case plus the trailing RTL.
|
||||
if (cursor + IIGS_MVN_MAX_BYTES_PER_ROW + 1u > cap) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
if (row > 0) {
|
||||
out[cursor++] = advanceX ? 0x8A : 0x98; // TXA / TYA
|
||||
out[cursor++] = 0x18; // CLC
|
||||
|
|
@ -174,6 +192,9 @@ static uint16_t emitMvnCopyRoutine(uint8_t *out, uint16_t heightPx, uint16_t cop
|
|||
out[cursor++] = 0x00; // dstbk -- patched per call
|
||||
out[cursor++] = 0x00; // srcbk -- patched per call
|
||||
}
|
||||
if (cursor + 1u > cap) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
out[cursor++] = 0x6B; // RTL
|
||||
return cursor;
|
||||
}
|
||||
|
|
@ -184,7 +205,7 @@ static uint16_t emitMvnCopyRoutine(uint8_t *out, uint16_t heightPx, uint16_t cop
|
|||
// by copyBytes per row. Backup rows are contiguous in memory so Y is
|
||||
// already correct for the next row; screen rows are SURFACE_BYTES_PER_ROW
|
||||
// apart so X needs an explicit ADC between rows.
|
||||
uint16_t spriteEmitSaveIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
||||
uint32_t spriteEmitSaveIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
|
||||
uint16_t heightPx;
|
||||
uint16_t spriteBytesPerRow;
|
||||
uint16_t copyBytes;
|
||||
|
|
@ -195,16 +216,16 @@ uint16_t spriteEmitSaveIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
|||
|
||||
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
|
||||
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW);
|
||||
copyBytes = (uint16_t)(spriteBytesPerRow + (shift == 1 ? 1 : 0));
|
||||
copyBytes = (uint16_t)(spriteBytesPerRow + shift); /* shift is 0 or 1 */
|
||||
|
||||
return emitMvnCopyRoutine(out, heightPx, copyBytes, /*advanceX*/true);
|
||||
return emitMvnCopyRoutine(out, cap, heightPx, copyBytes, /*advanceX*/true);
|
||||
}
|
||||
|
||||
|
||||
// RESTORE (backup -> screen). Stub passes X = backup low offset,
|
||||
// Y = screen low offset. Backup is contiguous so X advances correctly
|
||||
// via MVN; screen needs explicit advance, so Y is the one we ADC.
|
||||
uint16_t spriteEmitRestoreIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
||||
uint32_t spriteEmitRestoreIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
|
||||
uint16_t heightPx;
|
||||
uint16_t spriteBytesPerRow;
|
||||
uint16_t copyBytes;
|
||||
|
|
@ -215,9 +236,9 @@ uint16_t spriteEmitRestoreIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift)
|
|||
|
||||
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
|
||||
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW);
|
||||
copyBytes = (uint16_t)(spriteBytesPerRow + (shift == 1 ? 1 : 0));
|
||||
copyBytes = (uint16_t)(spriteBytesPerRow + shift); /* shift is 0 or 1 */
|
||||
|
||||
return emitMvnCopyRoutine(out, heightPx, copyBytes, /*advanceX*/false);
|
||||
return emitMvnCopyRoutine(out, cap, heightPx, copyBytes, /*advanceX*/false);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -252,8 +273,8 @@ uint16_t spriteEmitRestoreIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift)
|
|||
//
|
||||
// Mixed bytes always run on the M=8 path because the AND/ORA in
|
||||
// M=16 would clobber the adjacent byte.
|
||||
uint16_t spriteEmitDrawIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
||||
uint16_t cursor;
|
||||
uint32_t spriteEmitDrawIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
|
||||
uint32_t cursor;
|
||||
uint16_t row;
|
||||
uint16_t col;
|
||||
uint16_t heightPx;
|
||||
|
|
@ -273,7 +294,7 @@ uint16_t spriteEmitDrawIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
|||
cursor = 0;
|
||||
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
|
||||
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW);
|
||||
destBytesPerRow = (uint16_t)(spriteBytesPerRow + (shift == 1 ? 1 : 0));
|
||||
destBytesPerRow = (uint16_t)(spriteBytesPerRow + shift); /* shift is 0 or 1 */
|
||||
wide = false;
|
||||
|
||||
// No prologue: caller (the inline-asm stub in jlSpriteCompile.c)
|
||||
|
|
@ -285,7 +306,19 @@ uint16_t spriteEmitDrawIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
|||
if (opaqueMask == 0x00) {
|
||||
continue;
|
||||
}
|
||||
absOffset = (uint16_t)(row * SURFACE_BYTES_PER_ROW + col);
|
||||
// Reserve the worst case for one dest byte: the 9-byte
|
||||
// mixed RMW, plus a possible REP/SEP mode transition (2),
|
||||
// plus the trailing SEP+RTL (3) at routine end.
|
||||
if (cursor + SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_IIGS + 2u + 3u > cap) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
// Compute the dest-byte offset in a 32-bit intermediate so
|
||||
// row*160 cannot wrap a 16-bit int used for plain
|
||||
// int arithmetic (row up to ~199, 199*160 = 31840 is in
|
||||
// range for legal sprites, but a clamp-bypassing caller
|
||||
// must not silently wrap). The result still fits the 16-bit
|
||||
// abs,Y operand for any on-surface sprite.
|
||||
absOffset = (uint16_t)((uint32_t)row * SURFACE_BYTES_PER_ROW + col);
|
||||
|
||||
if (opaqueMask == 0xFFu && (col + 1) < destBytesPerRow) {
|
||||
// Look ahead: if (col, col+1) are both fully opaque
|
||||
|
|
@ -341,6 +374,14 @@ uint16_t spriteEmitDrawIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
|||
}
|
||||
}
|
||||
|
||||
// Trailing SEP (2 bytes if still wide) + RTL (1 byte). The per-byte
|
||||
// reserve above accounts for these whenever a byte was emitted, but
|
||||
// an all-transparent sprite never entered the inner write, so guard
|
||||
// the epilogue explicitly.
|
||||
if (cursor + (wide ? 2u : 0u) + 1u > cap) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
|
||||
// Routine exits in M=8: the JSL stub assumes M=8 throughout (the
|
||||
// stub itself only ever ran with M=8 and doesn't restore M). The
|
||||
// asm wrapper after the JSL forces M=16 again, but be defensive
|
||||
|
|
|
|||
|
|
@ -1,34 +1,15 @@
|
|||
// 68k sprite codegen for ST word-interleaved planar layout. Emits a
|
||||
// cdecl-callable routine `void draw(uint8_t *groupBase)` that walks
|
||||
// the sprite's tile data and writes plane bytes via `d16(a0)` chains.
|
||||
// 68k sprite codegen for ST word-interleaved planar layout.
|
||||
//
|
||||
// ST planar layout reminder (doc/atarist_planar.md): one buffer; per
|
||||
// scanline 20 groups of 8 bytes; per group, 4 plane words back-to-
|
||||
// back. groupBase points at the FIRST group the sprite touches:
|
||||
// pd->base + y * 160 + (x >> 4) * 8
|
||||
//
|
||||
// Shift index for ST is bit 3 of x (whether the sprite starts in the
|
||||
// high half or low half of the first group). x mod 8 != 0 falls back
|
||||
// to the interpreter (returns 0 from this emitter so sp->slot stays
|
||||
// NULL for those alignments).
|
||||
//
|
||||
// Per (row, tile_col, plane) we emit one of:
|
||||
// * nothing (op byte = 0, all transparent)
|
||||
// * move.b #pbN, d16(a0) (op = 0xFF, full replace, 6 bytes)
|
||||
// * clr.b d16(a0) (op = 0xFF AND pbN = 0, 4 bytes)
|
||||
// * andi.b #~op, d16(a0) (op partial, pbN = 0, 6 bytes)
|
||||
// * ori.b #pbN, d16(a0) (op partial, pbN == op, 6 bytes)
|
||||
// * andi.b #~op + ori.b #pbN (mixed, 12 bytes)
|
||||
//
|
||||
// d16 is the byte offset from groupBase to the target plane byte.
|
||||
// Layout of the byte offset:
|
||||
// shift 0: byteOff = (col >> 1) * 8 + plane*2 + (col & 1)
|
||||
// shift 1: byteOff = ((col + 1) >> 1) * 8 + plane*2 + (1 - (col & 1))
|
||||
// Each tile column is 8 sprite pixels = exactly half a 16-pixel
|
||||
// group, alternating high (offset 0) and low (offset 1) bytes of
|
||||
// each plane word.
|
||||
//
|
||||
// Per row we adda.w #160, a0 to advance to the next scanline.
|
||||
// Phase 11 (shared-walker): the per-shift unrolled emitter was
|
||||
// retired. halSpriteDrawPlanes in src/port/atarist/hal.c now handles
|
||||
// all shifts byte-wide at runtime, including the formerly fast shifts
|
||||
// 0 and 1 -- per-cel arena cost drops to zero. The byte-aligned fast
|
||||
// path stSpriteDrawByteAligned is still called from halSpriteDrawPlanes
|
||||
// for shift==0 sprites that are fully on surface, so byte-aligned UBER
|
||||
// perf does not regress. The entry points below are kept so the
|
||||
// dispatcher API stays uniform across ports; they emit zero bytes, so
|
||||
// jlSpriteCompile takes the "totalSize == 0 -> bail" path and sp->slot
|
||||
// stays NULL.
|
||||
|
||||
#include "joey/sprite.h"
|
||||
#include "joey/surface.h"
|
||||
|
|
@ -36,194 +17,21 @@
|
|||
#include "spriteInternal.h"
|
||||
|
||||
|
||||
// ----- Constants -----
|
||||
|
||||
#define TILE_PIXELS 8
|
||||
#define TILE_BYTES 32
|
||||
#define TILE_BYTES_PER_ROW 4
|
||||
#define ST_BYTES_PER_ROW 160
|
||||
|
||||
|
||||
// ----- Helpers -----
|
||||
|
||||
static uint16_t writeBE16(uint8_t *out, uint16_t value) {
|
||||
out[0] = (uint8_t)(value >> 8);
|
||||
out[1] = (uint8_t)(value & 0xFFu);
|
||||
return 2;
|
||||
}
|
||||
|
||||
|
||||
// Build the 4 plane bytes + opacity byte for one (row, tileCol)
|
||||
// pair. pbN bit 7 is sprite pixel 0 (leftmost), bit 0 is pixel 7.
|
||||
// op bit N is set iff that pixel's color != 0.
|
||||
static void buildPlaneBytes(const jlSpriteT *sp, uint16_t row, uint16_t tileCol,
|
||||
uint8_t *outPb0, uint8_t *outPb1,
|
||||
uint8_t *outPb2, uint8_t *outPb3,
|
||||
uint8_t *outOp) {
|
||||
uint16_t tileY = (uint16_t)(row >> 3);
|
||||
uint16_t inTileY = (uint16_t)(row & 7u);
|
||||
uint16_t wTiles = sp->widthTiles;
|
||||
const uint8_t *tileBytes = sp->tileData + (uint32_t)(tileY * wTiles + tileCol) * 32u;
|
||||
const uint8_t *tileRow = tileBytes + (uint32_t)inTileY * TILE_BYTES_PER_ROW;
|
||||
uint8_t pb0 = 0u;
|
||||
uint8_t pb1 = 0u;
|
||||
uint8_t pb2 = 0u;
|
||||
uint8_t pb3 = 0u;
|
||||
uint8_t op = 0u;
|
||||
uint8_t p;
|
||||
uint8_t b;
|
||||
uint8_t color;
|
||||
uint8_t bit;
|
||||
|
||||
for (p = 0; p < 8u; p++) {
|
||||
b = tileRow[p >> 1];
|
||||
color = (p & 1u) ? (uint8_t)(b & 0x0Fu) : (uint8_t)(b >> 4);
|
||||
bit = (uint8_t)(0x80u >> p);
|
||||
if (color != 0u) {
|
||||
op = (uint8_t)(op | bit);
|
||||
if (color & 1u) pb0 = (uint8_t)(pb0 | bit);
|
||||
if (color & 2u) pb1 = (uint8_t)(pb1 | bit);
|
||||
if (color & 4u) pb2 = (uint8_t)(pb2 | bit);
|
||||
if (color & 8u) pb3 = (uint8_t)(pb3 | bit);
|
||||
}
|
||||
}
|
||||
*outPb0 = pb0;
|
||||
*outPb1 = pb1;
|
||||
*outPb2 = pb2;
|
||||
*outPb3 = pb3;
|
||||
*outOp = op;
|
||||
}
|
||||
|
||||
|
||||
// Emit code for one plane byte at d16(a0). Returns bytes written.
|
||||
// op=opacity byte, pb=plane byte (subset of op).
|
||||
static uint16_t emitPlaneByte(uint8_t *out, uint16_t cursor, uint16_t d16, uint8_t op, uint8_t pb) {
|
||||
uint16_t start = cursor;
|
||||
|
||||
if (op == 0u) {
|
||||
return 0u; /* nothing to emit */
|
||||
}
|
||||
if (op == 0xFFu) {
|
||||
/* All 8 pixels opaque: replace the byte. */
|
||||
if (pb == 0u) {
|
||||
/* clr.b d16(a0). Opcode 0x4228 + d16. 4 bytes. */
|
||||
cursor += writeBE16(out + cursor, 0x4228u);
|
||||
cursor += writeBE16(out + cursor, d16);
|
||||
} else {
|
||||
/* move.b #pb, d16(a0). Opcode 0x117C + #imm word + d16. 6 bytes. */
|
||||
cursor += writeBE16(out + cursor, 0x117Cu);
|
||||
cursor += writeBE16(out + cursor, (uint16_t)pb);
|
||||
cursor += writeBE16(out + cursor, d16);
|
||||
}
|
||||
return (uint16_t)(cursor - start);
|
||||
}
|
||||
/* Partial opacity. pb is a subset of op. */
|
||||
if (pb == 0u) {
|
||||
/* All opaque pixels have plane bit 0: just clear those bits. */
|
||||
/* andi.b #~op, d16(a0). Opcode 0x0228 + #imm word + d16. 6 bytes. */
|
||||
cursor += writeBE16(out + cursor, 0x0228u);
|
||||
cursor += writeBE16(out + cursor, (uint16_t)(~op & 0xFFu));
|
||||
cursor += writeBE16(out + cursor, d16);
|
||||
return (uint16_t)(cursor - start);
|
||||
}
|
||||
if (pb == op) {
|
||||
/* All opaque pixels have plane bit 1: just set those bits. */
|
||||
/* ori.b #op, d16(a0). Opcode 0x0028 + #imm word + d16. 6 bytes. */
|
||||
cursor += writeBE16(out + cursor, 0x0028u);
|
||||
cursor += writeBE16(out + cursor, (uint16_t)op);
|
||||
cursor += writeBE16(out + cursor, d16);
|
||||
return (uint16_t)(cursor - start);
|
||||
}
|
||||
/* Mixed: clear opaque bits, then set the plane bits. */
|
||||
cursor += writeBE16(out + cursor, 0x0228u);
|
||||
cursor += writeBE16(out + cursor, (uint16_t)(~op & 0xFFu));
|
||||
cursor += writeBE16(out + cursor, d16);
|
||||
cursor += writeBE16(out + cursor, 0x0028u);
|
||||
cursor += writeBE16(out + cursor, (uint16_t)pb);
|
||||
cursor += writeBE16(out + cursor, d16);
|
||||
return (uint16_t)(cursor - start);
|
||||
}
|
||||
|
||||
|
||||
// ----- Emit API -----
|
||||
|
||||
uint16_t spriteEmitDrawInterleaved68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
||||
/* Phase 11 (shared-walker): bypass the per-shift unrolled emitter.
|
||||
* halSpriteDrawPlanes in src/port/atarist/hal.c now handles all
|
||||
* shifts byte-wide at runtime, including the formerly fast
|
||||
* shifts 0 and 1 -- per-cel arena cost drops to zero. The byte-
|
||||
* aligned fast path stSpriteDrawByteAligned is still called from
|
||||
* halSpriteDrawPlanes for shift==0 sprites that are fully on
|
||||
* surface, so byte-aligned UBER perf doesn't regress. */
|
||||
(void)out; (void)sp; (void)shift;
|
||||
uint32_t spriteEmitDrawInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
|
||||
(void)out; (void)cap; (void)sp; (void)shift;
|
||||
return 0u;
|
||||
}
|
||||
|
||||
|
||||
uint16_t spriteEmitDrawInterleaved68kOld(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
||||
uint16_t cursor;
|
||||
uint16_t row;
|
||||
uint16_t col;
|
||||
uint16_t plane;
|
||||
uint16_t heightPx;
|
||||
uint16_t wTiles;
|
||||
uint8_t pb[4];
|
||||
uint8_t op;
|
||||
|
||||
if (shift > 1u) {
|
||||
return 0u;
|
||||
}
|
||||
|
||||
cursor = 0u;
|
||||
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
|
||||
wTiles = sp->widthTiles;
|
||||
|
||||
/* Prologue: movea.l 4(sp), a0. Opcode 0x206F + d16=4. 4 bytes. */
|
||||
cursor += writeBE16(out + cursor, 0x206Fu);
|
||||
cursor += writeBE16(out + cursor, 0x0004u);
|
||||
|
||||
for (row = 0; row < heightPx; row++) {
|
||||
if (row > 0u) {
|
||||
/* adda.w #160, a0. Opcode 0xD0FC + imm word. 4 bytes. */
|
||||
cursor += writeBE16(out + cursor, 0xD0FCu);
|
||||
cursor += writeBE16(out + cursor, (uint16_t)ST_BYTES_PER_ROW);
|
||||
}
|
||||
for (col = 0; col < wTiles; col++) {
|
||||
buildPlaneBytes(sp, row, col, &pb[0], &pb[1], &pb[2], &pb[3], &op);
|
||||
if (op == 0u) {
|
||||
continue; /* whole tile column row is transparent */
|
||||
}
|
||||
for (plane = 0; plane < 4u; plane++) {
|
||||
uint16_t d16;
|
||||
if (shift == 0u) {
|
||||
/* col 0 (high) -> +0, col 1 (low) -> +1, col 2
|
||||
* (high group 1) -> +8, ... */
|
||||
d16 = (uint16_t)((col >> 1) * 8 + plane * 2 + (col & 1u));
|
||||
} else {
|
||||
/* col 0 (low) -> +1, col 1 (high group 1) -> +8, ... */
|
||||
d16 = (uint16_t)(((col + 1u) >> 1) * 8 + plane * 2 + (1u - (col & 1u)));
|
||||
}
|
||||
cursor += emitPlaneByte(out, cursor, d16, op, pb[plane]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Epilogue: rts. */
|
||||
cursor += writeBE16(out + cursor, 0x4E75u);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
|
||||
/* Save / restore aren't implemented yet -- returning 0 so they fall
|
||||
* through to the C interpreter (halSpriteSavePlanes / halSpriteRestorePlanes
|
||||
* fast paths cover the byte-aligned case). */
|
||||
uint16_t spriteEmitSaveInterleaved68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
||||
(void)out; (void)sp; (void)shift;
|
||||
uint32_t spriteEmitSaveInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
|
||||
(void)out; (void)cap; (void)sp; (void)shift;
|
||||
return 0u;
|
||||
}
|
||||
|
||||
|
||||
uint16_t spriteEmitRestoreInterleaved68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
||||
(void)out; (void)sp; (void)shift;
|
||||
uint32_t spriteEmitRestoreInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
|
||||
(void)out; (void)cap; (void)sp; (void)shift;
|
||||
return 0u;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,40 +1,20 @@
|
|||
// Planar 68k sprite codegen for Amiga (post-Phase 9, no chunky shadow).
|
||||
// Planar 68k sprite codegen for Amiga.
|
||||
//
|
||||
// Emits PIC routines that write directly to the four bitplanes via 4
|
||||
// address-register pointers (a0..a3 = plane[0..3] base + byteOff,
|
||||
// where byteOff = y*40 + x/8 -- the dispatcher pre-computes this).
|
||||
// Phase 11 (shared-walker): the Amiga port no longer emits per-cel
|
||||
// unrolled draw/save/restore code. Each cel was ~17-23 KB compiled --
|
||||
// 21 cels burned ~340 KB of arena. The cross-platform dispatcher in
|
||||
// src/core/sprite.c routes through halSpriteXxxPlanes in
|
||||
// src/port/amiga/hal.c when sp->slot is NULL, and those HAL hooks are
|
||||
// byte-wide walkers that read sp->tileData on the fly and synthesize
|
||||
// plane bytes at draw time. Same per-byte work, shared as one static
|
||||
// lib routine instead of 21 inlined copies; the pre-shift plane-byte
|
||||
// decomposition (and its shift/transparency math) now lives ONLY in
|
||||
// halSpriteDrawPlanes, so there is one source of truth.
|
||||
//
|
||||
// Calling convention (cdecl on m68k-amigaos-gcc):
|
||||
// draw(p0, p1, p2, p3):
|
||||
// args at 4(sp), 8(sp), 12(sp), 16(sp) -- one ULONG per plane.
|
||||
// loaded into a0..a3 by the prologue.
|
||||
// save(p0, p1, p2, p3, backup):
|
||||
// 5 args; backup at 20(sp), loaded into a4.
|
||||
// restore(p0, p1, p2, p3, backup):
|
||||
// same as save but reads backup, writes planes.
|
||||
//
|
||||
// Per-byte plane write encoding decisions:
|
||||
// - all-transparent (mask=0): skip the byte entirely
|
||||
// - all-opaque (mask=0xFF): move.b #imm, d16(an) (6 bytes)
|
||||
// - mixed (0<mask<0xFF): move.b d16(an), d0;
|
||||
// andi.b #~mask, d0;
|
||||
// ori.b #imm, d0;
|
||||
// move.b d0, d16(an) (4+6+6+4 = 20 bytes)
|
||||
//
|
||||
// Per row advance: 4 plane pointers each get adda.w #SURFACE_WIDTH/8
|
||||
// = adda.w #40, an (4 bytes encoded each, 16 bytes total per row).
|
||||
// We omit the advance after the last row.
|
||||
//
|
||||
// Shift handling: shifts 0..7 are pre-baked. The dispatcher selects
|
||||
// the variant via x % 8 and pre-computes byteOff = y*40 + (x & ~7)/8
|
||||
// (i.e. round x DOWN to 8-pixel boundary). The variant for shift s
|
||||
// then emits to (widthTiles + 1) plane bytes per row when s != 0
|
||||
// (the rightmost shift bits spill into one extra plane byte) and to
|
||||
// widthTiles plane bytes per row when s == 0.
|
||||
//
|
||||
// The emitter assumes sprite width is a multiple of 8 (= a multiple
|
||||
// of one tile = a multiple of 8 pixels) so plane bytes per row are
|
||||
// integer. JoeyLib sprites are always tile-multiple by API contract.
|
||||
// The entry points below are kept so the dispatcher API stays uniform
|
||||
// across ports. Returning 0 tells emitTotalSize() the emitter has no
|
||||
// bytes for this op, so jlSpriteCompile takes the "totalSize == 0 ->
|
||||
// bail" path and sp->slot stays NULL.
|
||||
|
||||
#include "joey/sprite.h"
|
||||
#include "joey/surface.h"
|
||||
|
|
@ -42,499 +22,21 @@
|
|||
#include "spriteInternal.h"
|
||||
|
||||
|
||||
// ----- Constants -----
|
||||
|
||||
#define TILE_PIXELS 8
|
||||
#define TILE_BYTES 32
|
||||
#define TILE_BYTES_PER_ROW 4
|
||||
#define TRANSPARENT_NIBBLE 0
|
||||
#define AMIGA_BITPLANES 4
|
||||
#define AMIGA_BYTES_PER_ROW 40
|
||||
|
||||
|
||||
// ----- Instruction encoding helpers -----
|
||||
|
||||
static uint16_t writeBE16(uint8_t *out, uint16_t value) {
|
||||
out[0] = (uint8_t)((value >> 8) & 0xFFu);
|
||||
out[1] = (uint8_t)(value & 0xFFu);
|
||||
return 2u;
|
||||
}
|
||||
|
||||
|
||||
// movea.l <d16,SP>, an -- load arg at SP+disp into An.
|
||||
// Encoding: 0010 nnn 001 010 111 + disp16
|
||||
// = 0x2057 + (n << 9), where n is dst An.
|
||||
// a0: 0x206F, a1: 0x226F, a2: 0x246F, a3: 0x266F, a4: 0x286F.
|
||||
static const uint16_t kMoveaSpToAn[] = {
|
||||
0x206Fu, 0x226Fu, 0x246Fu, 0x266Fu, 0x286Fu, 0x2A6Fu, 0x2C6Fu, 0x2E6Fu
|
||||
};
|
||||
|
||||
|
||||
// adda.w #imm, an -- adds 16-bit signed imm to An (sign-extended).
|
||||
// Encoding: 1101 nnn 011 111 100 + imm
|
||||
// = 0xD0FC + (n << 9).
|
||||
static const uint16_t kAddaWImmToAn[] = {
|
||||
0xD0FCu, 0xD2FCu, 0xD4FCu, 0xD6FCu, 0xD8FCu, 0xDAFCu, 0xDCFCu, 0xDEFCu
|
||||
};
|
||||
|
||||
|
||||
// ANDI.B #imm, D0 -- 4 bytes (opcode word + imm word, byte in low half).
|
||||
// Opcode: 0000 0010 00 000 000 (size=byte, mode=Dn, reg=D0)
|
||||
#define ANDI_B_IMM_D0 0x0200u
|
||||
|
||||
// ORI.B #imm, D0 -- 4 bytes (opcode word + imm word, byte in low half).
|
||||
// Opcode: 0000 0000 00 000 000
|
||||
#define ORI_B_IMM_D0 0x0000u
|
||||
|
||||
|
||||
// MOVE.B d16(An), D0 -- 4 bytes (opcode + disp).
|
||||
// Encoding: 0001 000 000 mode reg
|
||||
// = size=01 (byte), dst reg=000 (D0), dst mode=000 (Dn),
|
||||
// src mode=101 (d16,An), src reg=An.
|
||||
// = 0001000 000 101 nnn = 0x1028 + An.
|
||||
static const uint16_t kMoveBD16AnToD0[] = {
|
||||
0x1028u, 0x1029u, 0x102Au, 0x102Bu
|
||||
};
|
||||
|
||||
|
||||
// MOVE.B D0, d16(An) -- 4 bytes (opcode + disp).
|
||||
// Encoding: 0001 nnn 101 000 000 = 0x1140 + (An << 9).
|
||||
static const uint16_t kMoveBD0ToD16An[] = {
|
||||
0x1140u, 0x1340u, 0x1540u, 0x1740u
|
||||
};
|
||||
|
||||
|
||||
// MOVE.B #imm, d16(An) -- 6 bytes (opcode + imm + disp).
|
||||
// Encoding: 0001 nnn 101 111 100 = 0x117C + (An << 9).
|
||||
// (Was 0x113C earlier -- that's mode=100=predec; mode=101=d16(An)
|
||||
// is the bit difference. Predec emits a 4-byte instruction with no
|
||||
// disp word, so the byte stream went out of sync and every
|
||||
// subsequent instruction decoded into garbage.)
|
||||
static const uint16_t kMoveBImmToD16An[] = {
|
||||
0x117Cu, 0x137Cu, 0x157Cu, 0x177Cu
|
||||
};
|
||||
|
||||
|
||||
// MOVE.B (a4)+, d16(An) -- 4 bytes (opcode + disp). -- used by save/restore (backup in a4)
|
||||
// Encoding: 0001 nnn 101 011 100 = 0x115C + (An << 9).
|
||||
static const uint16_t kMoveBA4PostincToD16An[] = {
|
||||
0x115Cu, 0x135Cu, 0x155Cu, 0x175Cu
|
||||
};
|
||||
|
||||
|
||||
// MOVE.B d16(An), (a4)+ -- 4 bytes (opcode + disp). -- used by save (planes -> backup)
|
||||
// Encoding: 1001 100 011 mode reg
|
||||
// Wait, MOVE.B src,(a4)+ : dst mode = 011 (an+), dst reg = 100 (A4),
|
||||
// so dst reg=100, dst mode=011 -> opcode high = 0001 100 011 ...
|
||||
// = 0001100011 mode reg = 0x18C0..
|
||||
// 0001 100 011 101 nnn = 0x18E8 + An.
|
||||
static const uint16_t kMoveBD16AnToA4Postinc[] = {
|
||||
0x18E8u, 0x18E9u, 0x18EAu, 0x18EBu
|
||||
};
|
||||
|
||||
|
||||
// MOVEM.L reglist, -(SP) -- 4 bytes (opcode + reglist mask).
|
||||
// Opcode 0x48E7. Predec mask is REVERSED vs all other modes:
|
||||
// bit 15 = D0, ..., bit 8 = D7, bit 7 = A0, bit 6 = A1, bit 5 = A2,
|
||||
// bit 4 = A3, bit 3 = A4, bit 2 = A5, bit 1 = A6, bit 0 = A7.
|
||||
#define MOVEM_L_PUSH_OPCODE 0x48E7u
|
||||
#define MOVEM_L_MASK_A2_A3 0x0030u /* bits 5,4 = A2,A3 (predec order) */
|
||||
#define MOVEM_L_MASK_A2_A3_A4 0x0038u /* bits 5,4,3 = A2,A3,A4 */
|
||||
|
||||
// MOVEM.L (SP)+, reglist -- 4 bytes (opcode + reglist mask).
|
||||
// Opcode 0x4CDF. Postinc mask follows the standard layout:
|
||||
// bit 0 = D0, ..., bit 7 = D7, bit 8 = A0, ..., bit 15 = A7.
|
||||
#define MOVEM_L_POP_OPCODE 0x4CDFu
|
||||
#define MOVEM_L_MASK_POP_A2_A3 0x0C00u /* bits 11,10 = A3,A2 */
|
||||
#define MOVEM_L_MASK_POP_A2_A3_A4 0x1C00u /* bits 12,11,10 = A4,A3,A2 */
|
||||
|
||||
// RTS opcode.
|
||||
#define OPCODE_RTS 0x4E75u
|
||||
|
||||
|
||||
// ----- Emit helpers -----
|
||||
|
||||
// For shift 0 (byte-aligned x), the sprite's chunky tile data converts
|
||||
// directly to plane bytes without any sub-byte shifting. For each
|
||||
// (row, col-byte, plane) we extract the 8 plane bits from 4 chunky
|
||||
// bytes (= 8 pixels) and produce one plane byte; we also produce a
|
||||
// mask byte indicating which pixel positions are non-transparent
|
||||
// (any plane bit != 0 in the source means non-transparent if
|
||||
// transparent index is 0, the JoeyLib convention).
|
||||
//
|
||||
// Sprite layout: tileData = wTiles x hTiles tiles, each tile = 8 rows
|
||||
// x 4 chunky bytes (32 bytes). Tiles laid out row-major within the
|
||||
// sprite. For plane-byte column `c` of row `r`:
|
||||
// tileX = c (since each plane byte covers exactly one tile column)
|
||||
// tileY = r / 8
|
||||
// inTileY = r % 8
|
||||
// chunky bytes = tileData + (tileY*wTiles + tileX)*32 + inTileY*4 + 0..3
|
||||
//
|
||||
// `col` must be in [0, widthTiles); callers handle out-of-range cols
|
||||
// (used when computing shifted variants that span widthTiles+1 output
|
||||
// bytes per row) by passing a sentinel and checking against widthTiles
|
||||
// before invoking this helper.
|
||||
static void planeByteAndMaskAt(const jlSpriteT *sp, uint16_t row, uint16_t col,
|
||||
uint8_t *planeBytes /*[4]*/, uint8_t *maskByte)
|
||||
{
|
||||
uint16_t tileX;
|
||||
uint16_t tileY;
|
||||
uint16_t inTileY;
|
||||
const uint8_t *tile;
|
||||
const uint8_t *chunky;
|
||||
uint8_t nibbles[8];
|
||||
uint8_t b0, b1, b2, b3;
|
||||
uint16_t p;
|
||||
uint8_t bitMask;
|
||||
uint8_t pix;
|
||||
|
||||
tileX = col;
|
||||
tileY = row >> 3;
|
||||
inTileY = row & 7u;
|
||||
|
||||
tile = sp->tileData + (uint32_t)((tileY * sp->widthTiles + tileX) * 32u);
|
||||
chunky = tile + inTileY * 4u;
|
||||
|
||||
nibbles[0] = (uint8_t)(chunky[0] >> 4);
|
||||
nibbles[1] = (uint8_t)(chunky[0] & 0x0Fu);
|
||||
nibbles[2] = (uint8_t)(chunky[1] >> 4);
|
||||
nibbles[3] = (uint8_t)(chunky[1] & 0x0Fu);
|
||||
nibbles[4] = (uint8_t)(chunky[2] >> 4);
|
||||
nibbles[5] = (uint8_t)(chunky[2] & 0x0Fu);
|
||||
nibbles[6] = (uint8_t)(chunky[3] >> 4);
|
||||
nibbles[7] = (uint8_t)(chunky[3] & 0x0Fu);
|
||||
|
||||
b0 = 0u; b1 = 0u; b2 = 0u; b3 = 0u;
|
||||
*maskByte = 0u;
|
||||
for (p = 0; p < 8u; p++) {
|
||||
pix = nibbles[p];
|
||||
if (pix == TRANSPARENT_NIBBLE) {
|
||||
continue;
|
||||
}
|
||||
bitMask = (uint8_t)(0x80u >> p);
|
||||
*maskByte = (uint8_t)(*maskByte | bitMask);
|
||||
if (pix & 1u) b0 = (uint8_t)(b0 | bitMask);
|
||||
if (pix & 2u) b1 = (uint8_t)(b1 | bitMask);
|
||||
if (pix & 4u) b2 = (uint8_t)(b2 | bitMask);
|
||||
if (pix & 8u) b3 = (uint8_t)(b3 | bitMask);
|
||||
}
|
||||
planeBytes[0] = b0;
|
||||
planeBytes[1] = b1;
|
||||
planeBytes[2] = b2;
|
||||
planeBytes[3] = b3;
|
||||
}
|
||||
|
||||
|
||||
// Shifted variant: produces 4 plane bytes and 1 mask byte for output
|
||||
// column `outCol` (0..widthTiles inclusive) of row `row` when the
|
||||
// sprite is shifted right by `shift` pixels (1..7). For shift 0,
|
||||
// callers should use planeByteAndMaskAt directly (faster, no spill).
|
||||
//
|
||||
// Each output byte is composed of bits drawn from up to two source
|
||||
// plane bytes:
|
||||
// leftPart = src[outCol-1] << (8 - shift) (high (shift) bits)
|
||||
// rightPart = src[outCol] >> shift (low (8-shift) bits)
|
||||
// with src[-1] and src[widthTiles] treated as 0/transparent. The
|
||||
// resulting plane byte is leftPart | rightPart; the mask byte is the
|
||||
// shifted union of the per-byte source masks.
|
||||
static void planeByteAndMaskShifted(const jlSpriteT *sp, uint16_t row, uint16_t outCol,
|
||||
uint8_t shift, uint16_t widthTiles,
|
||||
uint8_t *planeBytes /*[4]*/, uint8_t *maskByte)
|
||||
{
|
||||
uint8_t leftPlanes[AMIGA_BITPLANES];
|
||||
uint8_t leftMask;
|
||||
uint8_t rightPlanes[AMIGA_BITPLANES];
|
||||
uint8_t rightMask;
|
||||
uint8_t i;
|
||||
|
||||
leftMask = 0u;
|
||||
rightMask = 0u;
|
||||
for (i = 0; i < AMIGA_BITPLANES; i++) {
|
||||
leftPlanes[i] = 0u;
|
||||
rightPlanes[i] = 0u;
|
||||
}
|
||||
|
||||
if (outCol > 0u && (uint16_t)(outCol - 1u) < widthTiles) {
|
||||
planeByteAndMaskAt(sp, row, (uint16_t)(outCol - 1u), leftPlanes, &leftMask);
|
||||
}
|
||||
if (outCol < widthTiles) {
|
||||
planeByteAndMaskAt(sp, row, outCol, rightPlanes, &rightMask);
|
||||
}
|
||||
|
||||
*maskByte = (uint8_t)(((leftMask << (8u - shift)) & 0xFFu) |
|
||||
((rightMask >> shift) & 0xFFu));
|
||||
for (i = 0; i < AMIGA_BITPLANES; i++) {
|
||||
planeBytes[i] = (uint8_t)(((leftPlanes[i] << (8u - shift)) & 0xFFu) |
|
||||
((rightPlanes[i] >> shift) & 0xFFu));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Emit code that merges one plane byte into d16(an) where d16 is the
|
||||
// row-relative byte offset (0 since we re-base each row by adda.w).
|
||||
// The choice of all-opaque vs mixed encoding cuts code size when many
|
||||
// pixels are opaque (typical for sprite interiors).
|
||||
static uint16_t emitMergeByteToD16An(uint8_t *out, uint16_t cursor,
|
||||
uint8_t an, uint8_t disp,
|
||||
uint8_t maskByte, uint8_t srcByte)
|
||||
{
|
||||
if (maskByte == 0u) {
|
||||
return cursor; /* nothing to write */
|
||||
}
|
||||
if (maskByte == 0xFFu) {
|
||||
/* All-opaque shortcut: move.b #src, d16(an). */
|
||||
cursor += writeBE16(out + cursor, kMoveBImmToD16An[an]);
|
||||
cursor += writeBE16(out + cursor, (uint16_t)srcByte);
|
||||
cursor += writeBE16(out + cursor, (uint16_t)disp);
|
||||
return cursor;
|
||||
}
|
||||
/* Mixed: load existing, clear mask bits, OR in src, write back. */
|
||||
cursor += writeBE16(out + cursor, kMoveBD16AnToD0[an]);
|
||||
cursor += writeBE16(out + cursor, (uint16_t)disp);
|
||||
cursor += writeBE16(out + cursor, ANDI_B_IMM_D0);
|
||||
cursor += writeBE16(out + cursor, (uint16_t)((~maskByte) & 0xFFu));
|
||||
cursor += writeBE16(out + cursor, ORI_B_IMM_D0);
|
||||
cursor += writeBE16(out + cursor, (uint16_t)srcByte);
|
||||
cursor += writeBE16(out + cursor, kMoveBD0ToD16An[an]);
|
||||
cursor += writeBE16(out + cursor, (uint16_t)disp);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
|
||||
// ----- Public API -----
|
||||
|
||||
/* Phase 11 (shared-walker): the Amiga port no longer emits per-cel
|
||||
* unrolled draw/save/restore code. Each cel was ~17-23 KB compiled --
|
||||
* 21 cels burned ~340 KB of arena. The cross-platform dispatcher in
|
||||
* src/core/sprite.c routes through halSpriteXxxPlanes in src/port/amiga
|
||||
* /hal.c when sp->slot is NULL, and those HAL hooks are now byte-wide
|
||||
* walkers that read sp->tileData on the fly. Same per-byte work, just
|
||||
* shared as one static lib routine instead of 21 inlined copies.
|
||||
*
|
||||
* Returning 0 here tells emitTotalSize() the emitter has no bytes for
|
||||
* this op, so jlSpriteCompile takes the "totalSize == 0 -> bail" path
|
||||
* and sp->slot stays NULL. */
|
||||
uint16_t spriteEmitDrawPlanar68kOld(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
||||
uint16_t cursor;
|
||||
uint16_t row;
|
||||
uint16_t col;
|
||||
uint16_t heightPx;
|
||||
uint16_t widthTiles;
|
||||
uint16_t bytesPerRow; /* per plane, per row */
|
||||
uint8_t planeBytes[AMIGA_BITPLANES];
|
||||
uint8_t maskByte;
|
||||
uint8_t i;
|
||||
|
||||
if (shift > 7u) {
|
||||
return 0u;
|
||||
}
|
||||
|
||||
cursor = 0;
|
||||
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
|
||||
widthTiles = (uint16_t)sp->widthTiles;
|
||||
bytesPerRow = (uint16_t)(widthTiles + (shift == 0u ? 0u : 1u));
|
||||
|
||||
/* Prologue: m68k cdecl callee-saves a2-a6; we clobber a2 and a3
|
||||
* loading plane pointers, so push them first. After the push, all
|
||||
* stack arg displacements shift by +8 (two longs). */
|
||||
cursor += writeBE16(out + cursor, MOVEM_L_PUSH_OPCODE);
|
||||
cursor += writeBE16(out + cursor, MOVEM_L_MASK_A2_A3);
|
||||
for (i = 0; i < AMIGA_BITPLANES; i++) {
|
||||
cursor += writeBE16(out + cursor, kMoveaSpToAn[i]);
|
||||
cursor += writeBE16(out + cursor, (uint16_t)(8u + 4u + i * 4u));
|
||||
}
|
||||
|
||||
for (row = 0; row < heightPx; row++) {
|
||||
for (col = 0; col < bytesPerRow; col++) {
|
||||
if (shift == 0u) {
|
||||
planeByteAndMaskAt(sp, row, col, planeBytes, &maskByte);
|
||||
} else {
|
||||
planeByteAndMaskShifted(sp, row, col, shift, widthTiles, planeBytes, &maskByte);
|
||||
}
|
||||
for (i = 0; i < AMIGA_BITPLANES; i++) {
|
||||
cursor = emitMergeByteToD16An(out, cursor, i, (uint8_t)col,
|
||||
maskByte, planeBytes[i]);
|
||||
}
|
||||
}
|
||||
if (row + 1u < heightPx) {
|
||||
for (i = 0; i < AMIGA_BITPLANES; i++) {
|
||||
cursor += writeBE16(out + cursor, kAddaWImmToAn[i]);
|
||||
cursor += writeBE16(out + cursor, (uint16_t)AMIGA_BYTES_PER_ROW);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Epilogue: restore a2-a3, rts. */
|
||||
cursor += writeBE16(out + cursor, MOVEM_L_POP_OPCODE);
|
||||
cursor += writeBE16(out + cursor, MOVEM_L_MASK_POP_A2_A3);
|
||||
cursor += writeBE16(out + cursor, OPCODE_RTS);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
|
||||
// SAVE: planes -> backup. backup is one contiguous 4*H*W/8 byte buffer
|
||||
// laid out as 4 plane stripes, matching halSpriteSavePlanes format
|
||||
// (so cross-platform save buffer is interchangeable).
|
||||
//
|
||||
// Per row: for each plane, copy bytesPerRow bytes from d16(an) to
|
||||
// (a4)+. After the row's reads, the planes need to advance by 40,
|
||||
// while a4 advances naturally via post-increment.
|
||||
//
|
||||
// Plane stripes are sequential in backup. We could either (a) do all
|
||||
// rows of plane 0, then plane 1, etc. (matches halSpriteSavePlanes
|
||||
// layout), or (b) interleave rows of all 4 planes (different layout).
|
||||
// halSpriteSavePlanes does (a) -- 4 separate plane stripes. The
|
||||
// emitted code below matches that layout for compat.
|
||||
uint16_t spriteEmitSavePlanar68kOld(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
||||
uint16_t cursor;
|
||||
uint16_t row;
|
||||
uint16_t col;
|
||||
uint16_t heightPx;
|
||||
uint16_t bytesPerRow;
|
||||
uint8_t i;
|
||||
|
||||
/* Shifts 2..7 reuse shift 1's bytes (identical memcpy). The
|
||||
* jlSpriteCompile post-emit pass aliases their routineOffsets to
|
||||
* slot 1 so this routine is emitted once. */
|
||||
if (shift > 1u) {
|
||||
return 0u;
|
||||
}
|
||||
|
||||
cursor = 0;
|
||||
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
|
||||
bytesPerRow = (uint16_t)(sp->widthTiles + (shift == 0u ? 0u : 1u));
|
||||
|
||||
/* Prologue: callee-save a2/a3/a4 (m68k cdecl), then load 4 plane
|
||||
* pointers + backup pointer. After the push, all stack arg disps
|
||||
* shift by +12 (three longs). */
|
||||
cursor += writeBE16(out + cursor, MOVEM_L_PUSH_OPCODE);
|
||||
cursor += writeBE16(out + cursor, MOVEM_L_MASK_A2_A3_A4);
|
||||
for (i = 0; i < AMIGA_BITPLANES; i++) {
|
||||
cursor += writeBE16(out + cursor, kMoveaSpToAn[i]);
|
||||
cursor += writeBE16(out + cursor, (uint16_t)(12u + 4u + i * 4u));
|
||||
}
|
||||
/* a4 = backup. */
|
||||
cursor += writeBE16(out + cursor, kMoveaSpToAn[4]);
|
||||
cursor += writeBE16(out + cursor, (uint16_t)(12u + 4u + 4u * 4u));
|
||||
|
||||
/* Plane-major: for each plane, walk all rows. After this routine,
|
||||
* each An has advanced by H*40 (one frame full); we don't need to
|
||||
* unwind because the function returns. We DO need to reset An
|
||||
* back to start before walking the NEXT plane though.
|
||||
*
|
||||
* Simpler alternative: row-major (interleaved). Per row, copy
|
||||
* bytesPerRow bytes from each plane to (a4)+, then advance all
|
||||
* 4 planes by 40. Net: a4 advances by 4*H*bytesPerRow; planes
|
||||
* advance by H*40. Backup layout becomes interleaved (plane0_row0,
|
||||
* plane1_row0, plane2_row0, plane3_row0, plane0_row1, ...).
|
||||
*
|
||||
* That doesn't match halSpriteSavePlanes' plane-major layout. Need
|
||||
* to either (a) match it -- emit per-plane outer loop with a4
|
||||
* stride between planes -- or (b) change halSpriteSavePlanes to
|
||||
* interleaved. Picking (b) is simpler in emitted code, but ALSO
|
||||
* requires updating halSpriteRestorePlanes and halSpriteRestoreUnder
|
||||
* fallback math.
|
||||
*
|
||||
* For now: use plane-major matching halSpriteSavePlanes. Per
|
||||
* plane: walk rows, copy bytes from d16(an) to (a4)+, advance an
|
||||
* by 40 after each row except the last; reset an back to start
|
||||
* before next plane. */
|
||||
for (i = 0; i < AMIGA_BITPLANES; i++) {
|
||||
for (row = 0; row < heightPx; row++) {
|
||||
for (col = 0; col < bytesPerRow; col++) {
|
||||
cursor += writeBE16(out + cursor, kMoveBD16AnToA4Postinc[i]);
|
||||
cursor += writeBE16(out + cursor, (uint16_t)col);
|
||||
}
|
||||
if (row + 1u < heightPx) {
|
||||
cursor += writeBE16(out + cursor, kAddaWImmToAn[i]);
|
||||
cursor += writeBE16(out + cursor, (uint16_t)AMIGA_BYTES_PER_ROW);
|
||||
}
|
||||
}
|
||||
/* Reset An back to the plane base for next iteration. The
|
||||
* total advance was (heightPx - 1) * 40. Subtract that. */
|
||||
if (i + 1u < AMIGA_BITPLANES) {
|
||||
cursor += writeBE16(out + cursor, kAddaWImmToAn[i]);
|
||||
cursor += writeBE16(out + cursor, (uint16_t)(0u - ((heightPx - 1u) * AMIGA_BYTES_PER_ROW)));
|
||||
}
|
||||
}
|
||||
|
||||
cursor += writeBE16(out + cursor, MOVEM_L_POP_OPCODE);
|
||||
cursor += writeBE16(out + cursor, MOVEM_L_MASK_POP_A2_A3_A4);
|
||||
cursor += writeBE16(out + cursor, OPCODE_RTS);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
|
||||
// RESTORE: backup -> planes. Mirror of save. Uses MOVE.B (a4)+, d16(an).
|
||||
uint16_t spriteEmitRestorePlanar68kOld(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
||||
uint16_t cursor;
|
||||
uint16_t row;
|
||||
uint16_t col;
|
||||
uint16_t heightPx;
|
||||
uint16_t bytesPerRow;
|
||||
uint8_t i;
|
||||
|
||||
if (shift > 1u) {
|
||||
return 0u;
|
||||
}
|
||||
|
||||
cursor = 0;
|
||||
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
|
||||
bytesPerRow = (uint16_t)(sp->widthTiles + (shift == 0u ? 0u : 1u));
|
||||
|
||||
/* Callee-save a2/a3/a4; arg disps shift by +12. */
|
||||
cursor += writeBE16(out + cursor, MOVEM_L_PUSH_OPCODE);
|
||||
cursor += writeBE16(out + cursor, MOVEM_L_MASK_A2_A3_A4);
|
||||
for (i = 0; i < AMIGA_BITPLANES; i++) {
|
||||
cursor += writeBE16(out + cursor, kMoveaSpToAn[i]);
|
||||
cursor += writeBE16(out + cursor, (uint16_t)(12u + 4u + i * 4u));
|
||||
}
|
||||
cursor += writeBE16(out + cursor, kMoveaSpToAn[4]);
|
||||
cursor += writeBE16(out + cursor, (uint16_t)(12u + 4u + 4u * 4u));
|
||||
|
||||
for (i = 0; i < AMIGA_BITPLANES; i++) {
|
||||
for (row = 0; row < heightPx; row++) {
|
||||
for (col = 0; col < bytesPerRow; col++) {
|
||||
cursor += writeBE16(out + cursor, kMoveBA4PostincToD16An[i]);
|
||||
cursor += writeBE16(out + cursor, (uint16_t)col);
|
||||
}
|
||||
if (row + 1u < heightPx) {
|
||||
cursor += writeBE16(out + cursor, kAddaWImmToAn[i]);
|
||||
cursor += writeBE16(out + cursor, (uint16_t)AMIGA_BYTES_PER_ROW);
|
||||
}
|
||||
}
|
||||
if (i + 1u < AMIGA_BITPLANES) {
|
||||
cursor += writeBE16(out + cursor, kAddaWImmToAn[i]);
|
||||
cursor += writeBE16(out + cursor, (uint16_t)(0u - ((heightPx - 1u) * AMIGA_BYTES_PER_ROW)));
|
||||
}
|
||||
}
|
||||
|
||||
cursor += writeBE16(out + cursor, MOVEM_L_POP_OPCODE);
|
||||
cursor += writeBE16(out + cursor, MOVEM_L_MASK_POP_A2_A3_A4);
|
||||
cursor += writeBE16(out + cursor, OPCODE_RTS);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
// ---- Phase 11 entry points: zero-byte stubs that force the dispatcher
|
||||
// ---- to the shared walker in halSpriteDrawPlanes / halSpriteSavePlanes
|
||||
// ---- / halSpriteRestorePlanes. The old emit-unrolled-bytes path is
|
||||
// ---- kept above (renamed *Old) for reference until the walker is
|
||||
// ---- thoroughly battle-tested.
|
||||
|
||||
uint16_t spriteEmitDrawPlanar68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
||||
(void)out; (void)sp; (void)shift;
|
||||
uint32_t spriteEmitDrawPlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
|
||||
(void)out; (void)cap; (void)sp; (void)shift;
|
||||
return 0u;
|
||||
}
|
||||
|
||||
|
||||
uint16_t spriteEmitSavePlanar68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
||||
(void)out; (void)sp; (void)shift;
|
||||
uint32_t spriteEmitSavePlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
|
||||
(void)out; (void)cap; (void)sp; (void)shift;
|
||||
return 0u;
|
||||
}
|
||||
|
||||
|
||||
uint16_t spriteEmitRestorePlanar68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
||||
(void)out; (void)sp; (void)shift;
|
||||
uint32_t spriteEmitRestorePlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
|
||||
(void)out; (void)cap; (void)sp; (void)shift;
|
||||
return 0u;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,17 +38,35 @@
|
|||
#define TILE_BYTES_PER_ROW 4
|
||||
#define TRANSPARENT_NIBBLE 0
|
||||
|
||||
// Worst-case bytes per emitted routine, used to size the scratch
|
||||
// buffer. A 32x32 sprite is 16 rows * (16 dest bytes + 1 for shift1)
|
||||
// = 272 dest-byte slots, each up to 10 bytes mixed = 2720; plus
|
||||
// per-row prologues 32*6=192; plus prologue/epilogue 8. Round up
|
||||
// generously.
|
||||
#define MAX_ROUTINE_BYTES 8192
|
||||
// Stack-arg displacements for the emitted cdecl routines, after the
|
||||
// prologue saves esi (+esi/edi for save/restore):
|
||||
// draw(dst) -> dst at [esp+8] (1 push)
|
||||
// save/restore(src, dst) -> src at [esp+12], dst at [esp+16] (2 pushes)
|
||||
#define DRAW_ARG_DST_DISP 0x08u
|
||||
#define COPY_ARG_SRC_DISP 0x0Cu
|
||||
#define COPY_ARG_DST_DISP 0x10u
|
||||
|
||||
// Largest disp8 [esi+disp8] can encode as a non-negative offset. col
|
||||
// values above this need the disp32 ModRM form (sign-extended disp8
|
||||
// would address a negative offset).
|
||||
#define DISP8_MAX 127u
|
||||
|
||||
// Worst-case emitted bytes for one draw dest byte. The mixed RMW path
|
||||
// is mov al,[esi+d]; and al; or al; mov [esi+d],al = 3+2+2+3 = 10 with
|
||||
// disp8; the disp32 form adds 3 bytes per [esi+disp32] operand (two of
|
||||
// them), so up to 16 for a wide mixed byte.
|
||||
#define DRAW_MAX_BYTES_PER_DESTBYTE 16u
|
||||
// Bytes for the per-row `add esi, imm32` advance.
|
||||
#define ROW_STRIDE_ADVANCE_BYTES 6u
|
||||
// Bytes for the `pop esi[/edi]; ret` epilogue (worst case save/restore).
|
||||
#define EPILOGUE_BYTES 3u
|
||||
|
||||
|
||||
// ----- Prototypes -----
|
||||
|
||||
static uint16_t emitCopyBodyX86(uint8_t *out, uint16_t cursor, uint16_t heightPx, uint16_t copyBytes, bool strideOnSrc);
|
||||
static uint32_t emitCopyBodyX86(uint8_t *out, uint32_t cap, uint32_t cursor, uint16_t heightPx, uint16_t copyBytes, bool strideOnSrc);
|
||||
static uint32_t emitEsiOperand(uint8_t *out, uint32_t cursor, uint16_t col);
|
||||
static uint32_t esiOperandLen(uint16_t col);
|
||||
static void shiftedByteAt(const jlSpriteT *sp, uint16_t row, uint16_t col, uint8_t shift, uint16_t spriteBytesPerRow, uint8_t *outValue, uint8_t *outOpaqueMask);
|
||||
static uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col);
|
||||
|
||||
|
|
@ -65,7 +83,7 @@ static uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col
|
|||
//
|
||||
// strideOnSrc=true -> source has the screen stride (SAVE)
|
||||
// strideOnSrc=false -> destination has the screen stride (RESTORE)
|
||||
static uint16_t emitCopyBodyX86(uint8_t *out, uint16_t cursor, uint16_t heightPx, uint16_t copyBytes, bool strideOnSrc) {
|
||||
static uint32_t emitCopyBodyX86(uint8_t *out, uint32_t cap, uint32_t cursor, uint16_t heightPx, uint16_t copyBytes, bool strideOnSrc) {
|
||||
uint16_t row;
|
||||
uint16_t dwords;
|
||||
uint16_t tail;
|
||||
|
|
@ -76,6 +94,10 @@ static uint16_t emitCopyBodyX86(uint8_t *out, uint16_t cursor, uint16_t heightPx
|
|||
advance = (int32_t)SURFACE_BYTES_PER_ROW - (int32_t)copyBytes;
|
||||
|
||||
for (row = 0; row < heightPx; row++) {
|
||||
// Worst case per row: rep movsd (7) + rep movsb (7) + add (6).
|
||||
if (cursor + 7u + 7u + 6u > cap) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
if (dwords > 0) {
|
||||
// mov ecx, dwords (B9 imm32); rep movsd (F3 A5)
|
||||
out[cursor++] = 0xB9;
|
||||
|
|
@ -111,6 +133,34 @@ static uint16_t emitCopyBodyX86(uint8_t *out, uint16_t cursor, uint16_t heightPx
|
|||
}
|
||||
|
||||
|
||||
// Emit the `[esi+col]` ModRM + displacement that follows an opcode in
|
||||
// the draw stores. reg field is 000 (al / opcode-extension 0), so the
|
||||
// ModRM is 0x46 for the disp8 form (col <= 127) and 0x86 for the disp32
|
||||
// form. disp8 is sign-extended by the CPU, so any col in 128..255 would
|
||||
// address a NEGATIVE offset and corrupt pixels; wide sprites
|
||||
// (widthTiles > 31) hit this, so emit the disp32 form there. Returns
|
||||
// the new cursor.
|
||||
static uint32_t emitEsiOperand(uint8_t *out, uint32_t cursor, uint16_t col) {
|
||||
if (col <= DISP8_MAX) {
|
||||
out[cursor++] = 0x46; // ModRM mod=01 [esi+disp8]
|
||||
out[cursor++] = (uint8_t)(col & 0xFFu);
|
||||
} else {
|
||||
out[cursor++] = 0x86; // ModRM mod=10 [esi+disp32]
|
||||
out[cursor++] = (uint8_t)(col & 0xFFu);
|
||||
out[cursor++] = (uint8_t)((col >> 8) & 0xFFu);
|
||||
out[cursor++] = 0x00;
|
||||
out[cursor++] = 0x00;
|
||||
}
|
||||
return cursor;
|
||||
}
|
||||
|
||||
|
||||
// Bytes emitEsiOperand will write for `col` (ModRM + disp).
|
||||
static uint32_t esiOperandLen(uint16_t col) {
|
||||
return (col <= DISP8_MAX) ? 2u : 5u;
|
||||
}
|
||||
|
||||
|
||||
// Decompose a destination byte's contribution from the sprite into
|
||||
// (value, opaqueMask) for shift in {0, 1}. opaqueMask high nibble
|
||||
// 0xF0 means high dest nibble is opaque; 0x0F means low is opaque;
|
||||
|
|
@ -182,10 +232,11 @@ static uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col)
|
|||
}
|
||||
|
||||
|
||||
// Emit a draw routine for one shift variant. Returns bytes written.
|
||||
// Emit a draw routine for one shift variant. Returns bytes written, or
|
||||
// SPRITE_EMIT_OVERFLOW if the routine would exceed `cap`.
|
||||
// Routine signature: void f(uint8_t *dst).
|
||||
uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
||||
uint16_t cursor;
|
||||
uint32_t spriteEmitDrawX86(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
|
||||
uint32_t cursor;
|
||||
uint16_t row;
|
||||
uint16_t col;
|
||||
uint16_t runEnd;
|
||||
|
|
@ -207,17 +258,23 @@ uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
|||
cursor = 0;
|
||||
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
|
||||
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW);
|
||||
destBytesPerRow = (uint16_t)(spriteBytesPerRow + (shift == 1 ? 1 : 0));
|
||||
destBytesPerRow = (uint16_t)(spriteBytesPerRow + shift); /* shift is 0 or 1 */
|
||||
|
||||
// Prologue: push esi; mov esi, [esp+8]
|
||||
if (cursor + 5u > cap) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
out[cursor++] = 0x56; // push esi
|
||||
out[cursor++] = 0x8B; out[cursor++] = 0x74;
|
||||
out[cursor++] = 0x24; out[cursor++] = 0x08;
|
||||
out[cursor++] = 0x24; out[cursor++] = DRAW_ARG_DST_DISP;
|
||||
|
||||
// Body: per row, scan dest bytes coalescing fully-opaque runs.
|
||||
for (row = 0; row < heightPx; row++) {
|
||||
if (row > 0) {
|
||||
// add esi, SURFACE_BYTES_PER_ROW (32-bit imm)
|
||||
if (cursor + ROW_STRIDE_ADVANCE_BYTES > cap) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
out[cursor++] = 0x81; out[cursor++] = 0xC6;
|
||||
out[cursor++] = (uint8_t)(SURFACE_BYTES_PER_ROW & 0xFFu);
|
||||
out[cursor++] = (uint8_t)((SURFACE_BYTES_PER_ROW >> 8) & 0xFFu);
|
||||
|
|
@ -231,20 +288,25 @@ uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
|||
col++;
|
||||
continue;
|
||||
}
|
||||
// Reserve the worst case for one dest byte (wide mixed RMW
|
||||
// = 16 bytes) plus the epilogue.
|
||||
if (cursor + DRAW_MAX_BYTES_PER_DESTBYTE + EPILOGUE_BYTES > cap) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
if (opaqueMask != 0xFFu) {
|
||||
// Mixed: read-modify-write.
|
||||
// mov al, [esi+col] (8A 46 cc)
|
||||
// mov al, [esi+col] (8A /r)
|
||||
// and al, ~opaqueMask (24 mm)
|
||||
// or al, value (0C vv)
|
||||
// mov [esi+col], al (88 46 cc)
|
||||
out[cursor++] = 0x8A; out[cursor++] = 0x46;
|
||||
out[cursor++] = (uint8_t)(col & 0xFFu);
|
||||
// mov [esi+col], al (88 /r)
|
||||
out[cursor++] = 0x8A;
|
||||
cursor = emitEsiOperand(out, cursor, col);
|
||||
out[cursor++] = 0x24;
|
||||
out[cursor++] = (uint8_t)(~opaqueMask & 0xFFu);
|
||||
out[cursor++] = 0x0C;
|
||||
out[cursor++] = value;
|
||||
out[cursor++] = 0x88; out[cursor++] = 0x46;
|
||||
out[cursor++] = (uint8_t)(col & 0xFFu);
|
||||
out[cursor++] = 0x88;
|
||||
cursor = emitEsiOperand(out, cursor, col);
|
||||
col++;
|
||||
continue;
|
||||
}
|
||||
|
|
@ -264,12 +326,15 @@ uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
|||
// enough that re-reading per chunk beats threading a
|
||||
// fixed-size buffer through.
|
||||
while (runLen >= 4) {
|
||||
// mov dword [esi+col], imm32 (C7 /0 ... ii ii ii ii)
|
||||
if (cursor + 1u + esiOperandLen(col) + 4u + EPILOGUE_BYTES > cap) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
shiftedByteAt(sp, row, (uint16_t)(col + 1), shift, spriteBytesPerRow, &v1, &m);
|
||||
shiftedByteAt(sp, row, (uint16_t)(col + 2), shift, spriteBytesPerRow, &v2, &m);
|
||||
shiftedByteAt(sp, row, (uint16_t)(col + 3), shift, spriteBytesPerRow, &v3, &m);
|
||||
// mov dword [esi+col], imm32 (C7 46 cc ii ii ii ii)
|
||||
out[cursor++] = 0xC7; out[cursor++] = 0x46;
|
||||
out[cursor++] = (uint8_t)(col & 0xFFu);
|
||||
out[cursor++] = 0xC7;
|
||||
cursor = emitEsiOperand(out, cursor, col);
|
||||
out[cursor++] = value;
|
||||
out[cursor++] = v1;
|
||||
out[cursor++] = v2;
|
||||
|
|
@ -281,11 +346,14 @@ uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
|||
}
|
||||
}
|
||||
if (runLen >= 2) {
|
||||
// mov word [esi+col], imm16 (66 C7 /0 ... ii ii)
|
||||
if (cursor + 2u + esiOperandLen(col) + 2u + EPILOGUE_BYTES > cap) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
shiftedByteAt(sp, row, (uint16_t)(col + 1), shift, spriteBytesPerRow, &v1, &m);
|
||||
// mov word [esi+col], imm16 (66 C7 46 cc ii ii)
|
||||
out[cursor++] = 0x66;
|
||||
out[cursor++] = 0xC7; out[cursor++] = 0x46;
|
||||
out[cursor++] = (uint8_t)(col & 0xFFu);
|
||||
out[cursor++] = 0xC7;
|
||||
cursor = emitEsiOperand(out, cursor, col);
|
||||
out[cursor++] = value;
|
||||
out[cursor++] = v1;
|
||||
col = (uint16_t)(col + 2);
|
||||
|
|
@ -295,9 +363,12 @@ uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
|||
}
|
||||
}
|
||||
if (runLen == 1) {
|
||||
// mov byte [esi+col], imm8 (C6 46 cc ii)
|
||||
out[cursor++] = 0xC6; out[cursor++] = 0x46;
|
||||
out[cursor++] = (uint8_t)(col & 0xFFu);
|
||||
// mov byte [esi+col], imm8 (C6 /0 ... ii)
|
||||
if (cursor + 1u + esiOperandLen(col) + 1u + EPILOGUE_BYTES > cap) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
out[cursor++] = 0xC6;
|
||||
cursor = emitEsiOperand(out, cursor, col);
|
||||
out[cursor++] = value;
|
||||
col++;
|
||||
}
|
||||
|
|
@ -305,6 +376,9 @@ uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
|||
}
|
||||
|
||||
// Epilogue: pop esi; ret
|
||||
if (cursor + 2u > cap) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
out[cursor++] = 0x5E;
|
||||
out[cursor++] = 0xC3;
|
||||
return cursor;
|
||||
|
|
@ -312,8 +386,8 @@ uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
|||
|
||||
|
||||
// RESTORE: copy backup -> screen. Destination has the screen stride.
|
||||
uint16_t spriteEmitRestoreX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
||||
uint16_t cursor;
|
||||
uint32_t spriteEmitRestoreX86(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
|
||||
uint32_t cursor;
|
||||
uint16_t heightPx;
|
||||
uint16_t copyBytes;
|
||||
|
||||
|
|
@ -323,17 +397,26 @@ uint16_t spriteEmitRestoreX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift)
|
|||
|
||||
cursor = 0;
|
||||
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
|
||||
copyBytes = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW + (shift == 1u ? 1u : 0u));
|
||||
copyBytes = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW + shift); /* shift is 0 or 1 */
|
||||
|
||||
// Prologue: push esi; push edi; mov esi,[esp+12]; mov edi,[esp+16]
|
||||
if (cursor + 10u > cap) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
out[cursor++] = 0x56;
|
||||
out[cursor++] = 0x57;
|
||||
out[cursor++] = 0x8B; out[cursor++] = 0x74; out[cursor++] = 0x24; out[cursor++] = 0x0C;
|
||||
out[cursor++] = 0x8B; out[cursor++] = 0x7C; out[cursor++] = 0x24; out[cursor++] = 0x10;
|
||||
out[cursor++] = 0x8B; out[cursor++] = 0x74; out[cursor++] = 0x24; out[cursor++] = COPY_ARG_SRC_DISP;
|
||||
out[cursor++] = 0x8B; out[cursor++] = 0x7C; out[cursor++] = 0x24; out[cursor++] = COPY_ARG_DST_DISP;
|
||||
|
||||
cursor = emitCopyBodyX86(out, cursor, heightPx, copyBytes, false);
|
||||
cursor = emitCopyBodyX86(out, cap, cursor, heightPx, copyBytes, false);
|
||||
if (cursor == SPRITE_EMIT_OVERFLOW) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
|
||||
// Epilogue: pop edi; pop esi; ret
|
||||
if (cursor + 3u > cap) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
out[cursor++] = 0x5F;
|
||||
out[cursor++] = 0x5E;
|
||||
out[cursor++] = 0xC3;
|
||||
|
|
@ -342,8 +425,8 @@ uint16_t spriteEmitRestoreX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift)
|
|||
|
||||
|
||||
// SAVE: copy screen -> backup. Source has the screen stride.
|
||||
uint16_t spriteEmitSaveX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
||||
uint16_t cursor;
|
||||
uint32_t spriteEmitSaveX86(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
|
||||
uint32_t cursor;
|
||||
uint16_t heightPx;
|
||||
uint16_t copyBytes;
|
||||
|
||||
|
|
@ -353,15 +436,24 @@ uint16_t spriteEmitSaveX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
|
|||
|
||||
cursor = 0;
|
||||
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
|
||||
copyBytes = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW + (shift == 1u ? 1u : 0u));
|
||||
copyBytes = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW + shift); /* shift is 0 or 1 */
|
||||
|
||||
if (cursor + 10u > cap) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
out[cursor++] = 0x56;
|
||||
out[cursor++] = 0x57;
|
||||
out[cursor++] = 0x8B; out[cursor++] = 0x74; out[cursor++] = 0x24; out[cursor++] = 0x0C;
|
||||
out[cursor++] = 0x8B; out[cursor++] = 0x7C; out[cursor++] = 0x24; out[cursor++] = 0x10;
|
||||
out[cursor++] = 0x8B; out[cursor++] = 0x74; out[cursor++] = 0x24; out[cursor++] = COPY_ARG_SRC_DISP;
|
||||
out[cursor++] = 0x8B; out[cursor++] = 0x7C; out[cursor++] = 0x24; out[cursor++] = COPY_ARG_DST_DISP;
|
||||
|
||||
cursor = emitCopyBodyX86(out, cursor, heightPx, copyBytes, true);
|
||||
cursor = emitCopyBodyX86(out, cap, cursor, heightPx, copyBytes, true);
|
||||
if (cursor == SPRITE_EMIT_OVERFLOW) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
|
||||
if (cursor + 3u > cap) {
|
||||
return SPRITE_EMIT_OVERFLOW;
|
||||
}
|
||||
out[cursor++] = 0x5F;
|
||||
out[cursor++] = 0x5E;
|
||||
out[cursor++] = 0xC3;
|
||||
|
|
|
|||
|
|
@ -8,12 +8,25 @@
|
|||
// plane bytes at draw time -- but the entry points are kept so the
|
||||
// dispatcher API stays uniform across ports.
|
||||
//
|
||||
// Each emit function takes the sprite + shift variant and writes
|
||||
// position-independent draw-routine bytes into `out`. Returns the
|
||||
// number of bytes written. The output bytes follow the target CPU's
|
||||
// calling convention as documented in the per-CPU file's header
|
||||
// comment, so the bytes can be called via a C function pointer cast
|
||||
// once placed in executable memory.
|
||||
// Each emit function takes the sprite + shift variant and a capacity
|
||||
// `cap` (bytes available at `out`), and writes position-independent
|
||||
// draw-routine bytes into `out`. It returns the number of bytes
|
||||
// written (uint32_t -- a single pathological large routine can exceed
|
||||
// 64 KB, so a 16-bit count would truncate and mis-size the arena
|
||||
// slot). The output bytes follow the target CPU's calling convention
|
||||
// as documented in the per-CPU file's header comment, so the bytes
|
||||
// can be called via a C function pointer cast once placed in
|
||||
// executable memory.
|
||||
//
|
||||
// Return-value contract:
|
||||
// 0 -- op not implemented for this (shift, op)
|
||||
// on this platform; dispatcher falls back
|
||||
// to the interpreted path.
|
||||
// SPRITE_EMIT_OVERFLOW -- the routine would not fit in `cap`. The
|
||||
// sizing pass and the real emit pass both
|
||||
// propagate this and jlSpriteCompile bails
|
||||
// cleanly (no arena alloc, sp->slot NULL).
|
||||
// any other value -- bytes written.
|
||||
|
||||
#ifndef JOEYLIB_SPRITE_EMITTER_H
|
||||
#define JOEYLIB_SPRITE_EMITTER_H
|
||||
|
|
@ -21,9 +34,35 @@
|
|||
#include "joey/sprite.h"
|
||||
#include "spriteInternal.h"
|
||||
|
||||
uint16_t spriteEmitDrawX86 (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
|
||||
uint16_t spriteEmitDraw68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
|
||||
uint16_t spriteEmitDrawIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift);
|
||||
// Overflow sentinel returned by an emitter whose output would exceed
|
||||
// the supplied capacity. Distinct from a real byte count and from the
|
||||
// 0 "not implemented" return.
|
||||
#define SPRITE_EMIT_OVERFLOW 0xFFFFFFFFul
|
||||
|
||||
// Worst-case emitted bytes per destination byte, per target. Used to
|
||||
// size the emit scratch and to derive the create-time dimension cap so
|
||||
// the scratch, the cap, and the per-emitter bound all share one fact:
|
||||
// 68k mixed RMW : move.b d16(a0),d0; andi.b; ori.b; move.b d0,d16(a0)
|
||||
// = 4 instructions * 4 bytes = 16
|
||||
// x86 mixed RMW : mov al,[esi+d]; and al; or al; mov [esi+d],al = 10
|
||||
// IIgs mixed RMW: lda abs,Y; and #; ora #; sta abs,Y = 9
|
||||
#define SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_68K 16u
|
||||
#define SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_X86 10u
|
||||
#define SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_IIGS 9u
|
||||
|
||||
#if defined(JOEYLIB_PLATFORM_DOS)
|
||||
#define SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_X86
|
||||
#elif defined(JOEYLIB_PLATFORM_IIGS)
|
||||
#define SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_IIGS
|
||||
#else
|
||||
// 68k chunky (unused) / planar / interleaved walkers emit 0 bytes;
|
||||
// the 68k chunky worst case is the safe upper bound for sizing.
|
||||
#define SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_68K
|
||||
#endif
|
||||
|
||||
uint32_t spriteEmitDrawX86 (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
|
||||
uint32_t spriteEmitDraw68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
|
||||
uint32_t spriteEmitDrawIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
|
||||
|
||||
// Save-under and restore-under emitters. Both copy a byte-aligned
|
||||
// rectangle between the destination surface and a backup buffer. The
|
||||
|
|
@ -37,12 +76,12 @@ uint16_t spriteEmitDrawIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift);
|
|||
// 68k use a plain cdecl `void copy(const uint8_t *src, uint8_t *dst)`
|
||||
// where the caller swaps args between SAVE (screen->backup) and
|
||||
// RESTORE (backup->screen).
|
||||
uint16_t spriteEmitSaveIigs (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
|
||||
uint16_t spriteEmitRestoreIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift);
|
||||
uint16_t spriteEmitSaveX86 (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
|
||||
uint16_t spriteEmitRestoreX86 (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
|
||||
uint16_t spriteEmitSave68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
|
||||
uint16_t spriteEmitRestore68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
|
||||
uint32_t spriteEmitSaveIigs (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
|
||||
uint32_t spriteEmitRestoreIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
|
||||
uint32_t spriteEmitSaveX86 (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
|
||||
uint32_t spriteEmitRestoreX86 (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
|
||||
uint32_t spriteEmitSave68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
|
||||
uint32_t spriteEmitRestore68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
|
||||
|
||||
// Planar 68k emitters (Amiga). Distinct from the chunky 68k emitters
|
||||
// above because the destination addressing is across 4 separate
|
||||
|
|
@ -55,9 +94,9 @@ uint16_t spriteEmitRestore68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift)
|
|||
// dispatcher). Returns 0 for shifts not yet implemented (today only
|
||||
// shift 0 == byte-aligned x is emitted; shifts 1..7 fall back to the
|
||||
// cross-platform interpreter).
|
||||
uint16_t spriteEmitDrawPlanar68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
|
||||
uint16_t spriteEmitSavePlanar68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
|
||||
uint16_t spriteEmitRestorePlanar68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
|
||||
uint32_t spriteEmitDrawPlanar68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
|
||||
uint32_t spriteEmitSavePlanar68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
|
||||
uint32_t spriteEmitRestorePlanar68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
|
||||
|
||||
// Word-interleaved planar 68k emitter (ST). Calling convention for
|
||||
// the emitted bytes:
|
||||
|
|
@ -66,8 +105,8 @@ uint16_t spriteEmitRestorePlanar68k (uint8_t *out, const jlSpriteT *sp, uint8_t
|
|||
// real bytes (x mod 16 == 0 for shift 0, x mod 16 == 8 for shift 1);
|
||||
// other shifts return 0 so the cross-platform dispatcher falls back
|
||||
// to halSpriteDrawPlanes.
|
||||
uint16_t spriteEmitDrawInterleaved68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
|
||||
uint16_t spriteEmitSaveInterleaved68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
|
||||
uint16_t spriteEmitRestoreInterleaved68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
|
||||
uint32_t spriteEmitDrawInterleaved68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
|
||||
uint32_t spriteEmitSaveInterleaved68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
|
||||
uint32_t spriteEmitRestoreInterleaved68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -26,13 +26,12 @@
|
|||
#include "joey/tile.h"
|
||||
#include "spriteInternal.h"
|
||||
|
||||
// Push the file-IO out of the ORCA-C _ROOT segment on the IIgs. Every
|
||||
// Push the file-IO out of the root segment on the IIgs. Every
|
||||
// fopen / fread / fclose call drags a chunk of the stdio cluster
|
||||
// in; concentrating them in one named segment keeps the root bank
|
||||
// from overflowing past 64 KB (the failure mode is the linker's
|
||||
// "Code exceeds code bank size" / "Address is not in current bank"
|
||||
// errors documented in the feedback memory).
|
||||
JOEYLIB_SEGMENT("ASSETLD")
|
||||
|
||||
// ----- Constants -----
|
||||
|
||||
|
|
@ -41,6 +40,14 @@ JOEYLIB_SEGMENT("ASSETLD")
|
|||
#define ASSET_PALETTE_ENTRIES 16
|
||||
#define ASSET_TILE_DATA_BYTES 32 // 4bpp packed: 4 bytes/row * 8 rows
|
||||
|
||||
// Largest legitimately drawable sprite cel: the surface is 40x25 tiles
|
||||
// (320/8 x 200/8), so any cel larger than that can never be fully
|
||||
// on-screen. Reject crafted/truncated headers claiming more, which
|
||||
// would otherwise force an unbounded per-cel malloc (DoS) and feed the
|
||||
// downstream emitters/draw walkers out-of-range dimensions.
|
||||
#define ASSET_MAX_TILES_W 40
|
||||
#define ASSET_MAX_TILES_H 25
|
||||
|
||||
// Per-platform expected target byte for .tbk files. .spr files carry
|
||||
// target=0 (cross-platform chunky) and pass on every build.
|
||||
#if defined(JOEYLIB_PLATFORM_AMIGA)
|
||||
|
|
@ -57,10 +64,46 @@ JOEYLIB_SEGMENT("ASSETLD")
|
|||
|
||||
// ----- Prototypes -----
|
||||
|
||||
static bool openReadValidateHeader(const char *path, const char *magic, uint8_t expectedTarget, uint8_t *header, uint16_t *outPalette, FILE **outFp);
|
||||
static void readPaletteFromHeader(const uint8_t *header, uint16_t *outPalette);
|
||||
|
||||
// ----- Internal helpers (alphabetical) -----
|
||||
|
||||
// Open `path`, read the shared 44-byte header, and validate the magic
|
||||
// + target byte common to both .spr and .tbk. On success leaves *outFp
|
||||
// open (caller must fclose) and returns true; on any failure closes
|
||||
// the file (if opened) and returns false. When outPalette is non-NULL
|
||||
// and the header advertises a palette (header[5] != 0), the palette is
|
||||
// decoded into outPalette. Shared by jlSpriteBankLoad and
|
||||
// jlTileBankLoad so the open/read/validate/parse-palette boilerplate
|
||||
// (and its fclose-on-error pattern) lives in one place.
|
||||
static bool openReadValidateHeader(const char *path, const char *magic, uint8_t expectedTarget, uint8_t *header, uint16_t *outPalette, FILE **outFp) {
|
||||
FILE *fp;
|
||||
|
||||
fp = fopen(path, "rb");
|
||||
if (fp == NULL) {
|
||||
return false;
|
||||
}
|
||||
if (fread(header, 1, ASSET_HEADER_BYTES, fp) != ASSET_HEADER_BYTES) {
|
||||
fclose(fp);
|
||||
return false;
|
||||
}
|
||||
if (memcmp(header, magic, 4) != 0) {
|
||||
fclose(fp);
|
||||
return false;
|
||||
}
|
||||
if (header[4] != expectedTarget) {
|
||||
fclose(fp);
|
||||
return false;
|
||||
}
|
||||
if (outPalette != NULL && header[5] != 0u) {
|
||||
readPaletteFromHeader(header, outPalette);
|
||||
}
|
||||
*outFp = fp;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static void readPaletteFromHeader(const uint8_t *header, uint16_t *outPalette) {
|
||||
uint8_t i;
|
||||
|
||||
|
|
@ -72,6 +115,10 @@ static void readPaletteFromHeader(const uint8_t *header, uint16_t *outPalette) {
|
|||
|
||||
// ----- Public API (alphabetical) -----
|
||||
|
||||
// Returns the number of cels successfully loaded into outCels[0..N).
|
||||
// A return value strictly less than min(headerCellCount, maxCels)
|
||||
// indicates the file was truncated/corrupt mid-stream relative to its
|
||||
// own header count; the cels that were read are still valid.
|
||||
uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels, uint16_t maxCels,
|
||||
uint16_t *outPalette) {
|
||||
FILE *fp;
|
||||
|
|
@ -87,26 +134,24 @@ uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels, uint16_t maxCel
|
|||
if (path == NULL || outCels == NULL || maxCels == 0u) {
|
||||
return 0u;
|
||||
}
|
||||
fp = fopen(path, "rb");
|
||||
if (fp == NULL) {
|
||||
return 0u;
|
||||
}
|
||||
if (fread(header, 1, ASSET_HEADER_BYTES, fp) != ASSET_HEADER_BYTES) {
|
||||
fclose(fp);
|
||||
return 0u;
|
||||
}
|
||||
// Magic "JSP1" + target byte = 0 (cross-platform chunky). Anything
|
||||
// else means the file was either baked wrong or is a stale .spr
|
||||
// from the old compiled-codegen pipeline.
|
||||
if (memcmp(header, "JSP1", 4) != 0 || header[4] != 0u) {
|
||||
fclose(fp);
|
||||
if (!openReadValidateHeader(path, "JSP1", 0u, header, outPalette, &fp)) {
|
||||
return 0u;
|
||||
}
|
||||
widthTiles = header[6];
|
||||
heightTiles = header[7];
|
||||
cellCount = (uint16_t)(header[8] | (header[9] << 8));
|
||||
if (outPalette != NULL && header[5] != 0u) {
|
||||
readPaletteFromHeader(header, outPalette);
|
||||
|
||||
// Reject degenerate (0 in either dimension -> tileBytes 0, an
|
||||
// implementation-defined malloc(0)) and oversized (a crafted header
|
||||
// could force a multi-MB per-cel malloc) sprites. Matches
|
||||
// jlSpriteCreate's invariant and the 40x25-tile surface ceiling.
|
||||
if (widthTiles == 0u || heightTiles == 0u ||
|
||||
widthTiles > ASSET_MAX_TILES_W || heightTiles > ASSET_MAX_TILES_H) {
|
||||
fclose(fp);
|
||||
return 0u;
|
||||
}
|
||||
|
||||
tileBytes = (uint32_t)widthTiles * (uint32_t)heightTiles * (uint32_t)ASSET_TILE_DATA_BYTES;
|
||||
|
|
@ -135,7 +180,9 @@ uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels, uint16_t maxCel
|
|||
sp->heightTiles = heightTiles;
|
||||
sp->ownsTileData = true;
|
||||
sp->slot = NULL;
|
||||
memset(sp->routineOffsets, 0, sizeof(sp->routineOffsets));
|
||||
// 0xFF == SPRITE_NOT_COMPILED for every routine offset (0 is a
|
||||
// valid offset, so zero-fill would alias a real entry).
|
||||
memset(sp->routineOffsets, 0xFF, sizeof(sp->routineOffsets));
|
||||
memset(sp->cachedDstBank, 0xFF, sizeof(sp->cachedDstBank));
|
||||
memset(sp->cachedSrcBank, 0xFF, sizeof(sp->cachedSrcBank));
|
||||
memset(sp->cachedSizeBytes, 0, sizeof(sp->cachedSizeBytes));
|
||||
|
|
@ -147,6 +194,11 @@ uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels, uint16_t maxCel
|
|||
}
|
||||
|
||||
|
||||
// Returns the number of tiles successfully loaded. outValid[0..maxTiles)
|
||||
// is always fully written: true for each tile read, false for the rest
|
||||
// (including slots past a truncated/short file), so the caller need not
|
||||
// pre-zero it. A return value strictly less than min(headerTileCount,
|
||||
// maxTiles) indicates the file was truncated/corrupt mid-stream.
|
||||
uint16_t jlTileBankLoad(const char *path, jlTileT *outTiles, uint16_t maxTiles,
|
||||
bool *outValid, uint16_t *outPalette) {
|
||||
FILE *fp;
|
||||
|
|
@ -159,28 +211,22 @@ uint16_t jlTileBankLoad(const char *path, jlTileT *outTiles, uint16_t maxTiles,
|
|||
if (path == NULL || outTiles == NULL || maxTiles == 0u) {
|
||||
return 0u;
|
||||
}
|
||||
fp = fopen(path, "rb");
|
||||
if (fp == NULL) {
|
||||
return 0u;
|
||||
}
|
||||
if (fread(header, 1, ASSET_HEADER_BYTES, fp) != ASSET_HEADER_BYTES) {
|
||||
fclose(fp);
|
||||
return 0u;
|
||||
}
|
||||
if (memcmp(header, "JTB1", 4) != 0) {
|
||||
fclose(fp);
|
||||
return 0u;
|
||||
}
|
||||
if (header[4] != EXPECTED_TILE_TARGET) {
|
||||
// File was baked for a different target. Build-system bug
|
||||
// -- the per-target make rule should have produced a .tbk
|
||||
// with the matching target byte.
|
||||
fclose(fp);
|
||||
// The target byte for .tbk must match the build platform. .spr
|
||||
// files carry target=0; tiles carry the per-platform value. A
|
||||
// mismatch is a build-system bug -- the per-target make rule
|
||||
// should have produced a .tbk with the matching target byte.
|
||||
if (!openReadValidateHeader(path, "JTB1", EXPECTED_TILE_TARGET, header, outPalette, &fp)) {
|
||||
return 0u;
|
||||
}
|
||||
tileCount = (uint16_t)(header[6] | (header[7] << 8));
|
||||
if (outPalette != NULL && header[5] != 0u) {
|
||||
readPaletteFromHeader(header, outPalette);
|
||||
|
||||
// Define the whole outValid array up front so leftover slots read
|
||||
// false exactly as the tile.h contract promises, regardless of
|
||||
// caller pre-zeroing.
|
||||
if (outValid != NULL) {
|
||||
for (i = 0; i < maxTiles; i++) {
|
||||
outValid[i] = false;
|
||||
}
|
||||
}
|
||||
|
||||
toLoad = (tileCount < maxTiles) ? tileCount : maxTiles;
|
||||
|
|
|
|||
|
|
@ -88,8 +88,8 @@ void jlAudioTone(uint16_t freqHz) {
|
|||
void jlAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) {
|
||||
// Same reasoning as jlAudioTone: direct hardware-register
|
||||
// programming, no DSP/DMA dependency. Range-check voice here so
|
||||
// the per-port HAL can assume voice < 3.
|
||||
if (voice >= 3u) {
|
||||
// the per-port HAL can assume voice < JOEY_AUDIO_VOICES.
|
||||
if (voice >= JOEY_AUDIO_VOICES) {
|
||||
return;
|
||||
}
|
||||
halAudioVoice(voice, freqHz, atten);
|
||||
|
|
|
|||
|
|
@ -1,27 +1,52 @@
|
|||
// Shared SFX overlay mixer. See audioSfxMixInternal.h for the contract.
|
||||
|
||||
#include <limits.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "audioSfxMixInternal.h"
|
||||
|
||||
// interp()/mix arithmetic (delta * frac up to ~16.7M, and the >> 16 on
|
||||
// a possibly-negative value) requires a >= 32-bit int with arithmetic
|
||||
// right shift. DOS (DJGPP) and ST (m68k gcc) both satisfy this; the
|
||||
// file is excluded from the IIgs build (16-bit int) by
|
||||
// make/iigs.mk. Fail loudly if anyone ever shares this mixer with a
|
||||
// 16-bit-int target instead of producing silent garbage SFX.
|
||||
#if INT_MAX < 0x7FFFFFFF
|
||||
#error "audioSfxMix requires >= 32-bit int (DOS/ST only); IIgs must use NTPstreamsound"
|
||||
#endif
|
||||
|
||||
|
||||
// Refill a streaming slot's prefetch buffer. Preserves the very last
|
||||
// sample of the previous chunk at streamBuf[0] so linear interp can
|
||||
// step across the chunk boundary without a discontinuity click. The
|
||||
// new chunk fills streamBuf[1..]; pos resets to "after byte 0" so
|
||||
// the next fetch is byte 1 (= first sample of the new chunk).
|
||||
// new chunk fills streamBuf[1..]. On a real refill (a previous chunk
|
||||
// existed) pos resets to 0 so the first read interpolates the carried
|
||||
// old-last sample at streamBuf[0] into the new chunk's first sample at
|
||||
// streamBuf[1], bridging the boundary. On the very first prime there
|
||||
// is no real previous sample, so pos resets to "after byte 0" and the
|
||||
// first read starts at the new chunk's first sample (index 1).
|
||||
//
|
||||
// Returns true if more samples are available, false if the stream
|
||||
// has ended (caller should mark the slot inactive).
|
||||
static bool streamRefill(AudioSfxSlotT *slot) {
|
||||
int8_t lastSample;
|
||||
uint32_t got;
|
||||
bool haveCarry;
|
||||
|
||||
if (slot->streamEof) {
|
||||
return false;
|
||||
}
|
||||
lastSample = (slot->streamLen > 0u) ? slot->streamBuf[slot->streamLen - 1u] : (int8_t)0;
|
||||
haveCarry = (slot->streamLen > 0u);
|
||||
lastSample = haveCarry ? slot->streamBuf[slot->streamLen - 1u] : (int8_t)0;
|
||||
// Clamp the callback's reported count to the request size so a
|
||||
// non-conforming fill() that returns more than asked cannot make
|
||||
// the mixer read past streamBuf. (Memory safety still requires
|
||||
// fill() to honor count -- an over-returning callback has already
|
||||
// over-written streamBuf+1; this caps only the subsequent reads.)
|
||||
got = slot->fill(slot->fillCtx, slot->streamBuf + 1, AUDIO_SFX_STREAM_BUF - 1u);
|
||||
if (got > (uint32_t)(AUDIO_SFX_STREAM_BUF - 1u)) {
|
||||
got = AUDIO_SFX_STREAM_BUF - 1u;
|
||||
}
|
||||
if (got == 0u) {
|
||||
slot->streamEof = true;
|
||||
slot->streamLen = 0u;
|
||||
|
|
@ -29,8 +54,17 @@ static bool streamRefill(AudioSfxSlotT *slot) {
|
|||
}
|
||||
slot->streamBuf[0] = lastSample;
|
||||
slot->streamLen = (uint16_t)(got + 1u);
|
||||
// Reset pos to (1 sample, 0 frac) so the next read is at index 1.
|
||||
if (haveCarry) {
|
||||
// Bridge the chunk boundary: start at index 0 so the first
|
||||
// read interpolates streamBuf[0] (old last sample) -> [1]
|
||||
// (new first sample), replaying one sample with no audible
|
||||
// discontinuity click.
|
||||
slot->posFp = 0;
|
||||
} else {
|
||||
// First prime: streamBuf[0] is a synthetic zero, not a real
|
||||
// sample. Skip it so it isn't played as a leading sample.
|
||||
slot->posFp = (uint64_t)1u << 16;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -39,10 +73,14 @@ static bool streamRefill(AudioSfxSlotT *slot) {
|
|||
// `frac` is the 16-bit fractional part of pos; 0 = exactly at s0,
|
||||
// 0xFFFF = nearly s1. Result is a signed int in [-128, +127].
|
||||
static int interp(int8_t s0, int8_t s1, uint32_t frac) {
|
||||
int delta;
|
||||
int32_t delta;
|
||||
|
||||
delta = (int)s1 - (int)s0;
|
||||
return (int)s0 + ((delta * (int)frac) >> 16);
|
||||
// delta (up to +/-255) * frac (up to 65535) is ~16.7M -- needs the
|
||||
// explicit 32-bit width so it can't overflow a 16-bit int. The
|
||||
// >> 16 of a possibly-negative int32_t relies on arithmetic shift,
|
||||
// which holds on every target this file builds for (DOS/ST).
|
||||
delta = (int32_t)s1 - (int32_t)s0;
|
||||
return (int)s0 + (int)((delta * (int32_t)frac) >> 16);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,17 +5,20 @@
|
|||
|
||||
#if defined(JOEYLIB_PLATFORM_IIGS)
|
||||
// On the IIgs the arena holds 65816 machine code that callers JSL
|
||||
// into. ORCA-C's malloc returns memory from the C heap (often bank 0,
|
||||
// which is system RAM) so JSL'ing into it lands on whatever happens
|
||||
// to live there -> instant crash. Memory Manager NewHandle with
|
||||
// into. A normal C heap allocation returns memory from bank 0 (system
|
||||
// RAM), so JSL'ing into it lands on whatever happens to live there ->
|
||||
// instant crash. Memory Manager NewHandle with
|
||||
// attrFixed | attrLocked | attrPage | attrNoCross gives us a fixed
|
||||
// page-aligned region in a single bank we can safely jump into.
|
||||
//
|
||||
// types.h must be included before our stdbool shim because ORCA's
|
||||
// types.h defines true/false as #define ... without #ifndef guards
|
||||
// and would re-#define our shim's macros.
|
||||
#include <types.h>
|
||||
#include <memory.h>
|
||||
// The IIgs toolbox bindings live in <iigs/toolbox.h>. Handles are typed
|
||||
// as void* and the owner ID comes from MMStartUp(); the Memory Manager
|
||||
// attribute bits we use are defined here (values from Apple's Memory
|
||||
// Manager reference).
|
||||
#include <iigs/toolbox.h>
|
||||
#define attrPage 0x0004
|
||||
#define attrNoCross 0x0010
|
||||
#define attrFixed 0x4000
|
||||
#define attrLocked 0x8000
|
||||
#endif
|
||||
|
||||
#include <stdlib.h>
|
||||
|
|
@ -34,16 +37,15 @@
|
|||
// Memory Manager handle is locked-in-place on IIgs). Callers MUST
|
||||
// treat them as read-only.
|
||||
uint8_t *gCodegenArenaBase = NULL;
|
||||
// gCodegenArenaBaseAddr mirrors gCodegenArenaBase as a 24-bit
|
||||
// absolute address. ORCA-C's (uint32_t)pointer cast on the IIgs
|
||||
// zeros the bank byte for some pointer expressions, so JSL targets
|
||||
// read this field directly.
|
||||
// gCodegenArenaBaseAddr mirrors gCodegenArenaBase as a 24-bit absolute
|
||||
// address that JSL targets read directly (captured by copying the
|
||||
// handle pointer's raw bytes -- see codegenArenaInit).
|
||||
uint32_t gCodegenArenaBaseAddr = 0;
|
||||
static uint32_t gTotalBytes = 0;
|
||||
static uint32_t gUsedBytes = 0;
|
||||
static ArenaSlotT *gFirstSlot = NULL;
|
||||
#if defined(JOEYLIB_PLATFORM_IIGS)
|
||||
static Handle gCodegenArenaBaseHandle = NULL;
|
||||
static void *gCodegenArenaBaseHandle = NULL;
|
||||
#endif
|
||||
|
||||
|
||||
|
|
@ -224,23 +226,28 @@ bool codegenArenaInit(uint32_t totalBytes) {
|
|||
return false;
|
||||
}
|
||||
#if defined(JOEYLIB_PLATFORM_IIGS)
|
||||
gCodegenArenaBaseHandle = NewHandle(totalBytes, _ownerid,
|
||||
// MMStartUp() returns this application's master user ID (idempotent;
|
||||
// the system starts the Memory Manager before the app runs). A NULL
|
||||
// handle is the failure signal.
|
||||
{
|
||||
uint16_t ownerId;
|
||||
|
||||
ownerId = MMStartUp();
|
||||
gCodegenArenaBaseHandle = NewHandle((unsigned long)totalBytes, ownerId,
|
||||
attrFixed | attrLocked | attrPage | attrNoCross,
|
||||
NULL);
|
||||
if (gCodegenArenaBaseHandle == NULL || _toolErr != 0) {
|
||||
gCodegenArenaBaseHandle = NULL;
|
||||
(void *)0);
|
||||
}
|
||||
if (gCodegenArenaBaseHandle == NULL) {
|
||||
return false;
|
||||
}
|
||||
HLock(gCodegenArenaBaseHandle);
|
||||
// Capture the 24-bit absolute address by copying the Pointer's
|
||||
// raw bytes -- (uint32_t)pointer through a chain of expressions
|
||||
// has been observed to drop the bank byte under ORCA-C's
|
||||
// memorymodel 1, but a memcpy of the underlying 4 bytes is
|
||||
// reliable. The high byte (bytes[3]) is undefined and masked off.
|
||||
// Capture the 24-bit absolute address by copying the master
|
||||
// pointer's raw bytes; bytes[3] (bank-high/flags) is masked off.
|
||||
{
|
||||
Pointer p;
|
||||
void *p;
|
||||
uint8_t bytes[4];
|
||||
p = *gCodegenArenaBaseHandle;
|
||||
|
||||
p = *(void **)gCodegenArenaBaseHandle;
|
||||
gCodegenArenaBase = (uint8_t *)p;
|
||||
memcpy(bytes, &p, 4);
|
||||
gCodegenArenaBaseAddr = (uint32_t)bytes[0]
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
// compaction that moves a slot's bytes is transparent to callers.
|
||||
//
|
||||
// Allocations go through plain malloc; on every supported port
|
||||
// (IIgs ORCA, Amiga libnix, ST mintlib, DJGPP+CWSDPMI) the heap is
|
||||
// (IIgs, Amiga libnix, ST mintlib, DJGPP+CWSDPMI) the heap is
|
||||
// readable, writable, AND executable. We are not running in a W^X
|
||||
// environment.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
// Cross-platform "where did it hang?" logger. Holds joeylog.txt open
|
||||
// across calls; libc's stdio buffer absorbs writes (~4 KB) and the
|
||||
// final fclose at program exit (via atexit) gets the buffer to disk.
|
||||
// across calls and FLUSHES AFTER EVERY LINE: safe logs are more
|
||||
// important than fast logs. A crash, hang, or hard kill therefore
|
||||
// always leaves every line emitted so far on disk -- which is the whole
|
||||
// point of a "where did it hang?" logger.
|
||||
//
|
||||
// Earlier rev opened+closed per call for crash durability ("last line
|
||||
// guaranteed on disk if we hang"); that cost ~1 second per call
|
||||
// through GoldenGate's ProDOS FST emulation -- a 50-line UBER run
|
||||
// burned ~5 minutes in IO. Even per-line fflush is too expensive
|
||||
// because every fflush forces an FST WRITE, and host-OS file IO time
|
||||
// isn't tracked by the IIgs VBL counter so wall-time logs underreport.
|
||||
// The cost is one host FST WRITE per line (seconds each through the
|
||||
// emulator's ProDOS FST emulation; that host-IO time is not tracked
|
||||
// by the IIgs VBL counter, so wall-time totals computed from the frame
|
||||
// counter slightly underreport). The 16 KB full buffer below still
|
||||
// coalesces each line's individual vfprintf writes into ONE FST write,
|
||||
// so the flush is per-line, not per-fragment.
|
||||
//
|
||||
// Tradeoff: if the program crashes mid-run, buffered log lines may
|
||||
// not reach disk. For UBER and similar batch demos that's acceptable;
|
||||
// for hang-debugging where durability matters, call jlLogFlush()
|
||||
// at the suspected hang points.
|
||||
// (Earlier revs opened+closed per call, then buffered the whole run and
|
||||
// only flushed at the atexit fclose -- fast, but a mid-run crash lost
|
||||
// the buffered tail, exactly when you need it most. Safety wins.)
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdarg.h>
|
||||
|
|
@ -23,12 +24,10 @@
|
|||
|
||||
static const char *kLogPath = "joeylog.txt";
|
||||
static FILE *gLogFp = NULL;
|
||||
/* 16 KB is enough for UBER's full log (~5 KB) plus generous headroom,
|
||||
* so the file never auto-flushes mid-run. ORCA-C / libnix default
|
||||
* buffers are only ~512 bytes; with that, a 50-line log triggers ~10
|
||||
* ProDOS / AmigaDOS WRITEs through the host FST, each of which is
|
||||
* untracked-host-time (seconds). Buffer the whole thing in memory and
|
||||
* let the atexit fclose flush once. */
|
||||
/* 16 KB full buffer so a single log line's many small vfprintf writes
|
||||
* coalesce into one host FST WRITE; jlLog/jlLogF then fflush once per
|
||||
* line. Default stdio buffers are only ~512 bytes, which would split a
|
||||
* long line across multiple FST writes. */
|
||||
#define JOEY_LOG_BUF_BYTES 16384
|
||||
static char gLogBuf[JOEY_LOG_BUF_BYTES];
|
||||
|
||||
|
|
@ -56,6 +55,7 @@ void jlLog(const char *msg) {
|
|||
}
|
||||
fputs(msg, fp);
|
||||
fputc('\n', fp);
|
||||
fflush(fp);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -73,6 +73,7 @@ void jlLogF(const char *fmt, ...) {
|
|||
vfprintf(fp, fmt, args);
|
||||
va_end(args);
|
||||
fputc('\n', fp);
|
||||
fflush(fp);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
404
src/core/draw.c
404
src/core/draw.c
|
|
@ -21,43 +21,22 @@
|
|||
// On overflow the fill silently truncates rather than crashing.
|
||||
#define FLOOD_STACK_SIZE 512
|
||||
|
||||
// Sentinel extents for an empty accumulated dirty bounding box. min* seed
|
||||
// above any valid coordinate and max* below any valid coordinate so the
|
||||
// first plotted point sets every edge, and a box that never receives an
|
||||
// on-surface point collapses to a no-op after clamping (min > max).
|
||||
#define DIRTY_BOX_MIN_INIT ((int16_t)0x7FFF)
|
||||
#define DIRTY_BOX_MAX_INIT ((int16_t)0x8000)
|
||||
|
||||
// ----- Prototypes -----
|
||||
|
||||
static void clipRect(int16_t *x, int16_t *y, int16_t *w, int16_t *h, bool *outVisible);
|
||||
static void fillRectClipped(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex);
|
||||
static void fillRectOnSurface(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex);
|
||||
static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uint8_t newColor, uint8_t matchColor, bool matchEqual);
|
||||
static void plotPixelNoMark(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex);
|
||||
|
||||
// ----- Internal helpers (alphabetical) -----
|
||||
|
||||
// Clip a rectangle to the surface bounds. Outputs *outVisible = false
|
||||
// if the rect is entirely off-surface.
|
||||
static void clipRect(int16_t *x, int16_t *y, int16_t *w, int16_t *h, bool *outVisible) {
|
||||
if (*w <= 0 || *h <= 0) {
|
||||
*outVisible = false;
|
||||
return;
|
||||
}
|
||||
if (*x < 0) {
|
||||
*w += *x;
|
||||
*x = 0;
|
||||
}
|
||||
if (*y < 0) {
|
||||
*h += *y;
|
||||
*y = 0;
|
||||
}
|
||||
if (*x >= SURFACE_WIDTH || *y >= SURFACE_HEIGHT) {
|
||||
*outVisible = false;
|
||||
return;
|
||||
}
|
||||
if (*x + *w > SURFACE_WIDTH) {
|
||||
*w = SURFACE_WIDTH - *x;
|
||||
}
|
||||
if (*y + *h > SURFACE_HEIGHT) {
|
||||
*h = SURFACE_HEIGHT - *y;
|
||||
}
|
||||
*outVisible = (*w > 0 && *h > 0);
|
||||
}
|
||||
|
||||
|
||||
static void fillRectClipped(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex) {
|
||||
uint8_t nibble = colorIndex & 0x0F;
|
||||
uint8_t doubled = (uint8_t)((nibble << 4) | nibble);
|
||||
|
|
@ -67,9 +46,16 @@ static void fillRectClipped(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int1
|
|||
uint16_t midBytes;
|
||||
uint8_t *line;
|
||||
|
||||
// Planar ports have NULL s->pixels; the planar dual-write hook
|
||||
// handles the actual plane fill. Guarding here keeps the chunky
|
||||
// fallback from dereferencing NULL when a port has no chunky store.
|
||||
if (s->pixels == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* px* and midBytes are uint16_t (clipped values are non-negative)
|
||||
* so `>>1` lowers to a single LSR instead of ORCA-C's
|
||||
* ~SSHIFTRIGHT helper. Same with `<<1` for midBytes. */
|
||||
* so `>>1` lowers to a single LSR instead of a signed-shift
|
||||
* helper. Same with `<<1` for midBytes. */
|
||||
for (row = 0; row < h; row++) {
|
||||
line = &s->pixels[SURFACE_ROW_OFFSET(y + row)];
|
||||
pxStart = (uint16_t)x;
|
||||
|
|
@ -93,6 +79,23 @@ static void fillRectClipped(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int1
|
|||
}
|
||||
|
||||
|
||||
// Fill an axis-aligned span that the CALLER has already proven to be
|
||||
// fully on-surface (x >= 0, y >= 0, x + w <= SURFACE_WIDTH, y + h <=
|
||||
// SURFACE_HEIGHT, w > 0, h > 0). Identical tail to jlFillRect but with
|
||||
// the 32-bit clip elided -- on an on-surface rect that clip is a
|
||||
// no-op, so callers that already hold the on-surface guarantee (rect
|
||||
// outlines, circle scanlines, axis-aligned lines) avoid re-deriving
|
||||
// it per span. Pixel output and dirty-mark word bands are bit-for-bit
|
||||
// identical to routing the same span through jlFillRect.
|
||||
static void fillRectOnSurface(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex) {
|
||||
if (!halFastFillRect(s, x, y, (uint16_t)w, (uint16_t)h, colorIndex)) {
|
||||
fillRectClipped(s, x, y, w, h, colorIndex);
|
||||
}
|
||||
halFillRectPlanes(s, x, y, (uint16_t)w, (uint16_t)h, colorIndex);
|
||||
surfaceMarkDirtyRect(s, x, y, w, h);
|
||||
}
|
||||
|
||||
|
||||
// Smith's scanline flood fill. Implements both the unbounded and the
|
||||
// boundary-stopped variants in one pass: the matching predicate is
|
||||
// (pixel == matchColor) when matchEqual is true (unbounded jlFloodFill,
|
||||
|
|
@ -164,6 +167,12 @@ static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uin
|
|||
stackX, stackY,
|
||||
&sp, FLOOD_STACK_SIZE,
|
||||
&seedMatched, &leftX, &rightX)) {
|
||||
// The asm filled the span internally but does not mark
|
||||
// dirty -- that is the C wrapper's job. Mark the just-
|
||||
// filled run so jlStagePresent blits it.
|
||||
if (seedMatched) {
|
||||
surfaceMarkDirtyRect(s, leftX, y, (int16_t)(rightX - leftX + 1), 1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
|
@ -224,20 +233,17 @@ static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uin
|
|||
}
|
||||
}
|
||||
|
||||
// Fill the span. Bypass jlFillRect's clipping wrapper: walk-out
|
||||
// already guaranteed leftX/rightX are in [0..SURFACE_WIDTH-1]
|
||||
// and the seed-pop bounds check did the same for y. We DO
|
||||
// need the planar dual-write (which jlFillRect's wrapper would
|
||||
// call), so invoke halFillRectPlanes explicitly after the
|
||||
// chunky span fill -- otherwise PLANAR_PRESENT builds (and,
|
||||
// post-Phase-9, every build) display flood-filled regions
|
||||
// as the unfilled background.
|
||||
// Fill the span. walk-out already guaranteed leftX/rightX are in
|
||||
// [0..SURFACE_WIDTH-1] and the seed-pop bounds check did the same
|
||||
// for y, so the span is provably on-surface -- route it through
|
||||
// the shared on-surface tail (chunky fill + planar dual-write +
|
||||
// dirty mark) rather than re-clipping through jlFillRect. The
|
||||
// planar dual-write here is load-bearing: without it
|
||||
// PLANAR_PRESENT builds (and, post-Phase-9, every build) display
|
||||
// flood-filled regions as the unfilled background.
|
||||
{
|
||||
int16_t spanW = (int16_t)(rightX - leftX + 1);
|
||||
if (!halFastFillRect(s, leftX, y, (uint16_t)spanW, 1, newNibble)) {
|
||||
fillRectClipped(s, leftX, y, spanW, 1, newNibble);
|
||||
}
|
||||
halFillRectPlanes(s, leftX, y, (uint16_t)spanW, 1, newNibble);
|
||||
fillRectOnSurface(s, leftX, y, spanW, 1, newNibble);
|
||||
}
|
||||
|
||||
// Scan rows above and below for run boundaries. The hot
|
||||
|
|
@ -320,6 +326,36 @@ static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uin
|
|||
}
|
||||
|
||||
|
||||
// Plot a single clipped pixel WITHOUT marking the dirty rect. Shared by
|
||||
// the public jlDrawPixel (which marks per call afterward) and the
|
||||
// Bresenham line/circle fallbacks (which accumulate one bounding box and
|
||||
// mark it once after their loop). Mirrors jlDrawPixel's NULL + bounds
|
||||
// check, halFastDrawPixel dispatch, and chunky nibble RMW fallback.
|
||||
static void plotPixelNoMark(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex) {
|
||||
uint8_t *byte;
|
||||
uint8_t nibble;
|
||||
|
||||
if (s == NULL) {
|
||||
return;
|
||||
}
|
||||
if (x < 0 || x >= SURFACE_WIDTH || y < 0 || y >= SURFACE_HEIGHT) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!halFastDrawPixel(s, (uint16_t)x, (uint16_t)y, colorIndex) && s->pixels != NULL) {
|
||||
/* Cast to uint16_t before shift -- already validated x >= 0,
|
||||
* so unsigned semantics match. Avoids ~SSHIFTRIGHT helper. */
|
||||
byte = &s->pixels[SURFACE_ROW_OFFSET(y) + ((uint16_t)x >> 1)];
|
||||
nibble = colorIndex & 0x0F;
|
||||
if (x & 1) {
|
||||
*byte = (uint8_t)((*byte & 0xF0) | nibble);
|
||||
} else {
|
||||
*byte = (uint8_t)((*byte & 0x0F) | (nibble << 4));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ----- Public API (alphabetical) -----
|
||||
|
||||
void jlDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
|
||||
|
|
@ -327,6 +363,10 @@ void jlDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col
|
|||
int16_t y;
|
||||
int16_t err;
|
||||
int16_t ir;
|
||||
int16_t minX;
|
||||
int16_t minY;
|
||||
int16_t maxX;
|
||||
int16_t maxY;
|
||||
|
||||
if (s == NULL) {
|
||||
return;
|
||||
|
|
@ -338,34 +378,57 @@ void jlDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col
|
|||
|
||||
// Fast path: when the bounding circle is fully on-surface we can
|
||||
// hand off to the port asm (no per-pixel bounds check needed in
|
||||
// the inner loop) and mark the bounding box dirty once.
|
||||
// the inner loop) and mark the bounding box dirty once. A circle
|
||||
// with r >= SURFACE_HEIGHT can never fit on-surface, and casting
|
||||
// such an r to int16_t can wrap negative (r > 32767), so reject it
|
||||
// here before the cast feeds the fast path / dirty-rect extents.
|
||||
if (r < SURFACE_HEIGHT) {
|
||||
ir = (int16_t)r;
|
||||
if (cx - ir >= 0 && cx + ir < SURFACE_WIDTH &&
|
||||
cy - ir >= 0 && cy + ir < SURFACE_HEIGHT &&
|
||||
halFastDrawCircle(s, cx, cy, r, colorIndex)) {
|
||||
surfaceMarkDirtyRect(s, (int16_t)(cx - ir), (int16_t)(cy - ir),
|
||||
(uint16_t)(2 * ir + 1), (uint16_t)(2 * ir + 1));
|
||||
(int16_t)(2 * ir + 1), (int16_t)(2 * ir + 1));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Bresenham midpoint: maintain (x, y) on the perimeter, eight-
|
||||
// octant symmetry plots all 8 reflections each iteration. Routes
|
||||
// through jlDrawPixel so off-surface pixels clip individually.
|
||||
// octant symmetry plots all 8 reflections each iteration. Plots
|
||||
// through plotPixelNoMark so off-surface pixels clip individually
|
||||
// (same per-pixel clip as jlDrawPixel) without paying the
|
||||
// cross-segment dirty mark per pixel; one accumulated bounding box
|
||||
// is marked after the loop instead.
|
||||
minX = DIRTY_BOX_MIN_INIT;
|
||||
minY = DIRTY_BOX_MIN_INIT;
|
||||
maxX = DIRTY_BOX_MAX_INIT;
|
||||
maxY = DIRTY_BOX_MAX_INIT;
|
||||
x = (int16_t)r;
|
||||
y = 0;
|
||||
err = (int16_t)(1 - x);
|
||||
while (x >= y) {
|
||||
jlDrawPixel(s, (int16_t)(cx + x), (int16_t)(cy + y), colorIndex);
|
||||
jlDrawPixel(s, (int16_t)(cx - x), (int16_t)(cy + y), colorIndex);
|
||||
jlDrawPixel(s, (int16_t)(cx + x), (int16_t)(cy - y), colorIndex);
|
||||
jlDrawPixel(s, (int16_t)(cx - x), (int16_t)(cy - y), colorIndex);
|
||||
jlDrawPixel(s, (int16_t)(cx + y), (int16_t)(cy + x), colorIndex);
|
||||
jlDrawPixel(s, (int16_t)(cx - y), (int16_t)(cy + x), colorIndex);
|
||||
jlDrawPixel(s, (int16_t)(cx + y), (int16_t)(cy - x), colorIndex);
|
||||
jlDrawPixel(s, (int16_t)(cx - y), (int16_t)(cy - x), colorIndex);
|
||||
// The 8 octants span columns cx +/- x and cx +/- y, and rows
|
||||
// cy +/- y and cy +/- x. Fold all reflections into the box in
|
||||
// one shot, then clamp once after the loop.
|
||||
if (cx + x > maxX) { maxX = (int16_t)(cx + x); }
|
||||
if (cx - x < minX) { minX = (int16_t)(cx - x); }
|
||||
if (cx + y > maxX) { maxX = (int16_t)(cx + y); }
|
||||
if (cx - y < minX) { minX = (int16_t)(cx - y); }
|
||||
if (cy + x > maxY) { maxY = (int16_t)(cy + x); }
|
||||
if (cy - x < minY) { minY = (int16_t)(cy - x); }
|
||||
if (cy + y > maxY) { maxY = (int16_t)(cy + y); }
|
||||
if (cy - y < minY) { minY = (int16_t)(cy - y); }
|
||||
plotPixelNoMark(s, (int16_t)(cx + x), (int16_t)(cy + y), colorIndex);
|
||||
plotPixelNoMark(s, (int16_t)(cx - x), (int16_t)(cy + y), colorIndex);
|
||||
plotPixelNoMark(s, (int16_t)(cx + x), (int16_t)(cy - y), colorIndex);
|
||||
plotPixelNoMark(s, (int16_t)(cx - x), (int16_t)(cy - y), colorIndex);
|
||||
plotPixelNoMark(s, (int16_t)(cx + y), (int16_t)(cy + x), colorIndex);
|
||||
plotPixelNoMark(s, (int16_t)(cx - y), (int16_t)(cy + x), colorIndex);
|
||||
plotPixelNoMark(s, (int16_t)(cx + y), (int16_t)(cy - x), colorIndex);
|
||||
plotPixelNoMark(s, (int16_t)(cx - y), (int16_t)(cy - x), colorIndex);
|
||||
y++;
|
||||
/* Use `+ + 1` instead of `2 * y + 1` so ORCA-C never emits
|
||||
* the ~SMUL2 helper -- two ADDs are unconditionally cheaper. */
|
||||
/* Use `+ + 1` instead of `2 * y + 1` so the compiler never emits
|
||||
* a multiply helper -- two ADDs are unconditionally cheaper. */
|
||||
if (err <= 0) {
|
||||
err = (int16_t)(err + y + y + 1);
|
||||
} else {
|
||||
|
|
@ -373,6 +436,17 @@ void jlDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col
|
|||
err = (int16_t)(err + y + y - x - x + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Clamp the accumulated box to the surface and mark it once. If the
|
||||
// circle was entirely off-surface no pixel was plotted, so minX/minY
|
||||
// stay above maxX/maxY (after clamping) and nothing is marked.
|
||||
if (minX < 0) { minX = 0; }
|
||||
if (minY < 0) { minY = 0; }
|
||||
if (maxX > SURFACE_WIDTH - 1) { maxX = SURFACE_WIDTH - 1; }
|
||||
if (maxY > SURFACE_HEIGHT - 1) { maxY = SURFACE_HEIGHT - 1; }
|
||||
if (minX <= maxX && minY <= maxY) {
|
||||
surfaceMarkDirtyRect(s, minX, minY, (int16_t)(maxX - minX + 1), (int16_t)(maxY - minY + 1));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -384,13 +458,18 @@ void jlDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, u
|
|||
int16_t err;
|
||||
int16_t e2;
|
||||
int16_t tmp;
|
||||
int16_t minX;
|
||||
int16_t minY;
|
||||
int16_t maxX;
|
||||
int16_t maxY;
|
||||
int32_t span;
|
||||
|
||||
if (s == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Horizontal and vertical fast paths use jlFillRect; the general
|
||||
// case Bresenham routes per-pixel through jlDrawPixel so per-pixel
|
||||
// case Bresenham plots per-pixel through plotPixelNoMark so per-pixel
|
||||
// off-surface clipping just works.
|
||||
if (y0 == y1) {
|
||||
if (x0 > x1) {
|
||||
|
|
@ -398,7 +477,25 @@ void jlDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, u
|
|||
x0 = x1;
|
||||
x1 = tmp;
|
||||
}
|
||||
jlFillRect(s, x0, y0, (uint16_t)(x1 - x0 + 1), 1, colorIndex);
|
||||
// Compute the span in 32-bit so far-apart off-surface endpoints
|
||||
// (e.g. x0=-20000, x1=20000) do not overflow the 16-bit int the
|
||||
// subtraction would otherwise promote to on a 16-bit-int target;
|
||||
// clamp to the surface width so the downstream uint16_t narrowing
|
||||
// is safe.
|
||||
span = (int32_t)x1 - (int32_t)x0 + 1;
|
||||
if (span > SURFACE_WIDTH) {
|
||||
span = SURFACE_WIDTH;
|
||||
}
|
||||
// On-surface (the common case) skips jlFillRect's 32-bit clip:
|
||||
// span is already <= SURFACE_WIDTH and x0 <= x1, so the row is
|
||||
// fully on-surface exactly when its left edge and y are. Off-
|
||||
// surface still routes through jlFillRect for correct clipping.
|
||||
if (x0 >= 0 && y0 >= 0 && y0 < SURFACE_HEIGHT &&
|
||||
(int32_t)x0 + span <= SURFACE_WIDTH) {
|
||||
fillRectOnSurface(s, x0, y0, (int16_t)span, 1, colorIndex);
|
||||
} else {
|
||||
jlFillRect(s, x0, y0, (uint16_t)span, 1, colorIndex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (x0 == x1) {
|
||||
|
|
@ -407,7 +504,16 @@ void jlDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, u
|
|||
y0 = y1;
|
||||
y1 = tmp;
|
||||
}
|
||||
jlFillRect(s, x0, y0, 1, (uint16_t)(y1 - y0 + 1), colorIndex);
|
||||
span = (int32_t)y1 - (int32_t)y0 + 1;
|
||||
if (span > SURFACE_HEIGHT) {
|
||||
span = SURFACE_HEIGHT;
|
||||
}
|
||||
if (x0 >= 0 && x0 < SURFACE_WIDTH && y0 >= 0 &&
|
||||
(int32_t)y0 + span <= SURFACE_HEIGHT) {
|
||||
fillRectOnSurface(s, x0, y0, 1, (int16_t)span, colorIndex);
|
||||
} else {
|
||||
jlFillRect(s, x0, y0, 1, (uint16_t)span, colorIndex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -426,13 +532,25 @@ void jlDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, u
|
|||
return;
|
||||
}
|
||||
|
||||
// Diagonal fallback: plot through plotPixelNoMark so each pixel still
|
||||
// clips off-surface individually (matching jlDrawPixel), but skip the
|
||||
// per-pixel cross-segment dirty mark. Accumulate one bounding box over
|
||||
// the plotted points and mark it once after the loop.
|
||||
minX = DIRTY_BOX_MIN_INIT;
|
||||
minY = DIRTY_BOX_MIN_INIT;
|
||||
maxX = DIRTY_BOX_MAX_INIT;
|
||||
maxY = DIRTY_BOX_MAX_INIT;
|
||||
dx = (int16_t)((x1 > x0) ? (x1 - x0) : (x0 - x1));
|
||||
dy = (int16_t)(-((y1 > y0) ? (y1 - y0) : (y0 - y1)));
|
||||
sx = (int16_t)((x0 < x1) ? 1 : -1);
|
||||
sy = (int16_t)((y0 < y1) ? 1 : -1);
|
||||
err = (int16_t)(dx + dy);
|
||||
while (1) {
|
||||
jlDrawPixel(s, x0, y0, colorIndex);
|
||||
if (x0 < minX) { minX = x0; }
|
||||
if (x0 > maxX) { maxX = x0; }
|
||||
if (y0 < minY) { minY = y0; }
|
||||
if (y0 > maxY) { maxY = y0; }
|
||||
plotPixelNoMark(s, x0, y0, colorIndex);
|
||||
if (x0 == x1 && y0 == y1) {
|
||||
break;
|
||||
}
|
||||
|
|
@ -446,32 +564,55 @@ void jlDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, u
|
|||
y0 = (int16_t)(y0 + sy);
|
||||
}
|
||||
}
|
||||
|
||||
// Clamp the accumulated box to the surface and mark it once. A line
|
||||
// entirely off-surface still ran the loop (so the box holds its raw
|
||||
// extents); clamping collapses it to a no-op when nothing landed on
|
||||
// the surface.
|
||||
if (minX < 0) { minX = 0; }
|
||||
if (minY < 0) { minY = 0; }
|
||||
if (maxX > SURFACE_WIDTH - 1) { maxX = SURFACE_WIDTH - 1; }
|
||||
if (maxY > SURFACE_HEIGHT - 1) { maxY = SURFACE_HEIGHT - 1; }
|
||||
if (minX <= maxX && minY <= maxY) {
|
||||
surfaceMarkDirtyRect(s, minX, minY, (int16_t)(maxX - minX + 1), (int16_t)(maxY - minY + 1));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void jlDrawPixel(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex) {
|
||||
uint8_t *byte;
|
||||
uint8_t nibble;
|
||||
|
||||
if (s == NULL) {
|
||||
return;
|
||||
}
|
||||
if (x < 0 || x >= SURFACE_WIDTH || y < 0 || y >= SURFACE_HEIGHT) {
|
||||
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||
// Hot per-pixel path: the asm plot (halFastDrawPixel -> iigsDrawPixelInner)
|
||||
// plus an inline dirty mark, with NO plotPixelNoMark / surfaceMarkDirtyRect
|
||||
// call layers. NB: an all-C inline RMW here (using the gRowOffsetLut table,
|
||||
// no multiply) was measured SLOWER on hardware (1875 -> 1672 ops/sec) --
|
||||
// the far-pointer code for s->pixels[...] costs more than the
|
||||
// cross-segment JSL into the hand-tuned asm, so the asm plot stays. The
|
||||
// single-row dirty mark is widened inline, only for the stage.
|
||||
if ((uint16_t)x >= SURFACE_WIDTH || (uint16_t)y >= SURFACE_HEIGHT) {
|
||||
return;
|
||||
}
|
||||
(void)halFastDrawPixel(s, (uint16_t)x, (uint16_t)y, colorIndex);
|
||||
if (s == jlStageGet()) {
|
||||
uint8_t word = SURFACE_WORD_INDEX(x);
|
||||
|
||||
if (!halFastDrawPixel(s, (uint16_t)x, (uint16_t)y, colorIndex)) {
|
||||
/* Cast to uint16_t before shift -- already validated x >= 0,
|
||||
* so unsigned semantics match. Avoids ~SSHIFTRIGHT helper. */
|
||||
byte = &s->pixels[SURFACE_ROW_OFFSET(y) + ((uint16_t)x >> 1)];
|
||||
nibble = colorIndex & 0x0F;
|
||||
if (x & 1) {
|
||||
*byte = (uint8_t)((*byte & 0xF0) | nibble);
|
||||
} else {
|
||||
*byte = (uint8_t)((*byte & 0x0F) | (nibble << 4));
|
||||
if (word < gStageMinWord[y]) {
|
||||
gStageMinWord[y] = word;
|
||||
}
|
||||
if (word > gStageMaxWord[y]) {
|
||||
gStageMaxWord[y] = word;
|
||||
}
|
||||
}
|
||||
#else
|
||||
plotPixelNoMark(s, x, y, colorIndex);
|
||||
// Public single-pixel path keeps the original per-call dirty mark
|
||||
// with the raw (x, y); only the Bresenham fallbacks batch the mark
|
||||
// into one accumulated box. plotPixelNoMark already no-op'd the
|
||||
// pixel itself if it was off-surface.
|
||||
surfaceMarkDirtyRect(s, x, y, 1, 1);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -489,6 +630,26 @@ void jlDrawRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uin
|
|||
jlFillRect(s, x, y, w, h, colorIndex);
|
||||
return;
|
||||
}
|
||||
// Fast path: when the OUTER rect is fully on-surface every one of
|
||||
// the four edge spans is also fully on-surface, so each can skip
|
||||
// jlFillRect's 32-bit clip and route straight through the shared
|
||||
// on-surface tail. Test in 32-bit -- w/h are uint16_t, so x + w can
|
||||
// exceed int16_t range. Output is identical because on an
|
||||
// on-surface span jlFillRect's clip is a no-op.
|
||||
if (x >= 0 && y >= 0 &&
|
||||
(int32_t)x + (int32_t)w <= SURFACE_WIDTH &&
|
||||
(int32_t)y + (int32_t)h <= SURFACE_HEIGHT) {
|
||||
// Top edge.
|
||||
fillRectOnSurface(s, x, y, (int16_t)w, 1, colorIndex);
|
||||
// Bottom edge.
|
||||
fillRectOnSurface(s, x, (int16_t)(y + (int16_t)h - 1), (int16_t)w, 1, colorIndex);
|
||||
// Left edge (interior only -- top and bottom corners already drawn).
|
||||
fillRectOnSurface(s, x, (int16_t)(y + 1), 1, (int16_t)(h - 2), colorIndex);
|
||||
// Right edge (interior only).
|
||||
fillRectOnSurface(s, (int16_t)(x + (int16_t)w - 1), (int16_t)(y + 1), 1, (int16_t)(h - 2), colorIndex);
|
||||
return;
|
||||
}
|
||||
// General (possibly clipped) case: let jlFillRect clip each edge.
|
||||
// Top edge.
|
||||
jlFillRect(s, x, y, w, 1, colorIndex);
|
||||
// Bottom edge.
|
||||
|
|
@ -504,9 +665,10 @@ void jlFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col
|
|||
int16_t y;
|
||||
int16_t x;
|
||||
int16_t ir;
|
||||
uint16_t xx;
|
||||
uint16_t yy;
|
||||
uint16_t r2;
|
||||
uint32_t xx;
|
||||
uint32_t yy;
|
||||
uint32_t r2;
|
||||
bool onSurface;
|
||||
|
||||
if (s == NULL) {
|
||||
return;
|
||||
|
|
@ -516,26 +678,39 @@ void jlFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col
|
|||
return;
|
||||
}
|
||||
|
||||
// Fast path: a circle with r >= SURFACE_HEIGHT can never fit fully
|
||||
// on-surface, and casting such an r to int16_t can wrap negative
|
||||
// (r > 32767), spuriously passing the bounds test and feeding the
|
||||
// asm / dirty-rect garbage. Reject it here before the cast.
|
||||
onSurface = false;
|
||||
if (r < SURFACE_HEIGHT) {
|
||||
ir = (int16_t)r;
|
||||
if (cx - ir >= 0 && cx + ir < SURFACE_WIDTH &&
|
||||
cy - ir >= 0 && cy + ir < SURFACE_HEIGHT &&
|
||||
halFastFillCircle(s, cx, cy, r, colorIndex)) {
|
||||
cy - ir >= 0 && cy + ir < SURFACE_HEIGHT) {
|
||||
// The whole bounding circle is on-surface, so every scanline
|
||||
// span below is too. Remember this so the C fallback can
|
||||
// route spans through the lighter on-surface fill instead of
|
||||
// re-clipping each one through jlFillRect.
|
||||
onSurface = true;
|
||||
if (halFastFillCircle(s, cx, cy, r, colorIndex)) {
|
||||
surfaceMarkDirtyRect(s, (int16_t)(cx - ir), (int16_t)(cy - ir),
|
||||
(uint16_t)(2 * ir + 1), (uint16_t)(2 * ir + 1));
|
||||
(int16_t)(2 * ir + 1), (int16_t)(2 * ir + 1));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For each y from 0 to r, find the largest x such that x*x + y*y
|
||||
// <= r*r and emit a horizontal span. Maintain xx=x*x, yy=y*y
|
||||
// incrementally so the hot loop never does a 32-bit multiply --
|
||||
// critical on 65816 / 68000 / 286 where mul is slow or absent.
|
||||
// (y+1)^2 = y^2 + 2y + 1; (x-1)^2 = x^2 - 2x + 1. r is uint16_t
|
||||
// so xx, yy, r2 fit in uint16_t for any r where x*x+y*y can equal
|
||||
// r2 (i.e. r <= 255 -> r2 <= 65025).
|
||||
/* Same `+ +` pattern as jlDrawCircle so ORCA-C doesn't emit ~SMUL2 /
|
||||
* ~CUMUL2 helpers for the `2 * ...` constants. spanWidth is hoisted
|
||||
// (y+1)^2 = y^2 + 2y + 1; (x-1)^2 = x^2 - 2x + 1. xx, yy and r2 are
|
||||
// uint32_t so r*r does not wrap for r >= 256 (a uint16_t product
|
||||
// overflows at r == 256 -> r2 == 0).
|
||||
/* Same `+ +` pattern as jlDrawCircle so the compiler doesn't emit
|
||||
* multiply helpers for the `2 * ...` constants. spanWidth is hoisted
|
||||
* because both jlFillRect calls in the body need it. */
|
||||
xx = (uint16_t)(r * r);
|
||||
xx = (uint32_t)r * (uint32_t)r;
|
||||
r2 = xx;
|
||||
yy = 0;
|
||||
x = (int16_t)r;
|
||||
|
|
@ -543,15 +718,25 @@ void jlFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col
|
|||
uint16_t spanWidth;
|
||||
|
||||
while (xx + yy > r2) {
|
||||
xx = (uint16_t)(xx - (uint16_t)((uint16_t)x + (uint16_t)x - 1u));
|
||||
xx = xx - (uint32_t)((uint16_t)x + (uint16_t)x - 1u);
|
||||
x--;
|
||||
}
|
||||
spanWidth = (uint16_t)((uint16_t)x + (uint16_t)x + 1u);
|
||||
if (onSurface) {
|
||||
// 0 <= cy +/- y <= cy + ir < SURFACE_HEIGHT and
|
||||
// 0 <= cx - x ... cx + x < SURFACE_WIDTH for every span, so
|
||||
// jlFillRect's clip would be a no-op -- skip it.
|
||||
fillRectOnSurface(s, (int16_t)(cx - x), (int16_t)(cy + y), (int16_t)spanWidth, 1, colorIndex);
|
||||
if (y > 0) {
|
||||
fillRectOnSurface(s, (int16_t)(cx - x), (int16_t)(cy - y), (int16_t)spanWidth, 1, colorIndex);
|
||||
}
|
||||
} else {
|
||||
jlFillRect(s, (int16_t)(cx - x), (int16_t)(cy + y), spanWidth, 1, colorIndex);
|
||||
if (y > 0) {
|
||||
jlFillRect(s, (int16_t)(cx - x), (int16_t)(cy - y), spanWidth, 1, colorIndex);
|
||||
}
|
||||
yy = (uint16_t)(yy + (uint16_t)((uint16_t)y + (uint16_t)y + 1u));
|
||||
}
|
||||
yy = yy + (uint32_t)((uint16_t)y + (uint16_t)y + 1u);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -561,25 +746,40 @@ void jlFillRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uin
|
|||
int16_t sy;
|
||||
int16_t sw;
|
||||
int16_t sh;
|
||||
bool visible;
|
||||
int32_t left;
|
||||
int32_t top;
|
||||
int32_t right;
|
||||
int32_t bottom;
|
||||
|
||||
if (s == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
sx = x;
|
||||
sy = y;
|
||||
sw = (int16_t)w;
|
||||
sh = (int16_t)h;
|
||||
clipRect(&sx, &sy, &sw, &sh, &visible);
|
||||
if (!visible) {
|
||||
// Clip in 32-bit so any int16_t x/y combined with any uint16_t w/h
|
||||
// is handled without overflow (a uint16_t w > 32767 would wrap
|
||||
// negative if narrowed to int16_t before clipping, and a negative x
|
||||
// with a large w must still fill out to the right surface edge).
|
||||
// The surface is only 320x200, so the clipped result always fits
|
||||
// int16_t.
|
||||
left = (int32_t)x;
|
||||
top = (int32_t)y;
|
||||
right = left + (int32_t)w;
|
||||
bottom = top + (int32_t)h;
|
||||
if (left < 0) { left = 0; }
|
||||
if (top < 0) { top = 0; }
|
||||
if (right > SURFACE_WIDTH) { right = SURFACE_WIDTH; }
|
||||
if (bottom > SURFACE_HEIGHT) { bottom = SURFACE_HEIGHT; }
|
||||
if (right <= left || bottom <= top) {
|
||||
return;
|
||||
}
|
||||
if (!halFastFillRect(s, sx, sy, (uint16_t)sw, (uint16_t)sh, colorIndex)) {
|
||||
fillRectClipped(s, sx, sy, sw, sh, colorIndex);
|
||||
}
|
||||
halFillRectPlanes(s, sx, sy, (uint16_t)sw, (uint16_t)sh, colorIndex);
|
||||
surfaceMarkDirtyRect(s, sx, sy, sw, sh);
|
||||
sx = (int16_t)left;
|
||||
sy = (int16_t)top;
|
||||
sw = (int16_t)(right - left);
|
||||
sh = (int16_t)(bottom - top);
|
||||
// The clipped rect is now provably on-surface; share the fill +
|
||||
// planar dual-write + dirty-mark tail with the other on-surface
|
||||
// span emitters so there is one source of truth for it.
|
||||
fillRectOnSurface(s, sx, sy, sw, sh, colorIndex);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -649,7 +849,7 @@ void jlSurfaceClear(jlSurfaceT *s, uint8_t colorIndex) {
|
|||
}
|
||||
nibble = colorIndex & 0x0F;
|
||||
doubled = (uint8_t)((nibble << 4) | nibble);
|
||||
if (!halFastSurfaceClear(s, doubled)) {
|
||||
if (!halFastSurfaceClear(s, doubled) && s->pixels != NULL) {
|
||||
memset(s->pixels, doubled, SURFACE_PIXELS_SIZE);
|
||||
}
|
||||
surfaceMarkDirtyAll(s);
|
||||
|
|
|
|||
|
|
@ -191,8 +191,12 @@ uint16_t halFrameHz(void);
|
|||
|
||||
// Wall-clock millisecond counter since halInit. Source of truth
|
||||
// for time-based scheduling (sound event timing, animation cadence
|
||||
// on platforms where rendering can outpace VBL). Per-port impl
|
||||
// uses the cheapest sub-frame timer available on that hardware.
|
||||
// on platforms where rendering can outpace VBL). Per-port impl uses
|
||||
// the cheapest sub-frame timer available on that hardware: DOS uses
|
||||
// uclock() (PIT counter 0, ~1 us resolution); ST reads the 200 Hz
|
||||
// Timer C counter at _hz_200; Amiga uses ReadEClock (~1.4 us PAL);
|
||||
// IIgs falls back to frame_count * 1000 / 60 since pulling in time()
|
||||
// bursts the stdio cluster bank.
|
||||
uint32_t halMillisElapsed(void);
|
||||
|
||||
// Audio: per-port engine setup, module + SFX playback, teardown.
|
||||
|
|
@ -234,7 +238,7 @@ void halAudioFrameTick(void);
|
|||
// caller should fall back to the platform-agnostic C implementation.
|
||||
//
|
||||
// Funneling all asm dispatches through hal.c (one TU per port) avoids
|
||||
// the cumulative ORCA Linker "Expression too complex" failure that
|
||||
// the cumulative linker "Expression too complex" failure that
|
||||
// hits when multiple cross-platform TUs each call into a named load
|
||||
// segment full of asm primitives. Cross-platform code in src/core/
|
||||
// only ever calls into HAL, so the link-time expression cost is paid
|
||||
|
|
@ -358,7 +362,7 @@ bool halFloodScanRowPlanes(const jlSurfaceT *s, int16_t leftX, int16_t rightX, i
|
|||
//
|
||||
// Cross-platform code in src/core/*.c is unchanged -- it still calls
|
||||
// halFastDrawPixel(...) etc. On IIgs the preprocessor swaps that for
|
||||
// the macro expansion before ORCA-C compiles the file. The matching
|
||||
// the macro expansion before the file is compiled. The matching
|
||||
// halFast* C definitions in src/port/iigs/hal.c are deleted, since
|
||||
// nothing references them once the macros take effect.
|
||||
//
|
||||
|
|
@ -371,6 +375,7 @@ extern void iigsDrawLineInner (uint8_t *pixels, uint16_t x0, uint16_t y0, uin
|
|||
extern void iigsDrawCircleInner (uint8_t *pixels, uint16_t cx, uint16_t cy, uint16_t r, uint16_t nibble);
|
||||
extern void iigsFillCircleInner (uint8_t *pixels, uint16_t cx, uint16_t cy, uint16_t r, uint16_t fillWord);
|
||||
extern void iigsSurfaceClearInner(uint8_t *pixels, uint16_t fillWord);
|
||||
extern void iigsSurfaceClearFastInner(uint8_t *pixels, uint16_t fillWord);
|
||||
extern void iigsTileFillInner (uint8_t *dstRow0, uint16_t fillWord);
|
||||
extern void iigsTileCopyInner (uint8_t *dstRow0, const uint8_t *srcRow0);
|
||||
extern void iigsTileCopyMaskedInner(uint8_t *dstRow0, const uint8_t *srcRow0, uint16_t transparent);
|
||||
|
|
@ -416,7 +421,7 @@ extern uint16_t gFloodRightX;
|
|||
#undef halFastSurfaceClear
|
||||
#define halFastSurfaceClear(_s, _d) \
|
||||
((_s) == jlStageGet() \
|
||||
? (iigsSurfaceClearInner((_s)->pixels, \
|
||||
? (iigsSurfaceClearFastInner((_s)->pixels, \
|
||||
(uint16_t)((uint16_t)(_d) | ((uint16_t)(_d) << 8))), \
|
||||
true) \
|
||||
: false)
|
||||
|
|
@ -438,7 +443,7 @@ extern uint16_t gFloodRightX;
|
|||
// Tile primitives operate on caller-computed row pointers; just
|
||||
// forward the args. by/bx are tile coords -> bx*4 + by*8*160 byte
|
||||
// offset within the surface. Use SURFACE_ROW_OFFSET (LUT lookup) to
|
||||
// dodge ORCA-C 2.2.1's ~CUMUL2 helper for the *160 multiply.
|
||||
// dodge a software multiply helper for the *160 multiply.
|
||||
#undef halFastTileFill
|
||||
#define halFastTileFill(_s, _bx, _by, _fw) \
|
||||
(iigsTileFillInner(&(_s)->pixels[SURFACE_ROW_OFFSET((uint16_t)(_by) << 3) \
|
||||
|
|
@ -461,8 +466,8 @@ extern uint16_t gFloodRightX;
|
|||
|
||||
// Tier 2/3 flood fallbacks always returned false on IIgs (the asm
|
||||
// impls were deleted as unreachable). Macros to constant false so
|
||||
// ORCA-C dead-code-eliminates the never-taken fallback branches in
|
||||
// floodFillInternal.
|
||||
// the compiler dead-code-eliminates the never-taken fallback branches
|
||||
// in floodFillInternal.
|
||||
#undef halFastFloodWalk
|
||||
#define halFastFloodWalk(_row, _sx, _mc, _nc, _me, _sm, _lx, _rx) (false)
|
||||
|
||||
|
|
@ -494,6 +499,63 @@ extern uint16_t gFloodRightX;
|
|||
*(_rxOut) = (int16_t)gFloodRightX, \
|
||||
true)
|
||||
|
||||
// =====================================================================
|
||||
// IIgs planar dual-write elision.
|
||||
//
|
||||
// Every cross-platform primitive calls a halXxxPlanes() hook
|
||||
// unconditionally so planar ports (Amiga, ST) can mirror each draw
|
||||
// into their bitplanes. IIgs is chunky-native, so those hooks are
|
||||
// pure no-op stubs -- but the call site still costs a cross-segment
|
||||
// JSL per primitive (PHB/PHD setup + RTL) for zero work. These macros
|
||||
// expand each call to ((void)0) at preprocess time so the call site is
|
||||
// deleted entirely, exactly as the halFast* macros above eliminate the
|
||||
// wrapper layer. The matching no-op stub definitions in
|
||||
// src/port/iigs/hal.c are deleted, since nothing references them once
|
||||
// the macros take effect.
|
||||
//
|
||||
// Gated on JOEYLIB_PLATFORM_IIGS specifically (NOT JOEYLIB_NATIVE_CHUNKY):
|
||||
// DOS is also chunky-native and keeps its real stub functions, because
|
||||
// its stubs are cheap near-calls and DOS is not the perf reference.
|
||||
//
|
||||
// Arg lists match each hook's arity so the preprocessor parses and
|
||||
// discards the exact call expression; arguments with side effects are
|
||||
// not present at any halXxxPlanes call site (all args are plain lvalues
|
||||
// / field reads), so dropping them changes no behavior.
|
||||
// =====================================================================
|
||||
|
||||
#undef halFillRectPlanes
|
||||
#define halFillRectPlanes(_s, _x, _y, _w, _h, _c) ((void)0)
|
||||
|
||||
#undef halSurfaceCopyPlanes
|
||||
#define halSurfaceCopyPlanes(_dst, _src) ((void)0)
|
||||
|
||||
#undef halTileFillPlanes
|
||||
#define halTileFillPlanes(_s, _bx, _by, _c) ((void)0)
|
||||
|
||||
#undef halTileCopyPlanes
|
||||
#define halTileCopyPlanes(_dst, _dbx, _dby, _src, _sbx, _sby) ((void)0)
|
||||
|
||||
#undef halTileCopyMaskedPlanes
|
||||
#define halTileCopyMaskedPlanes(_dst, _dbx, _dby, _src, _sbx, _sby, _t) ((void)0)
|
||||
|
||||
#undef halTilePastePlanes
|
||||
#define halTilePastePlanes(_dst, _bx, _by, _ct) ((void)0)
|
||||
|
||||
#undef halTilePasteMonoPlanes
|
||||
#define halTilePasteMonoPlanes(_dst, _bx, _by, _mt, _fg, _bg) ((void)0)
|
||||
|
||||
#undef halTileSnapPlanes
|
||||
#define halTileSnapPlanes(_src, _bx, _by, _out) ((void)0)
|
||||
|
||||
#undef halSpriteDrawPlanes
|
||||
#define halSpriteDrawPlanes(_s, _sp, _x, _y) ((void)0)
|
||||
|
||||
#undef halSpriteSavePlanes
|
||||
#define halSpriteSavePlanes(_s, _x, _y, _w, _h, _dst) ((void)0)
|
||||
|
||||
#undef halSpriteRestorePlanes
|
||||
#define halSpriteRestorePlanes(_s, _x, _y, _w, _h, _src) ((void)0)
|
||||
|
||||
#endif /* JOEYLIB_PLATFORM_IIGS */
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -15,13 +15,16 @@
|
|||
|
||||
// 8 KB fits the largest typical sprite working set (~3-4 KB per
|
||||
// 32x32 sprite at all opaque) and keeps malloc requests small enough
|
||||
// for IIgs ORCA-C's small-memory-model heap to satisfy them.
|
||||
// for the IIgs heap to satisfy them.
|
||||
#define DEFAULT_CODEGEN_BYTES (8u * 1024u)
|
||||
|
||||
// ----- Prototypes -----
|
||||
|
||||
static void clearError(void);
|
||||
static void setError(const char *message);
|
||||
|
||||
// coreSetError is the shared error setter (declared in surfaceInternal.h
|
||||
// and called by surface.c etc.); its definition lives here next to the
|
||||
// rest of the error-reporting state.
|
||||
|
||||
// ----- Module state -----
|
||||
|
||||
|
|
@ -36,7 +39,7 @@ static void clearError(void) {
|
|||
}
|
||||
|
||||
|
||||
static void setError(const char *message) {
|
||||
void coreSetError(const char *message) {
|
||||
gLastError = message;
|
||||
}
|
||||
|
||||
|
|
@ -47,12 +50,12 @@ bool jlInit(const jlConfigT *config) {
|
|||
clearError();
|
||||
|
||||
if (gInitialized) {
|
||||
setError("jlInit called while already initialized");
|
||||
coreSetError("jlInit called while already initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config == NULL) {
|
||||
setError("jlInit called with NULL config");
|
||||
coreSetError("jlInit called with NULL config");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -63,19 +66,19 @@ bool jlInit(const jlConfigT *config) {
|
|||
// SHR setup that halInit performs.
|
||||
if (!halInit(&gConfig)) {
|
||||
const char *halMsg = halLastError();
|
||||
setError(halMsg != NULL ? halMsg : "halInit failed");
|
||||
coreSetError(halMsg != NULL ? halMsg : "halInit failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!stageAlloc()) {
|
||||
setError("failed to allocate stage surface");
|
||||
coreSetError("failed to allocate stage surface");
|
||||
halShutdown();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!codegenArenaInit(gConfig.codegenBytes != 0 ? gConfig.codegenBytes
|
||||
: DEFAULT_CODEGEN_BYTES)) {
|
||||
setError("failed to allocate codegen arena");
|
||||
coreSetError("failed to allocate codegen arena");
|
||||
stageFree();
|
||||
halShutdown();
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ extern void iigsInputSnapshot(void);
|
|||
// and the small button counts, and walks every array one byte per
|
||||
// element. If a future change adds/removes keys or buttons the asm
|
||||
// must be updated; if anyone re-types the arrays back to bool the
|
||||
// per-element size grows to ORCA-C's 2-byte _Bool and the asm reads
|
||||
// per-element size could grow past one byte and the asm reads
|
||||
// the wrong bytes. Either condition declares a zero-size array
|
||||
// below, which is a compile error.
|
||||
typedef int joey_keycount_check [(KEY_COUNT == 60) ? 1 : -1];
|
||||
|
|
@ -53,7 +53,7 @@ typedef int joey_joybtn_size_check [(sizeof(gJoyButtonState) == JOYSTICK_COU
|
|||
void jlInputPoll(void) {
|
||||
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||
// One asm pass for: TTL decrement + key snapshot + mouse/joy
|
||||
// button snapshots. Replaces 3 ORCA-C memcpys + the C TTL loop
|
||||
// button snapshots. Replaces 3 memcpys + the C TTL loop
|
||||
// that used to live in halInputPoll. ~0.6 ms saved per frame.
|
||||
iigsInputSnapshot();
|
||||
#else
|
||||
|
|
@ -88,8 +88,7 @@ void jlWaitForAnyKey(void) {
|
|||
* and upper-bound check (`>= COUNT`) into a single unsigned compare.
|
||||
* Index 0 (KEY_NONE / MOUSE_BUTTON_NONE) is a sentinel that no HAL
|
||||
* ever writes, so reading gKeyState[0] / gMouseButtonState[0] is
|
||||
* always 0 -- the predicate result is unchanged but ORCA-C drops the
|
||||
* compound `||` into one branch each. */
|
||||
* always 0 -- the predicate result is unchanged. */
|
||||
bool jlKeyDown(jlKeyE key) {
|
||||
if ((uint16_t)key >= (uint16_t)KEY_COUNT) {
|
||||
return false;
|
||||
|
|
@ -172,9 +171,9 @@ int8_t jlJoystickY(jlJoystickE js) {
|
|||
}
|
||||
|
||||
|
||||
/* Joystick button predicates: ORCA-C 2.2.1 lowers `gJoyButtonState[js][button]`
|
||||
* to a ~MUL4 helper per access. Compute the 1D byte index once and read
|
||||
* via an explicit (uint8_t *) cast -- no helpers. */
|
||||
/* Joystick button predicates: compute the 1D byte index once and read
|
||||
* via an explicit (uint8_t *) cast so the 2D-array indexing avoids a
|
||||
* per-access multiply helper. */
|
||||
bool jlJoyDown(jlJoystickE js, jlJoyButtonE button) {
|
||||
uint16_t idx;
|
||||
if ((uint16_t)js >= (uint16_t)JOYSTICK_COUNT) {
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@
|
|||
#include "joey/input.h"
|
||||
#include "joey/types.h"
|
||||
|
||||
// Stored as uint8_t (not bool) because ORCA-C compiles _Bool as a
|
||||
// 2-byte word (Symbol.pas: size := cgWordSize). The IIgs asm fast
|
||||
// path (iigsInputSnapshot) walks these arrays one byte per element;
|
||||
// a 2-byte bool would put element k at byte offset 2*k and the asm's
|
||||
// per-byte clear would never reach the live half. uint8_t pins the
|
||||
// storage to one byte per element on every port. Public predicates
|
||||
// still return bool via implicit coercion.
|
||||
// Stored as uint8_t (not bool) because a _Bool wider than one byte
|
||||
// would break the IIgs asm fast path (iigsInputSnapshot), which walks
|
||||
// these arrays one byte per element; with a 2-byte bool element k
|
||||
// would sit at byte offset 2*k and the asm's per-byte clear would
|
||||
// never reach the live half. uint8_t pins the storage to one byte per
|
||||
// element on every port. Public predicates still return bool via
|
||||
// implicit coercion.
|
||||
extern uint8_t gKeyState[KEY_COUNT];
|
||||
extern uint8_t gKeyPrev [KEY_COUNT];
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,14 @@
|
|||
#include "joey/palette.h"
|
||||
#include "surfaceInternal.h"
|
||||
|
||||
// jlPaletteSet / jlPaletteGet reach palette row N via byte-pointer math
|
||||
// + a single `<< 5` shift (= 32 bytes per row) to avoid a per-access
|
||||
// multiply helper. That stride is only correct while a palette row is
|
||||
// exactly SURFACE_COLORS_PER_PALETTE * sizeof(uint16_t) bytes; pin the
|
||||
// relationship so a future palette-width change fails to compile here
|
||||
// rather than silently reading the wrong row.
|
||||
typedef char paletteRowStrideMatchesShift[((1u << 5) == (SURFACE_COLORS_PER_PALETTE * sizeof(uint16_t))) ? 1 : -1];
|
||||
|
||||
|
||||
// Standard 16-color EGA palette in IIgs $0RGB format. Used as the
|
||||
// per-surface default at allocation time (paletteInitDefault) so a
|
||||
|
|
@ -45,8 +53,14 @@ void paletteInitDefault(jlSurfaceT *s) {
|
|||
if (s == NULL) {
|
||||
return;
|
||||
}
|
||||
// kDefaultPaletteEga is already valid $0RGB with color 0 == 0x0000,
|
||||
// so copy it directly into each palette row instead of routing
|
||||
// through jlPaletteSet 16 times (which would re-mask every entry
|
||||
// and re-run the stage-dirty check on a surface that is not yet
|
||||
// the stage). gStagePaletteDirty already defaults to true so the
|
||||
// default palette is still uploaded on first present.
|
||||
for (i = 0; i < SURFACE_PALETTE_COUNT; i++) {
|
||||
jlPaletteSet(s, i, kDefaultPaletteEga);
|
||||
memcpy(s->palette[i], kDefaultPaletteEga, sizeof(kDefaultPaletteEga));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -62,7 +76,7 @@ void jlPaletteGet(const jlSurfaceT *s, uint8_t paletteIndex, uint16_t *out16) {
|
|||
if (paletteIndex >= SURFACE_PALETTE_COUNT) {
|
||||
return;
|
||||
}
|
||||
/* Byte-pointer math + shift to skip the ~MUL4 helper -- see
|
||||
/* Byte-pointer math + shift to skip the multiply helper -- see
|
||||
* jlPaletteSet for the reasoning. */
|
||||
row = (const uint16_t *)((const uint8_t *)s->palette + ((uint16_t)paletteIndex << 5));
|
||||
memcpy(out16, row, SURFACE_COLORS_PER_PALETTE * sizeof(uint16_t));
|
||||
|
|
@ -82,10 +96,10 @@ void jlPaletteSet(jlSurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16)
|
|||
}
|
||||
|
||||
/* Compute the row pointer via byte-pointer math + a single shift
|
||||
* (16 entries * 2 bytes = 32 = 1 << 5) so ORCA-C doesn't emit a
|
||||
* ~MUL4 helper for the 2D-array indexing. Then walk both arrays
|
||||
* with post-increment pointers so the inner loop avoids ~MUL4
|
||||
* for every `row[i]` / `colors16[i]` index multiply too. */
|
||||
* (16 entries * 2 bytes = 32 = 1 << 5) so the 2D-array indexing
|
||||
* avoids a multiply helper. Then walk both arrays with
|
||||
* post-increment pointers so the inner loop avoids the index
|
||||
* multiply for every `row[i]` / `colors16[i]` too. */
|
||||
row = (uint16_t *)((uint8_t *)s->palette + ((uint16_t)paletteIndex << 5));
|
||||
src = colors16;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// Deterministic, seedable PRNG (xorshift32 / Marsaglia).
|
||||
//
|
||||
// stdlib rand() is unsuitable: DJGPP, ORCA-C, libnix, and mintlib all
|
||||
// ship different LCG constants, and ORCA-C's rand() returns a 16-bit
|
||||
// value while the others return 31 bits. Any game logic relying on
|
||||
// stdlib rand() is unsuitable: the per-platform C libraries ship
|
||||
// different LCG constants and differing result widths (some return a
|
||||
// 16-bit value, others 31 bits). Any game logic relying on
|
||||
// reproducible sequences (replay, demo records, level seeds, deck
|
||||
// shuffles) would diverge silently. xorshift32 has no multiply, so
|
||||
// the 65816 perf floor isn't penalized by a software mul routine.
|
||||
|
|
@ -32,7 +32,12 @@ uint16_t jlRandomRange(uint16_t bound) {
|
|||
if (bound == 0u) {
|
||||
return 0u;
|
||||
}
|
||||
return (uint16_t)(jlRandom() % bound);
|
||||
// Multiply-high map over the top 16 (best-mixed) bits of the
|
||||
// xorshift state. Divide-free -- the 65816/68000 lack a hardware
|
||||
// divide, so plain `% bound` would pull in a software divide
|
||||
// routine the multiply-free PRNG was chosen to avoid. Residual
|
||||
// bias is the same negligible order as modulo.
|
||||
return (uint16_t)(((uint32_t)(jlRandom() >> 16) * (uint32_t)bound) >> 16);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ void jlScbSet(jlSurfaceT *s, uint16_t line, uint8_t paletteIndex) {
|
|||
|
||||
|
||||
void jlScbSetRange(jlSurfaceT *s, uint16_t firstLine, uint16_t lastLine, uint8_t paletteIndex) {
|
||||
uint16_t line;
|
||||
uint16_t last;
|
||||
|
||||
if (s == NULL) {
|
||||
|
|
@ -56,12 +55,11 @@ void jlScbSetRange(jlSurfaceT *s, uint16_t firstLine, uint16_t lastLine, uint8_t
|
|||
return;
|
||||
}
|
||||
|
||||
// memset is far cheaper than the per-iter loop on ORCA-C with -b
|
||||
// (scb is uint8_t, sizeof(uint8_t)==1, so the call form below is
|
||||
// exact). On IIgs ORCA-C lowers small fixed-size memsets to MVP /
|
||||
// PEI tricks; on Amiga/ST/DOS it uses libc memset which is
|
||||
// already vectorized. Either way, much tighter than the C loop.
|
||||
(void)line;
|
||||
// memset is far cheaper than the per-iter loop (scb is uint8_t,
|
||||
// sizeof(uint8_t)==1, so the call form below is exact). On IIgs
|
||||
// small fixed-size memsets lower to MVP / PEI tricks; on
|
||||
// Amiga/ST/DOS libc memset is already vectorized. Either way, much
|
||||
// tighter than the C loop.
|
||||
memset(&s->scb[firstLine], paletteIndex, (size_t)(last - firstLine + 1));
|
||||
if (s == jlStageGet()) {
|
||||
gStageScbDirty = true;
|
||||
|
|
|
|||
|
|
@ -8,16 +8,15 @@
|
|||
#include <string.h>
|
||||
|
||||
#include "joey/sprite.h"
|
||||
#include "joey/tile.h"
|
||||
#include "codegenArenaInternal.h"
|
||||
#include "hal.h"
|
||||
#include "spriteInternal.h"
|
||||
#include "surfaceInternal.h"
|
||||
|
||||
|
||||
// 8x8 tiles, 4bpp packed = 4 bytes/row * 8 rows = 32 bytes/tile.
|
||||
#define TILE_BYTES 32
|
||||
#define TILE_PIXELS 8
|
||||
#define TILE_BYTES_PER_ROW 4
|
||||
// 8x8 tile geometry constants come from joey/tile.h -- one source of
|
||||
// truth for the 4bpp packed layout (32 bytes/tile, 4 bytes/row, 8 px/side).
|
||||
|
||||
// Color 0 is always transparent for sprites (DESIGN.md contract).
|
||||
#define TRANSPARENT_NIBBLE 0
|
||||
|
|
@ -43,10 +42,10 @@
|
|||
|
||||
// ----- Prototypes -----
|
||||
|
||||
static uint8_t srcNibble(const jlSpriteT *sp, int16_t pxX, int16_t pxY);
|
||||
static void writeDstNibble(uint8_t *row, int16_t x, uint8_t nibble);
|
||||
static bool clipRect(int16_t *dstX, int16_t *dstY, int16_t *srcX, int16_t *srcY, int16_t *w, int16_t *h);
|
||||
static bool isFullyOnSurface(int16_t x, int16_t y, uint16_t widthPx, uint16_t heightPx);
|
||||
static void spriteInitFields(jlSpriteT *sp, const uint8_t *tileData, uint8_t widthTiles, uint8_t heightTiles, bool ownsTileData);
|
||||
static void spriteDrawInterpreted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y);
|
||||
|
||||
|
||||
|
|
@ -85,34 +84,6 @@ static bool clipRect(int16_t *dstX, int16_t *dstY, int16_t *srcX, int16_t *srcY,
|
|||
}
|
||||
|
||||
|
||||
// Sample one source pixel from the sprite at (pxX, pxY) where (0,0)
|
||||
// is the sprite's top-left tile. The tile data is laid out tile by
|
||||
// tile in row-major order, each tile internally row-major with 4
|
||||
// bytes per row. High nibble = left pixel within each byte.
|
||||
static uint8_t srcNibble(const jlSpriteT *sp, int16_t pxX, int16_t pxY) {
|
||||
int16_t tileX;
|
||||
int16_t tileY;
|
||||
int16_t inTileX;
|
||||
int16_t inTileY;
|
||||
uint32_t byteOff;
|
||||
uint8_t byte;
|
||||
const uint8_t *tile;
|
||||
|
||||
tileX = (int16_t)(pxX / TILE_PIXELS);
|
||||
tileY = (int16_t)(pxY / TILE_PIXELS);
|
||||
inTileX = (int16_t)(pxX & (TILE_PIXELS - 1));
|
||||
inTileY = (int16_t)(pxY & (TILE_PIXELS - 1));
|
||||
|
||||
tile = sp->tileData + ((uint32_t)(tileY * sp->widthTiles + tileX)) * TILE_BYTES;
|
||||
byteOff = (uint32_t)(inTileY * TILE_BYTES_PER_ROW + (inTileX >> 1));
|
||||
byte = tile[byteOff];
|
||||
if (inTileX & 1) {
|
||||
return (uint8_t)(byte & 0x0F);
|
||||
}
|
||||
return (uint8_t)(byte >> 4);
|
||||
}
|
||||
|
||||
|
||||
static void writeDstNibble(uint8_t *row, int16_t x, uint8_t nibble) {
|
||||
uint8_t *byte;
|
||||
|
||||
|
|
@ -139,9 +110,34 @@ static bool isFullyOnSurface(int16_t x, int16_t y, uint16_t widthPx, uint16_t he
|
|||
}
|
||||
|
||||
|
||||
// Initialize a freshly-allocated jlSpriteT's fields to the
|
||||
// uncompiled/default state. routineOffsets is filled with 0xFF so
|
||||
// every (shift, op) entry reads as SPRITE_NOT_COMPILED (0xFFFF); 0
|
||||
// would be a VALID offset and could dispatch into stale arena bytes.
|
||||
static void spriteInitFields(jlSpriteT *sp, const uint8_t *tileData, uint8_t widthTiles, uint8_t heightTiles, bool ownsTileData) {
|
||||
sp->tileData = tileData;
|
||||
sp->widthTiles = widthTiles;
|
||||
sp->heightTiles = heightTiles;
|
||||
sp->ownsTileData = ownsTileData;
|
||||
sp->slot = NULL;
|
||||
memset(sp->routineOffsets, 0xFF, sizeof(sp->routineOffsets));
|
||||
memset(sp->cachedDstBank, 0xFF, sizeof(sp->cachedDstBank));
|
||||
memset(sp->cachedSrcBank, 0xFF, sizeof(sp->cachedSrcBank));
|
||||
memset(sp->cachedSizeBytes, 0, sizeof(sp->cachedSizeBytes));
|
||||
}
|
||||
|
||||
|
||||
// Pixel-by-pixel draw. Used directly when the sprite has no compiled
|
||||
// slot, and as the clip-edge fallback when a compiled draw would
|
||||
// extend off the surface.
|
||||
//
|
||||
// The source sprite is tile-major (8x8 tiles, 4 bytes/row, consecutive
|
||||
// tile columns 32 bytes apart). Rather than recompute tile/in-tile
|
||||
// coordinates per pixel (the old srcNibble did two divisions + a
|
||||
// multiply EVERY pixel), this walks one source-tile-row pointer that
|
||||
// advances by TILE_BYTES each time the column crosses an 8-pixel tile
|
||||
// boundary, so the inner loop is a byte read + nibble extract with no
|
||||
// division. ~5x on the interpreted path (the every-edge-clip fallback).
|
||||
static void spriteDrawInterpreted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y) {
|
||||
int16_t dx;
|
||||
int16_t dy;
|
||||
|
|
@ -150,17 +146,16 @@ static void spriteDrawInterpreted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16
|
|||
int16_t w;
|
||||
int16_t h;
|
||||
int16_t row;
|
||||
int16_t col;
|
||||
uint8_t nibble;
|
||||
uint8_t *dstRow;
|
||||
uint8_t wTiles;
|
||||
|
||||
dx = x;
|
||||
dy = y;
|
||||
w = (int16_t)(sp->widthTiles * TILE_PIXELS);
|
||||
h = (int16_t)(sp->heightTiles * TILE_PIXELS);
|
||||
w = (int16_t)(sp->widthTiles * TILE_PIXELS_PER_SIDE);
|
||||
h = (int16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
|
||||
if (!clipRect(&dx, &dy, &sx, &sy, &w, &h)) {
|
||||
return;
|
||||
}
|
||||
wTiles = sp->widthTiles;
|
||||
|
||||
/* Skip the chunky write loop on planar ports (s->pixels == NULL).
|
||||
* halSpriteDrawPlanes is called by the jlSpriteDraw caller and does
|
||||
|
|
@ -168,14 +163,41 @@ static void spriteDrawInterpreted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16
|
|||
* happen there. Phase 9 dropped the chunky shadow on Amiga. */
|
||||
if (s->pixels != NULL) {
|
||||
for (row = 0; row < h; row++) {
|
||||
dstRow = &s->pixels[(dy + row) * SURFACE_BYTES_PER_ROW];
|
||||
uint8_t *dstRow;
|
||||
const uint8_t *srcTileRow;
|
||||
uint16_t srcY;
|
||||
uint16_t tileRowBase;
|
||||
uint16_t inTileX;
|
||||
int16_t col;
|
||||
|
||||
dstRow = &s->pixels[SURFACE_ROW_OFFSET((uint16_t)(dy + row))];
|
||||
srcY = (uint16_t)(sy + row);
|
||||
/* Byte offset to tile-column 0's data for this pixel row:
|
||||
* (tileY * wTiles) tiles in, each TILE_BYTES, plus the
|
||||
* in-tile row. Shifts dodge multiply helpers
|
||||
* (TILE_BYTES == 1<<5, TILE_BYTES_PER_ROW == 1<<2). */
|
||||
tileRowBase = (uint16_t)((((uint16_t)(srcY >> 3) * wTiles) << 5)
|
||||
+ ((srcY & 7u) << 2));
|
||||
inTileX = (uint16_t)((uint16_t)sx & 7u);
|
||||
srcTileRow = sp->tileData + tileRowBase
|
||||
+ (((uint16_t)((uint16_t)sx >> 3)) << 5);
|
||||
|
||||
for (col = 0; col < w; col++) {
|
||||
nibble = srcNibble(sp, (int16_t)(sx + col), (int16_t)(sy + row));
|
||||
if (nibble == TRANSPARENT_NIBBLE) {
|
||||
continue;
|
||||
}
|
||||
uint8_t byte;
|
||||
uint8_t nibble;
|
||||
|
||||
byte = srcTileRow[inTileX >> 1];
|
||||
nibble = (inTileX & 1u) ? (uint8_t)(byte & 0x0Fu)
|
||||
: (uint8_t)(byte >> 4);
|
||||
if (nibble != TRANSPARENT_NIBBLE) {
|
||||
writeDstNibble(dstRow, (int16_t)(dx + col), nibble);
|
||||
}
|
||||
inTileX++;
|
||||
if (inTileX == TILE_PIXELS_PER_SIDE) {
|
||||
inTileX = 0;
|
||||
srcTileRow += TILE_BYTES;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
surfaceMarkDirtyRect(s, dx, dy, w, h);
|
||||
|
|
@ -194,15 +216,7 @@ jlSpriteT *jlSpriteCreate(const uint8_t *tileData, uint8_t widthTiles, uint8_t h
|
|||
if (sp == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
sp->tileData = tileData;
|
||||
sp->widthTiles = widthTiles;
|
||||
sp->heightTiles = heightTiles;
|
||||
sp->ownsTileData = false;
|
||||
sp->slot = NULL;
|
||||
memset(sp->routineOffsets, 0, sizeof(sp->routineOffsets));
|
||||
memset(sp->cachedDstBank, 0xFF, sizeof(sp->cachedDstBank));
|
||||
memset(sp->cachedSrcBank, 0xFF, sizeof(sp->cachedSrcBank));
|
||||
memset(sp->cachedSizeBytes, 0, sizeof(sp->cachedSizeBytes));
|
||||
spriteInitFields(sp, tileData, widthTiles, heightTiles, false);
|
||||
return sp;
|
||||
}
|
||||
|
||||
|
|
@ -226,11 +240,11 @@ jlSpriteT *jlSpriteCreateFromSurface(const jlSurfaceT *src, int16_t x, int16_t y
|
|||
// Source x/y must be on a tile boundary so each captured tile lands
|
||||
// on whole bytes -- mid-byte snapshots would lose half a pixel at
|
||||
// the left edge.
|
||||
if ((x & (TILE_PIXELS - 1)) != 0 || (y & (TILE_PIXELS - 1)) != 0) {
|
||||
if ((x & (TILE_PIXELS_PER_SIDE - 1)) != 0 || (y & (TILE_PIXELS_PER_SIDE - 1)) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
widthPx = (uint16_t)(widthTiles * TILE_PIXELS);
|
||||
heightPx = (uint16_t)(heightTiles * TILE_PIXELS);
|
||||
widthPx = (uint16_t)(widthTiles * TILE_PIXELS_PER_SIDE);
|
||||
heightPx = (uint16_t)(heightTiles * TILE_PIXELS_PER_SIDE);
|
||||
if (x < 0 || y < 0 ||
|
||||
x + (int16_t)widthPx > SURFACE_WIDTH ||
|
||||
y + (int16_t)heightPx > SURFACE_HEIGHT) {
|
||||
|
|
@ -251,8 +265,8 @@ jlSpriteT *jlSpriteCreateFromSurface(const jlSurfaceT *src, int16_t x, int16_t y
|
|||
for (ty = 0; ty < heightTiles; ty++) {
|
||||
for (tx = 0; tx < widthTiles; tx++) {
|
||||
dstTile = &buf[(ty * widthTiles + tx) * TILE_BYTES];
|
||||
for (row = 0; row < TILE_PIXELS; row++) {
|
||||
srcRow = &src->pixels[((uint16_t)y + ty * TILE_PIXELS + row) * SURFACE_BYTES_PER_ROW];
|
||||
for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) {
|
||||
srcRow = &src->pixels[((uint16_t)y + ty * TILE_PIXELS_PER_SIDE + row) * SURFACE_BYTES_PER_ROW];
|
||||
memcpy(&dstTile[row * TILE_BYTES_PER_ROW],
|
||||
&srcRow[((uint16_t)x >> 1) + tx * TILE_BYTES_PER_ROW],
|
||||
TILE_BYTES_PER_ROW);
|
||||
|
|
@ -275,10 +289,10 @@ jlSpriteT *jlSpriteCreateFromSurface(const jlSurfaceT *src, int16_t x, int16_t y
|
|||
for (ty = 0; ty < heightTiles; ty++) {
|
||||
for (tx = 0; tx < widthTiles; tx++) {
|
||||
dstTile = &buf[(ty * widthTiles + tx) * TILE_BYTES];
|
||||
for (row = 0; row < TILE_PIXELS; row++) {
|
||||
py = (uint16_t)((uint16_t)y + ty * TILE_PIXELS + row);
|
||||
for (col = 0; col < TILE_PIXELS; col += 2) {
|
||||
px = (uint16_t)((uint16_t)x + tx * TILE_PIXELS + col);
|
||||
for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) {
|
||||
py = (uint16_t)((uint16_t)y + ty * TILE_PIXELS_PER_SIDE + row);
|
||||
for (col = 0; col < TILE_PIXELS_PER_SIDE; col += 2) {
|
||||
px = (uint16_t)((uint16_t)x + tx * TILE_PIXELS_PER_SIDE + col);
|
||||
hiNibble = (uint8_t)(halSamplePixel(src, (int16_t)px, (int16_t)py) & 0x0Fu);
|
||||
loNibble = (uint8_t)(halSamplePixel(src, (int16_t)(px + 1), (int16_t)py) & 0x0Fu);
|
||||
dstTile[row * TILE_BYTES_PER_ROW + (col >> 1)] =
|
||||
|
|
@ -294,15 +308,7 @@ jlSpriteT *jlSpriteCreateFromSurface(const jlSurfaceT *src, int16_t x, int16_t y
|
|||
free(buf);
|
||||
return NULL;
|
||||
}
|
||||
sp->tileData = buf;
|
||||
sp->widthTiles = widthTiles;
|
||||
sp->heightTiles = heightTiles;
|
||||
sp->ownsTileData = true;
|
||||
sp->slot = NULL;
|
||||
memset(sp->routineOffsets, 0, sizeof(sp->routineOffsets));
|
||||
memset(sp->cachedDstBank, 0xFF, sizeof(sp->cachedDstBank));
|
||||
memset(sp->cachedSrcBank, 0xFF, sizeof(sp->cachedSrcBank));
|
||||
memset(sp->cachedSizeBytes, 0, sizeof(sp->cachedSizeBytes));
|
||||
spriteInitFields(sp, buf, widthTiles, heightTiles, true);
|
||||
return sp;
|
||||
}
|
||||
|
||||
|
|
@ -329,8 +335,8 @@ void jlSpriteDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y) {
|
|||
if (s == NULL || sp == NULL) {
|
||||
return;
|
||||
}
|
||||
widthPx = (uint16_t)(sp->widthTiles * TILE_PIXELS);
|
||||
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
|
||||
widthPx = (uint16_t)(sp->widthTiles * TILE_PIXELS_PER_SIDE);
|
||||
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
|
||||
|
||||
// Fast path: compiled bytes + fully on surface. Off-surface draws
|
||||
// fall back to the interpreter so the compiled routines never
|
||||
|
|
@ -384,8 +390,8 @@ void jlSpriteSaveAndDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlS
|
|||
hTiles = sp->heightTiles;
|
||||
slot = sp->slot;
|
||||
|
||||
widthPx = (uint16_t)(wTiles * TILE_PIXELS);
|
||||
heightPx = (uint16_t)(hTiles * TILE_PIXELS);
|
||||
widthPx = (uint16_t)(wTiles * TILE_PIXELS_PER_SIDE);
|
||||
heightPx = (uint16_t)(hTiles * TILE_PIXELS_PER_SIDE);
|
||||
|
||||
// Fast path: compiled bytes available, fully on surface, backup
|
||||
// buffer supplied. Save fills out backup->{x,y,width,height,
|
||||
|
|
@ -422,18 +428,8 @@ void jlSpriteSaveAndDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlS
|
|||
}
|
||||
|
||||
|
||||
// .spr file format:
|
||||
// offset bytes field
|
||||
// ------ ----- --------------------------------------------
|
||||
// 0 1 widthTiles
|
||||
// 1 1 heightTiles
|
||||
// 2 2 codeSize (LE16)
|
||||
// 4 2 tileBytes (LE16) = widthTiles*heightTiles*32
|
||||
// 6 ... offsets table (JOEY_SPRITE_SHIFT_COUNT *
|
||||
// SPRITE_OP_COUNT * uint16_t)
|
||||
// ... codeSize compiled code
|
||||
// ... tileBytes raw tile data (4bpp packed, tile-major)
|
||||
//
|
||||
// Defragment the codegen arena (compiled sprite slots). The live .spr
|
||||
// on-disk format is documented beside the loader in core/assetLoad.c.
|
||||
void jlSpriteCompact(void) {
|
||||
codegenArenaCompact();
|
||||
}
|
||||
|
|
@ -471,7 +467,7 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) {
|
|||
/* Validate. Note: SURFACE_WIDTH - bx and SURFACE_HEIGHT - by stay
|
||||
* in uint16_t range once bx >= 0 / by >= 0 has been checked, so
|
||||
* the right-edge / bottom-edge tests don't need 32-bit arithmetic
|
||||
* (which would invoke ORCA-C's ~GRTL helper, ~50 cyc per call). */
|
||||
* (which would invoke a costly long-compare helper). */
|
||||
if (backup->bytes == NULL ||
|
||||
bw == 0 || bh == 0 ||
|
||||
bx < 0 || by < 0 ||
|
||||
|
|
@ -483,11 +479,20 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) {
|
|||
}
|
||||
|
||||
sp = backup->sprite;
|
||||
if (sp != NULL && sp->slot != NULL && bh == sp->heightTiles * TILE_PIXELS) {
|
||||
if (sp != NULL && sp->slot != NULL && bh == sp->heightTiles * TILE_PIXELS_PER_SIDE) {
|
||||
uint16_t routeIdx;
|
||||
uint16_t routeOffset;
|
||||
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW);
|
||||
copyBytes = (int16_t)(bw >> 1);
|
||||
// The compiled restore routine walks a FIXED per-row byte count
|
||||
// derived purely from the chosen shift (spriteBytesPerRow for
|
||||
// shift 0, spriteBytesPerRow+1 for shift 1). A horizontally
|
||||
// clipped save records a narrower backup->width / copyBytes that
|
||||
// matches neither, so only take the fast path when copyBytes is
|
||||
// exactly one of those two compiled widths; any other width must
|
||||
// fall through to the interpreted memcpy that honors copyBytes.
|
||||
if (copyBytes == (int16_t)spriteBytesPerRow ||
|
||||
copyBytes == (int16_t)(spriteBytesPerRow + 1)) {
|
||||
shift = (copyBytes == (int16_t)spriteBytesPerRow) ? 0 : 1;
|
||||
/* Byte-pointer arithmetic dodges ~MUL4 for 2D-array indexing. */
|
||||
routeIdx = (uint16_t)(((uint16_t)shift << 1) + (uint16_t)shift + SPRITE_OP_RESTORE);
|
||||
|
|
@ -501,6 +506,7 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) {
|
|||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Slow / interpreted memcpy fallback. Skip the chunky memcpy if
|
||||
* the port has no chunky shadow (Phase 9 Amiga: s->pixels NULL);
|
||||
|
|
@ -526,7 +532,7 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) {
|
|||
|
||||
void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup) {
|
||||
/* Only fast-path locals here. Slow-path declarations live inside
|
||||
* the slow-path block below so ORCA-C with -b doesn't reserve
|
||||
* the slow-path block below so the compiler doesn't reserve
|
||||
* stack frame for them on every fast-path call. */
|
||||
uint16_t widthPx;
|
||||
uint16_t heightPx;
|
||||
|
|
@ -540,8 +546,8 @@ void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y,
|
|||
backup->sizeBytes = 0;
|
||||
|
||||
slot = sp->slot;
|
||||
widthPx = (uint16_t)(sp->widthTiles * TILE_PIXELS);
|
||||
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
|
||||
widthPx = (uint16_t)(sp->widthTiles * TILE_PIXELS_PER_SIDE);
|
||||
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
|
||||
|
||||
// Compiled fast path: fully on surface and the platform emitted
|
||||
// bytes for SAVE at this shift. The compiled routine assumes a
|
||||
|
|
@ -549,8 +555,8 @@ void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y,
|
|||
// through to the interpreted memcpy loop below.
|
||||
//
|
||||
// The routineOffsets[shift][SPRITE_OP_SAVE] access is rewritten as
|
||||
// explicit byte-pointer arithmetic to dodge ORCA-C 2.2.1's ~MUL4
|
||||
// helper that gets emitted for `uint16_t arr[N][M]` indexing.
|
||||
// explicit byte-pointer arithmetic to dodge the multiply helper
|
||||
// that gets emitted for `uint16_t arr[N][M]` indexing.
|
||||
if (backup->bytes != NULL && slot != NULL && isFullyOnSurface(x, y, widthPx, heightPx)) {
|
||||
uint16_t routeIdx;
|
||||
uint16_t routeOffset;
|
||||
|
|
|
|||
|
|
@ -61,8 +61,8 @@ struct jlSpriteT {
|
|||
uint8_t cachedSrcBank[JOEY_SPRITE_SHIFT_COUNT][SPRITE_OP_COUNT];
|
||||
|
||||
// Cached `copyBytes * heightPx` per shift for spriteCompiledSaveUnder's
|
||||
// `backup->sizeBytes` field. uint16_t * uint16_t goes through ORCA-C
|
||||
// 2.2.1's ~CUMUL2 helper (~30-50 cyc); cache hit dodges it. Filled
|
||||
// `backup->sizeBytes` field. uint16_t * uint16_t goes through a
|
||||
// software multiply helper (~30-50 cyc); cache hit dodges it. Filled
|
||||
// lazily on first call (0 sentinel = uncached).
|
||||
uint16_t cachedSizeBytes[JOEY_SPRITE_SHIFT_COUNT];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -30,20 +30,20 @@ uint8_t gStageMinWord[SURFACE_HEIGHT];
|
|||
uint8_t gStageMaxWord[SURFACE_HEIGHT];
|
||||
|
||||
// "Stage SCB / palette has changed since last present-side upload."
|
||||
// Cheap flag check at present time replaces the 200+512 byte memcmps
|
||||
// the IIgs port used to run every frame in halPresentRect's
|
||||
// uploadScbAndPaletteIfNeeded -- ~7 ms / frame saved on demos that
|
||||
// don't churn palette/SCB (i.e., almost all demos).
|
||||
// A cheap flag check at present time lets a port skip re-uploading the
|
||||
// 200-byte SCB and 512-byte palette when neither changed -- ~7 ms /
|
||||
// frame saved on demos that don't churn palette/SCB (i.e., almost all
|
||||
// demos).
|
||||
//
|
||||
// Initially true so the first present uploads. jlScbSet*/jlPaletteSet
|
||||
// re-mark dirty when the stage's data changes; per-port present code
|
||||
// clears the flag after consuming.
|
||||
// Initially true so the first present uploads. jlScbSet*, jlPaletteSet,
|
||||
// and the bulk stage-replace paths (jlSurfaceCopy / jlSurfaceLoadFile)
|
||||
// re-mark dirty when the stage's data changes; the per-port present
|
||||
// code reads and clears the flags after uploading.
|
||||
bool gStageScbDirty = true;
|
||||
bool gStagePaletteDirty = true;
|
||||
|
||||
// ----- Internal helpers (alphabetical) -----
|
||||
|
||||
#ifndef JOEYLIB_PLATFORM_IIGS
|
||||
static void widenRow(int16_t y, uint8_t minWord, uint8_t maxWord) {
|
||||
if (minWord < gStageMinWord[y]) {
|
||||
gStageMinWord[y] = minWord;
|
||||
|
|
@ -52,7 +52,6 @@ static void widenRow(int16_t y, uint8_t minWord, uint8_t maxWord) {
|
|||
gStageMaxWord[y] = maxWord;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// ----- Public API (alphabetical) -----
|
||||
|
||||
|
|
@ -70,6 +69,13 @@ void jlSurfaceCopy(jlSurfaceT *dst, const jlSurfaceT *src) {
|
|||
memcpy(dst->palette, src->palette, sizeof(src->palette));
|
||||
halSurfaceCopyPlanes(dst, src); /* 4 plane memcpys on planar ports; no-op on chunky */
|
||||
surfaceMarkDirtyAll(dst);
|
||||
// Bulk-replacing the stage's SCB/palette must re-mark them dirty so
|
||||
// the per-port present re-uploads them; surfaceMarkDirtyAll only
|
||||
// widens the pixel bands. Matches the jlScbSet / jlPaletteSet path.
|
||||
if (dst == gStage) {
|
||||
gStageScbDirty = true;
|
||||
gStagePaletteDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -78,12 +84,31 @@ jlSurfaceT *jlSurfaceCreate(void) {
|
|||
|
||||
s = (jlSurfaceT *)calloc(1, sizeof(jlSurfaceT));
|
||||
if (s == NULL) {
|
||||
coreSetError("out of memory allocating surface");
|
||||
return NULL;
|
||||
}
|
||||
/* halSurfaceAllocPixels returns NULL on planar ports (Amiga); the
|
||||
* primary storage is the port-allocated planes via portData below. */
|
||||
// halSurfaceAllocPixels returns NULL on planar ports (Amiga); the
|
||||
// primary storage is the port-allocated planes via portData below.
|
||||
// Classify NULL by the build's storage model: chunky ports need
|
||||
// pixels, planar ports need portData. A NULL in the required slot
|
||||
// is an out-of-memory failure (header contract: return NULL).
|
||||
s->pixels = halSurfaceAllocPixels();
|
||||
s->portData = halSurfaceAllocPortData(s, false);
|
||||
#ifdef JOEYLIB_NATIVE_CHUNKY
|
||||
if (s->pixels == NULL) {
|
||||
halSurfaceFreePortData(s, false, s->portData);
|
||||
free(s);
|
||||
coreSetError("out of memory allocating surface pixels");
|
||||
return NULL;
|
||||
}
|
||||
#else
|
||||
if (s->portData == NULL) {
|
||||
halSurfaceFreePixels(s->pixels);
|
||||
free(s);
|
||||
coreSetError("out of memory allocating surface planes");
|
||||
return NULL;
|
||||
}
|
||||
#endif
|
||||
paletteInitDefault(s);
|
||||
return s;
|
||||
}
|
||||
|
|
@ -102,30 +127,11 @@ void jlSurfaceDestroy(jlSurfaceT *s) {
|
|||
}
|
||||
|
||||
|
||||
// Cheapest deterministic hash that still detects per-byte changes:
|
||||
// (hash << 1) ^ byte, a single 16-bit accumulator. ORCA-C / 65816
|
||||
// compiles to ASL + EOR -- about 35 cyc per byte. A 32-bit multiply
|
||||
// FNV-style hash takes ~200 cyc per byte via ~UMUL4, which adds
|
||||
// 80+ seconds to a UBER run on IIgs. Discrimination is weaker than
|
||||
// FNV but plenty for cross-port validation: we only need "did the
|
||||
// same logical-pixel sequence produce the same hash?" -- not
|
||||
// crypto-grade collision resistance over arbitrary inputs.
|
||||
//
|
||||
// Walks the chunky pixel buffer byte-by-byte, the same logical-pixel
|
||||
// ordering on every chunky-format port (IIgs, DOS, Amiga and ST
|
||||
// while still chunky). When the planar rewrite drops s->pixels on
|
||||
// Amiga/ST this function will need a HAL hook (halSurfaceHash) to
|
||||
// read planes natively while producing the same logical hash.
|
||||
/* Cross-port FNV-style hash of pixels + SCB + palette. The hash logic
|
||||
* (multiplier streams, byte ordering for palette) is identical across
|
||||
* ports, but the pixel READS go through the port HAL so chunky ports
|
||||
* walk s->pixels and planar ports walk plane bits and assemble nibble
|
||||
* pairs into chunky bytes for the hash. Both produce the same logical-
|
||||
* pixel hash because they hash the same logical pixel sequence in the
|
||||
* same chunky byte order. SCB and palette are still hashed inline
|
||||
* here because they live in the jlSurfaceT struct on every port (no
|
||||
* port-specific storage) and the byte/value-with-explicit-byte-order
|
||||
* walks are already endian-independent. */
|
||||
// Cross-port hash of the surface's logical pixel content (plus SCB and
|
||||
// palette), delegated to the port HAL so each port reads its native
|
||||
// pixel storage (chunky on IIgs/DOS, planes on Amiga/ST) while
|
||||
// producing the same logical-pixel hash. Used by the UBER validation
|
||||
// harness to pixel-compare ports against an IIgs golden reference.
|
||||
uint32_t jlSurfaceHash(const jlSurfaceT *s) {
|
||||
if (s == NULL) {
|
||||
return 0u;
|
||||
|
|
@ -172,6 +178,12 @@ bool jlSurfaceLoadFile(jlSurfaceT *dst, const char *path) {
|
|||
}
|
||||
fclose(fp);
|
||||
surfaceMarkDirtyAll(dst);
|
||||
/* Loading a file into the stage overwrites its SCB/palette; re-mark
|
||||
* them dirty so the next present re-uploads them. */
|
||||
if (dst == gStage) {
|
||||
gStageScbDirty = true;
|
||||
gStagePaletteDirty = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -204,15 +216,14 @@ bool jlSurfaceSaveFile(const jlSurfaceT *src, const char *path) {
|
|||
|
||||
|
||||
void surfaceMarkDirtyAll(const jlSurfaceT *s) {
|
||||
int16_t row;
|
||||
|
||||
if (s != gStage) {
|
||||
return;
|
||||
}
|
||||
for (row = 0; row < SURFACE_HEIGHT; row++) {
|
||||
gStageMinWord[row] = 0;
|
||||
gStageMaxWord[row] = (uint8_t)(SURFACE_WORDS_PER_ROW - 1);
|
||||
}
|
||||
// Whole-stage mark is a constant fill of both band arrays; memset
|
||||
// lowers to a tight fill (MVN-seeded on IIgs) and drops the 200-iter
|
||||
// indexed-store loop. One source of truth for "all rows fully dirty".
|
||||
memset(gStageMinWord, 0, SURFACE_HEIGHT);
|
||||
memset(gStageMaxWord, STAGE_DIRTY_FULL_MAX, SURFACE_HEIGHT);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -234,15 +245,23 @@ void surfaceMarkDirtyRect(const jlSurfaceT *s, int16_t x, int16_t y, int16_t w,
|
|||
if (w <= 0 || h <= 0) {
|
||||
return;
|
||||
}
|
||||
/* Clipped x/w are non-negative; cast to uint16_t before `>> 2` so
|
||||
* ORCA-C lowers to a pair of LSRs instead of the ~SSHIFTRIGHT
|
||||
* helper signed shifts emit. */
|
||||
minWord = (uint8_t)((uint16_t)x >> 2);
|
||||
maxWord = (uint8_t)((uint16_t)(x + w - 1) >> 2);
|
||||
/* Clipped x/w are non-negative; SURFACE_WORD_INDEX casts to uint16_t
|
||||
* before `>> 2` so the shift lowers to a pair of LSRs instead of a
|
||||
* signed-shift helper. */
|
||||
minWord = SURFACE_WORD_INDEX(x);
|
||||
maxWord = SURFACE_WORD_INDEX(x + w - 1);
|
||||
yEnd = y + h;
|
||||
#ifdef JOEYLIB_PLATFORM_IIGS
|
||||
// Single-row marks (every jlDrawPixel, every horizontal span) widen
|
||||
// inline in C; this is the common case and avoids the cross-segment
|
||||
// JSL into iigsMarkDirtyRowsInner. Multi-row marks still use the asm
|
||||
// marker, where the per-call JSL is amortized across the rows.
|
||||
if (h == 1) {
|
||||
widenRow(y, minWord, maxWord);
|
||||
} else {
|
||||
iigsMarkDirtyRowsInner((uint16_t)y, (uint16_t)yEnd,
|
||||
(uint16_t)minWord, (uint16_t)maxWord);
|
||||
}
|
||||
#else
|
||||
for (row = y; row < yEnd; row++) {
|
||||
widenRow(row, minWord, maxWord);
|
||||
|
|
@ -261,14 +280,31 @@ bool stageAlloc(void) {
|
|||
if (gStage == NULL) {
|
||||
return false;
|
||||
}
|
||||
/* halStageAllocPixels returns NULL on planar ports (Amiga) where
|
||||
* the chunky shadow doesn't exist; the planes from portData are
|
||||
* the source of truth. NULL pixels is no longer a failure. */
|
||||
// halStageAllocPixels returns NULL on planar ports (Amiga) where
|
||||
// the chunky shadow doesn't exist; the planes from portData are
|
||||
// the source of truth. On chunky ports NULL pixels means the
|
||||
// back-buffer alloc failed (IIgs pins a fixed address and never
|
||||
// returns NULL, so this is a real OOM only on DOS).
|
||||
gStage->pixels = halStageAllocPixels();
|
||||
if (gStage->pixels != NULL) {
|
||||
memset(gStage->pixels, 0, SURFACE_PIXELS_SIZE);
|
||||
}
|
||||
gStage->portData = halSurfaceAllocPortData(gStage, true);
|
||||
#ifdef JOEYLIB_NATIVE_CHUNKY
|
||||
if (gStage->pixels == NULL) {
|
||||
halSurfaceFreePortData(gStage, true, gStage->portData);
|
||||
free(gStage);
|
||||
gStage = NULL;
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
if (gStage->portData == NULL) {
|
||||
halStageFreePixels(gStage->pixels);
|
||||
free(gStage);
|
||||
gStage = NULL;
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
stageDirtyClearAll();
|
||||
paletteInitDefault(gStage);
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -33,13 +33,32 @@ struct jlSurfaceT {
|
|||
// 4 px per 16-bit word. SURFACE_BYTES_PER_ROW (160) / 2 = 80 words.
|
||||
// Dirty tracking grain is 16-bit words because that matches the IIgs
|
||||
// PEI / PHA slam unit and the Amiga / ST c2p group is 16 px = 4 words.
|
||||
#define SURFACE_PIXELS_PER_WORD 4
|
||||
#define SURFACE_WORDS_PER_ROW (SURFACE_BYTES_PER_ROW / 2)
|
||||
|
||||
// x pixel -> dirty word index (4 px per 16-bit word). x is non-negative
|
||||
// at every call site (clipped to surface bounds), so it is cast to
|
||||
// uint16_t before the shift to keep the compiler from emitting a signed
|
||||
// shift helper.
|
||||
#define SURFACE_WORD_INDEX(_x) ((uint8_t)((uint16_t)(_x) >> 2))
|
||||
|
||||
// Index of the last word in a fully-dirty row.
|
||||
#define STAGE_DIRTY_FULL_MAX ((uint8_t)(SURFACE_WORDS_PER_ROW - 1))
|
||||
|
||||
// Sentinels for "row is clean": min > max can never happen for a real
|
||||
// dirty range, so the present loop tests `min > max` to skip a row.
|
||||
#define STAGE_DIRTY_CLEAN_MIN 0xFFu
|
||||
#define STAGE_DIRTY_CLEAN_MAX 0x00u
|
||||
|
||||
// True when row `y`'s dirty band is the CLEAN sentinel (min > max).
|
||||
#define STAGE_DIRTY_ROW_CLEAN(_y) (gStageMinWord[_y] > gStageMaxWord[_y])
|
||||
|
||||
// The per-row dirty bands are stored as uint8_t, which is only valid
|
||||
// because SURFACE_WORDS_PER_ROW - 1 (79) fits in a byte. A future width
|
||||
// change that pushes the word count past 255 must fail to compile here
|
||||
// rather than silently truncate the band arrays.
|
||||
typedef char surfaceDirtyBandFitsByte[(SURFACE_WORDS_PER_ROW - 1) <= 0xFF ? 1 : -1];
|
||||
|
||||
// Per-row dirty word bands for the stage. gStageMinWord[y] is the
|
||||
// leftmost dirty 16-bit column on row y (inclusive); gStageMaxWord[y]
|
||||
// is the rightmost (inclusive). Both default to the CLEAN sentinels
|
||||
|
|
@ -48,8 +67,9 @@ extern uint8_t gStageMinWord[SURFACE_HEIGHT];
|
|||
extern uint8_t gStageMaxWord[SURFACE_HEIGHT];
|
||||
|
||||
// Per-byte mixer for jlSurfaceHash. Two-stream: lo *= 31 + b, hi *= 251 + b.
|
||||
// Strength-reduced to shifts so ORCA-C doesn't emit `~UMUL2` (~150 cyc
|
||||
// per call); 32 KB hashed twice -> ~5 minutes per UBER run. The
|
||||
// Strength-reduced to shifts so the compiler doesn't emit a software
|
||||
// multiply (~150 cyc per call); 32 KB hashed twice -> ~5 minutes per
|
||||
// UBER run. The
|
||||
// shift form is 16-bit-equivalent (mod 2^16) so hash values are
|
||||
// identical to the original `* 31u` / `* 251u` form.
|
||||
// lo *= 31 == (lo << 5) - lo
|
||||
|
|
@ -94,8 +114,8 @@ void stageDirtyClearAll(void);
|
|||
// On IIgs this expands to a single indexed long-mode read against
|
||||
// gRowOffsetLut (built once at halInit).
|
||||
//
|
||||
// The explicit (y << 1) byte-pointer arithmetic dodges ORCA-C 2.2.1's
|
||||
// `~MUL4` helper that gets emitted for `uint16_t arr[N]` indexing
|
||||
// The explicit (y << 1) byte-pointer arithmetic dodges the multiply
|
||||
// helper that gets emitted for `uint16_t arr[N]` indexing
|
||||
// (the implicit *sizeof(uint16_t)). With the byte-cast + shift, the
|
||||
// compiler emits a single ASL + indexed long-mode read.
|
||||
//
|
||||
|
|
@ -109,6 +129,11 @@ extern const uint16_t gRowOffsetLut[200];
|
|||
#define SURFACE_ROW_OFFSET(_y) ((uint16_t)((uint16_t)(_y) * SURFACE_BYTES_PER_ROW))
|
||||
#endif
|
||||
|
||||
// Record a human-readable error string for jlLastError(). Defined in
|
||||
// init.c; usable by other core TUs (e.g. surface.c on allocation /
|
||||
// limit failure). The string must have static lifetime.
|
||||
void coreSetError(const char *message);
|
||||
|
||||
// Allocate and free the library-owned stage (the back-buffer surface
|
||||
// that jlStagePresent flips to the display). Called from init.c during
|
||||
// jlInit / jlShutdown. The stage's pixel storage is supplied by
|
||||
|
|
|
|||
|
|
@ -16,11 +16,8 @@
|
|||
|
||||
// (No <string.h> -- the 4-byte-per-row inner copies are spelled out
|
||||
// inline below. Avoiding memcpy / memset from the DRAWPRIMS load
|
||||
// segment keeps cross-bank relocation references out of 13/SysLib;
|
||||
// without that the ORCA Linker hits "Expression too complex" on
|
||||
// the small-binary builds.)
|
||||
// segment keeps cross-bank relocation references out of 13/SysLib.)
|
||||
|
||||
JOEYLIB_SEGMENT("DRAWPRIMS")
|
||||
|
||||
// ----- Prototypes -----
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ extern UBYTE mt_E8Trigger;
|
|||
#define PAULA_CLOCK_PAL 3546895UL
|
||||
#define PAULA_PERIOD_MIN 124 // Paula HW limit
|
||||
#define PAULA_PERIOD_MAX 65535
|
||||
#define PAULA_CHANNELS 4 // hardware audio channel count
|
||||
|
||||
#define SFX_VOLUME_MAX 64
|
||||
|
||||
|
|
@ -289,9 +290,9 @@ void halAudioStopSfx(uint8_t slot) {
|
|||
// PTPlayer addresses SFX by Paula channel (0..3), but mt_playfx
|
||||
// picks a channel dynamically and does not return which one. We
|
||||
// can't reliably map slot -> channel after the fact, so stop all
|
||||
// four channels' SFX state. mt_stopfx is idempotent on idle
|
||||
// channels, so stopping a quiet one is harmless.
|
||||
for (i = 0; i < 4; i++) {
|
||||
// PAULA_CHANNELS channels' SFX state. mt_stopfx is idempotent on
|
||||
// idle channels, so stopping a quiet one is harmless.
|
||||
for (i = 0; i < PAULA_CHANNELS; i++) {
|
||||
mt_stopfx((void *)&custom, i);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@
|
|||
#include <graphics/view.h>
|
||||
|
||||
#include <hardware/custom.h>
|
||||
#include <hardware/blit.h>
|
||||
|
||||
#include <proto/exec.h>
|
||||
#include <proto/intuition.h>
|
||||
|
|
@ -48,7 +49,6 @@
|
|||
#include "hal.h"
|
||||
#include "spriteInternal.h"
|
||||
#include "surfaceInternal.h"
|
||||
#include "draw68k_inline.h"
|
||||
|
||||
extern struct Custom custom;
|
||||
|
||||
|
|
@ -86,6 +86,8 @@ typedef struct {
|
|||
|
||||
// ----- Prototypes -----
|
||||
|
||||
static void amigaBlitterCopyPlanes(uint8_t *dst[AMIGA_BITPLANES], uint8_t *src[AMIGA_BITPLANES], uint16_t widthWords, uint16_t height);
|
||||
static void amigaBlitterFillOne(uint8_t *dst, uint16_t widthWords, uint16_t height, uint16_t dstModBytes, bool setBits);
|
||||
static void amigaPlanarSetPixel(AmigaPlanarT *pd, int16_t x, int16_t y, uint8_t color);
|
||||
static void buildCopperList(const jlSurfaceT *src);
|
||||
static void installCopperList(void);
|
||||
|
|
@ -124,6 +126,16 @@ static volatile uint16_t *gBplPokes[AMIGA_BITPLANES][2]; // [plane][0=PTH,1=PTL
|
|||
static uint8_t gPrevStageMinWord[SURFACE_HEIGHT];
|
||||
static uint8_t gPrevStageMaxWord[SURFACE_HEIGHT];
|
||||
|
||||
// "The previous frame's snapshot recorded at least one dirty row."
|
||||
// Lets halPresent early-out of the whole union scan + 400-byte
|
||||
// snapshot copy on consecutive idle frames: if neither the previous
|
||||
// frame nor the current frame touched any row, union(prev,current) is
|
||||
// empty AND the snapshot would copy clean->clean (a no-op), so there
|
||||
// is nothing to present and nothing to record. Seeded true to match
|
||||
// the "all dirty" bootstrap of gPrevStage* above, so the first two
|
||||
// presents still run.
|
||||
static bool gPrevAnyDirty = true;
|
||||
|
||||
// Cached SCB + palettes from the last present. halPresent* only needs
|
||||
// to rebuild/install the copper list when SCB assignments or palette
|
||||
// RGB values differ from what is already on screen; pure pixel updates
|
||||
|
|
@ -143,6 +155,86 @@ static bool paletteOrScbChanged(const jlSurfaceT *src);
|
|||
|
||||
// ----- Internal helpers (alphabetical) -----
|
||||
|
||||
// Volatile view of the custom-chip register block. The Custom struct
|
||||
// fields are plain (non-volatile) UWORD/APTR, so a direct
|
||||
// `custom.bltcon0 = x` is eligible for -O2 reordering / dead-store
|
||||
// elimination. Accessing every blitter register through a volatile *
|
||||
// makes each member write volatile-qualified, which (a) keeps the
|
||||
// compiler from reordering BLTSIZE (the start trigger) ahead of the
|
||||
// setup writes and (b) stops it from dropping a "redundant" register
|
||||
// store. This is the standard idiom for poking the custom chip from C.
|
||||
static volatile struct Custom *const gCust = &custom;
|
||||
|
||||
// Blitter geometry shared by every block op here: each plane row is
|
||||
// AMIGA_BYTES_PER_ROW (40) bytes = 20 words. BLTSIZE packs the row
|
||||
// count in the high 10 bits (<< 6) and the word count in the low 6
|
||||
// bits (a width of 0 would mean 64 words, but our rows are 20 wide).
|
||||
#define AMIGA_BLT_WORDS_PER_ROW (AMIGA_BYTES_PER_ROW / 2u) // 20
|
||||
#define AMIGA_BLT_SIZESHIFT 6
|
||||
#define AMIGA_BLT_MASK_ALL 0xFFFFu
|
||||
|
||||
// Blitter A->D straight copy of `height` rows of `widthWords` words
|
||||
// across all four planes. src[i]/dst[i] are the per-plane start
|
||||
// addresses (already advanced to the first row/byte to copy by the
|
||||
// caller). Both must be CHIP RAM and word-aligned; rows are
|
||||
// contiguous (mod = 0) because every copy here is whole rows of a
|
||||
// 40-byte-stride plane. minterm $F0 (A_TO_D): D := A, no read of B/C/D.
|
||||
//
|
||||
// OwnBlitter once for the whole group; WaitBlit before each register
|
||||
// setup (the blitter is single-resource, so we must wait for the
|
||||
// previous plane's blit to drain before reprogramming) and a final
|
||||
// WaitBlit before DisownBlitter so the planes are fully written when
|
||||
// the caller (or the present copy) next reads them.
|
||||
static void amigaBlitterCopyPlanes(uint8_t *dst[AMIGA_BITPLANES], uint8_t *src[AMIGA_BITPLANES],
|
||||
uint16_t widthWords, uint16_t height) {
|
||||
uint16_t i;
|
||||
uint16_t bltsize;
|
||||
|
||||
bltsize = (uint16_t)((height << AMIGA_BLT_SIZESHIFT) | widthWords);
|
||||
OwnBlitter();
|
||||
for (i = 0; i < AMIGA_BITPLANES; i++) {
|
||||
WaitBlit();
|
||||
gCust->bltcon0 = (uint16_t)(SRCA | DEST | A_TO_D); // $09F0
|
||||
gCust->bltcon1 = 0u;
|
||||
gCust->bltafwm = AMIGA_BLT_MASK_ALL;
|
||||
gCust->bltalwm = AMIGA_BLT_MASK_ALL;
|
||||
gCust->bltamod = 0u;
|
||||
gCust->bltdmod = 0u;
|
||||
gCust->bltapt = (APTR)src[i];
|
||||
gCust->bltdpt = (APTR)dst[i];
|
||||
gCust->bltsize = bltsize; // starts the blit
|
||||
}
|
||||
WaitBlit();
|
||||
DisownBlitter();
|
||||
}
|
||||
|
||||
|
||||
// Blitter DEST-only block fill of one plane: `height` rows of
|
||||
// `widthWords` words, with `dstModBytes` added to the dest pointer
|
||||
// after each row (0 for a full-stride fill, (40 - rowBytes) for a
|
||||
// sub-row rect). setBits selects the constant written: $FFFF (set,
|
||||
// minterm $FF) or $0000 (clear, minterm $00). USED only -- no source
|
||||
// channel is read, so the minterm alone determines the output and the
|
||||
// fill needs no source data in memory. Caller owns the blitter and
|
||||
// has already WaitBlit'd if a prior blit may be in flight; this
|
||||
// routine WaitBlit's before its own register setup for safety when
|
||||
// called back-to-back across planes.
|
||||
static void amigaBlitterFillOne(uint8_t *dst, uint16_t widthWords, uint16_t height,
|
||||
uint16_t dstModBytes, bool setBits) {
|
||||
uint16_t minterm;
|
||||
|
||||
minterm = setBits ? 0x00FFu : 0x0000u;
|
||||
WaitBlit();
|
||||
gCust->bltcon0 = (uint16_t)(DEST | minterm);
|
||||
gCust->bltcon1 = 0u;
|
||||
gCust->bltafwm = AMIGA_BLT_MASK_ALL;
|
||||
gCust->bltalwm = AMIGA_BLT_MASK_ALL;
|
||||
gCust->bltdmod = dstModBytes;
|
||||
gCust->bltdpt = (APTR)dst;
|
||||
gCust->bltsize = (uint16_t)((height << AMIGA_BLT_SIZESHIFT) | widthWords);
|
||||
}
|
||||
|
||||
|
||||
// Build a user copper list for per-scanline palette (SCB emulation).
|
||||
// One WAIT + 16 MOVEs per displayed scanline + one CEND. The list is
|
||||
// stored in gNewUCL until installCopperList swaps it onto the screen.
|
||||
|
|
@ -647,6 +739,31 @@ void halPresent(const jlSurfaceT *src) {
|
|||
return;
|
||||
}
|
||||
|
||||
// Idle-frame early-out. When the previous frame's snapshot recorded
|
||||
// no dirty rows (gPrevAnyDirty == false) AND the current frame
|
||||
// touched no row, union(prev,current) is empty: there is nothing to
|
||||
// copy, no buffer to flip, and the snapshot below would only copy
|
||||
// clean->clean. Skip the whole 200-row union scan and the 400-byte
|
||||
// snapshot copy in that case. A row is dirty iff its min word was
|
||||
// widened below STAGE_DIRTY_CLEAN_MIN, so a single byte test per row
|
||||
// with early exit on the first dirty row suffices. Copper/palette
|
||||
// updates already ran above, so display state stays correct. This
|
||||
// cannot be taken while a stale band from the previous frame is
|
||||
// still pending (gPrevAnyDirty guards that), preserving the
|
||||
// 2-buffer staleness convergence.
|
||||
if (!gPrevAnyDirty) {
|
||||
bool anyDirty = false;
|
||||
for (y = 0; y < SURFACE_HEIGHT; y++) {
|
||||
if (gStageMinWord[y] != STAGE_DIRTY_CLEAN_MIN) {
|
||||
anyDirty = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!anyDirty) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 10 page-flip:
|
||||
// * Write to the OFF-screen plane set so the beam never crosses
|
||||
// in-flight pixels (no tearing regardless of copy speed).
|
||||
|
|
@ -687,6 +804,16 @@ void halPresent(const jlSurfaceT *src) {
|
|||
}
|
||||
|
||||
if (firstRow >= 0) {
|
||||
// The present copy reads pd->planes (the shadow) with the CPU.
|
||||
// The bulk plane ops (clear / full-row fill / surface copy) now
|
||||
// run on the blitter; each of those helpers WaitBlit's before
|
||||
// returning, so the blitter is normally idle here. Add a
|
||||
// WaitBlit anyway before the CPU read to guarantee no blit is
|
||||
// still in flight against the shadow planes -- a CPU read racing
|
||||
// a blitter write would copy half-written rows to the display.
|
||||
// WaitBlit is a no-op when the blitter is already idle and does
|
||||
// not require OwnBlitter.
|
||||
WaitBlit();
|
||||
// Each planar byte covers 8 px = 2 chunky words.
|
||||
byteStart = (uint16_t)(unionMinWord >> 1);
|
||||
bytesPerRow = (uint16_t)(((unionMaxWord >> 1) + 1u) - byteStart);
|
||||
|
|
@ -710,14 +837,22 @@ void halPresent(const jlSurfaceT *src) {
|
|||
|
||||
// Snapshot current dirty bands as "previous" for next present.
|
||||
// jlStagePresent clears the live bands after halPresent returns,
|
||||
// so we have to grab them now or they're gone.
|
||||
// so we have to grab them now or they're gone. Record whether any
|
||||
// row was dirty so the next present can take the idle-frame early-
|
||||
// out above. A row is dirty iff its min word stayed below the clean
|
||||
// sentinel.
|
||||
{
|
||||
uint16_t i;
|
||||
bool anyDirty = false;
|
||||
for (i = 0; i < SURFACE_HEIGHT; i++) {
|
||||
gPrevStageMinWord[i] = gStageMinWord[i];
|
||||
gPrevStageMaxWord[i] = gStageMaxWord[i];
|
||||
if (gStageMinWord[i] != STAGE_DIRTY_CLEAN_MIN) {
|
||||
anyDirty = true;
|
||||
}
|
||||
}
|
||||
gPrevAnyDirty = anyDirty;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -746,9 +881,11 @@ static bool gVblInstalled = false;
|
|||
// Server protocol: called by the interrupt dispatcher with A1 =
|
||||
// is_Data, A6 = ExecBase. We compile without fbaserel so the small-
|
||||
// data ABI is not in use; gFrameCount is a plain absolute global and
|
||||
// no __saveds is needed to reach it. Return Z=0 (non-zero result) to
|
||||
// keep the chain going so other VBL servers further down the priority
|
||||
// list still fire.
|
||||
// no __saveds is needed to reach it. Return 0 (D0 zero, Z flag set)
|
||||
// so the interrupt dispatcher treats this server as "not claimed" and
|
||||
// continues walking the INTB_VERTB chain, letting other VBL servers
|
||||
// further down the priority list still fire. Returning non-zero would
|
||||
// claim the interrupt and terminate the chain.
|
||||
static ULONG vblServer(void) {
|
||||
gFrameCount = (uint16_t)(gFrameCount + 1u);
|
||||
return 0;
|
||||
|
|
@ -837,19 +974,19 @@ void halShutdown(void) {
|
|||
FreeMem(gBitMap, (ULONG)sizeof(struct BitMap));
|
||||
gBitMap = NULL;
|
||||
}
|
||||
if (gNewUCL != NULL) {
|
||||
FreeMem(gNewUCL, sizeof(struct UCopList));
|
||||
gNewUCL = NULL;
|
||||
}
|
||||
// gNewUCL is never live at shutdown: buildCopperList and
|
||||
// installCopperList run back-to-back inside updateCopperIfNeeded,
|
||||
// so the built list is always consumed (gNewUCL == NULL) before we
|
||||
// get here. A bare FreeMem of the UCopList struct would also leak
|
||||
// the attached CopList chain that CINIT/CWAIT/CMOVE allocate, so we
|
||||
// deliberately do NOT attempt to free it here.
|
||||
}
|
||||
|
||||
|
||||
// Shared 68k fast paths for the chunky surface buffer (src/shared68k/
|
||||
// surface68k.s). Same primitives used by the Atari ST port -- the
|
||||
// stage / surfaces are an identical 4bpp packed layout on both.
|
||||
extern void surface68kClearLong(uint8_t *pixels, uint16_t fillByte);
|
||||
extern void surface68kFillRectFull(uint8_t *pixels, int16_t y, uint16_t h, uint16_t fillByte);
|
||||
extern void surface68kFillRectByteAligned(uint8_t *rowFirst, uint16_t midBytes, uint16_t h, uint16_t fillByte);
|
||||
// Shared 68k fast paths from src/shared68k/surface68k.s. Only the
|
||||
// planar span fill and the planar circle outline apply on Amiga (pure
|
||||
// planar after Phase 9); the chunky-layout primitives that file also
|
||||
// exports are unused here.
|
||||
extern void surface68kFillSpan4Planes(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3,
|
||||
uint16_t numMid, uint8_t leftMask, uint8_t rightMask,
|
||||
uint8_t fb0, uint8_t fb1, uint8_t fb2, uint8_t fb3);
|
||||
|
|
@ -859,10 +996,14 @@ extern void surface68kAmigaCircleOutline(uint8_t *p0, uint8_t *p1, uint8_t *p2,
|
|||
|
||||
// Phase 3 planar dual-write: write the bitplanes alongside the
|
||||
// Phase 9: Amiga is pure planar. Every halFast* below returns true
|
||||
// to suppress the cross-platform chunky fallback path -- there is no
|
||||
// chunky shadow on Amiga (s->pixels is NULL post-Phase 9). The actual
|
||||
// planar work happens in dedicated halXxxPlanes hooks called by
|
||||
// cross-platform code AFTER each halFast*.
|
||||
// for any surface that has planar storage (s->portData != NULL) to
|
||||
// suppress the cross-platform chunky fallback path -- there is no
|
||||
// chunky shadow on Amiga (s->pixels is NULL post-Phase 9). The gate
|
||||
// is portData presence, NOT stage identity, so non-stage surfaces
|
||||
// created by jlSurfaceCreate use their OWN planes instead of falling
|
||||
// into the chunky C fallback (which would deref s->pixels == NULL).
|
||||
// The actual planar work happens either inline here or in dedicated
|
||||
// halXxxPlanes hooks called by cross-platform code AFTER each halFast*.
|
||||
//
|
||||
// halFastSurfaceClear inlines the planar fill (no separate hook).
|
||||
// All other halFast* are short-circuit stubs that return true.
|
||||
|
|
@ -870,19 +1011,29 @@ bool halFastSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
|
|||
AmigaPlanarT *pd;
|
||||
uint8_t colorIndex;
|
||||
uint16_t i;
|
||||
uint8_t planeByte;
|
||||
bool setBits;
|
||||
|
||||
if (s != jlStageGet()) {
|
||||
if (s->portData == NULL) {
|
||||
return false;
|
||||
}
|
||||
colorIndex = (uint8_t)(doubled & 0x0Fu);
|
||||
pd = (AmigaPlanarT *)s->portData;
|
||||
if (pd != NULL) {
|
||||
|
||||
// Blitter DEST-only block fill of every whole plane to $00 (clear)
|
||||
// or $FF (set). Each plane is 200 rows x 20 words, contiguous, so
|
||||
// BLTSIZE = (200 << 6) | 20 and BLTDMOD = 0. The planes are now
|
||||
// CHIP RAM (see halSurfaceAllocPortData) so the blitter can reach
|
||||
// them. Own the blitter once and fill all four planes; the final
|
||||
// WaitBlit before DisownBlitter guarantees the planes are fully
|
||||
// written before any CPU read (a subsequent draw or the present
|
||||
// copy) touches them.
|
||||
OwnBlitter();
|
||||
for (i = 0; i < AMIGA_BITPLANES; i++) {
|
||||
planeByte = ((colorIndex >> i) & 1u) ? 0xFFu : 0x00u;
|
||||
memset(pd->planes[i], planeByte, AMIGA_PLANE_SIZE);
|
||||
}
|
||||
setBits = ((colorIndex >> i) & 1u) != 0u;
|
||||
amigaBlitterFillOne(pd->planes[i], AMIGA_BLT_WORDS_PER_ROW, SURFACE_HEIGHT, 0u, setBits);
|
||||
}
|
||||
WaitBlit();
|
||||
DisownBlitter();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -893,7 +1044,7 @@ bool halFastFillRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h
|
|||
* partial-byte mask handling. We just claim ownership so the C
|
||||
* chunky fallback never runs. */
|
||||
(void)x; (void)y; (void)w; (void)h; (void)colorIndex;
|
||||
if (s != jlStageGet()) {
|
||||
if (s->portData == NULL) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
|
@ -962,52 +1113,26 @@ void halFillRectPlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t
|
|||
yEnd = y + (int16_t)h;
|
||||
|
||||
/* Full-row fast path: no partial-byte RMW on either edge, so each
|
||||
* plane is a pure long-fill of (h * 40) bytes. jlFillRect 320x200
|
||||
* is the dominant case and lands here; saves 200 rows of leading
|
||||
* byte RMW + 200 of trailing byte RMW per plane = 1600 chip-bus
|
||||
* read+write cycles per plane on top of the actual data write. */
|
||||
* plane row is a whole 40-byte (20-word) store. jlFillRect 320x200
|
||||
* (clear-to-color, full-screen wipes) is the dominant case and
|
||||
* lands here. With the shadow planes now in CHIP RAM the blitter
|
||||
* can do this DEST-only block fill -- (h << 6) | 20 words, BLTDMOD
|
||||
* = 0 because consecutive full-width rows are contiguous. Set bits
|
||||
* write $FFFF (minterm $FF), clear bits write $0000 (minterm $00).
|
||||
* One OwnBlitter for all four planes; WaitBlit before DisownBlitter
|
||||
* so the planes are complete before the present copy / any later
|
||||
* CPU draw reads them. */
|
||||
if (byteFirst == 0u && byteLast == (uint16_t)(AMIGA_BYTES_PER_ROW - 1u)) {
|
||||
uint16_t totalLongs;
|
||||
uint16_t groups;
|
||||
uint16_t tail;
|
||||
uint32_t fillLong;
|
||||
uint32_t *p32;
|
||||
uint8_t *rowBase;
|
||||
|
||||
/* AMIGA_BYTES_PER_ROW = 40 = 10 longs / row. 8x-unrolled inner
|
||||
* loop amortizes the dec+bne to ~2 cyc per store; net ~12 cyc
|
||||
* per long including chip-bus contention. Tail handles the
|
||||
* 0..7 longs that don't fit a full group. */
|
||||
totalLongs = (uint16_t)((uint16_t)h * (AMIGA_BYTES_PER_ROW / 4u));
|
||||
groups = (uint16_t)(totalLongs >> 3);
|
||||
tail = (uint16_t)(totalLongs & 7u);
|
||||
OwnBlitter();
|
||||
for (plane = 0; plane < AMIGA_BITPLANES; plane++) {
|
||||
bitVal = (uint8_t)((colorIndex >> plane) & 1u);
|
||||
fullByte = bitVal ? 0xFFu : 0x00u;
|
||||
fillLong = (uint32_t)fullByte * 0x01010101UL;
|
||||
p32 = (uint32_t *)(pd->planes[plane]
|
||||
+ (uint16_t)y * AMIGA_BYTES_PER_ROW);
|
||||
if (groups > 0u) {
|
||||
uint16_t n = groups;
|
||||
do {
|
||||
p32[0] = fillLong;
|
||||
p32[1] = fillLong;
|
||||
p32[2] = fillLong;
|
||||
p32[3] = fillLong;
|
||||
p32[4] = fillLong;
|
||||
p32[5] = fillLong;
|
||||
p32[6] = fillLong;
|
||||
p32[7] = fillLong;
|
||||
p32 += 8;
|
||||
} while (--n);
|
||||
}
|
||||
{
|
||||
uint16_t t = tail;
|
||||
while (t > 0u) {
|
||||
*p32++ = fillLong;
|
||||
t--;
|
||||
}
|
||||
}
|
||||
rowBase = pd->planes[plane] + (uint16_t)y * AMIGA_BYTES_PER_ROW;
|
||||
amigaBlitterFillOne(rowBase, AMIGA_BLT_WORDS_PER_ROW, h, 0u, bitVal != 0u);
|
||||
}
|
||||
WaitBlit();
|
||||
DisownBlitter();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1015,9 +1140,9 @@ void halFillRectPlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t
|
|||
* bytes (leftMask == rightMask == 0xFF) every byte in the row is
|
||||
* a full overwrite -- no RMW needed. UBER jlFillRect 80x80 at x=120
|
||||
* lands here (byteFirst=15, byteLast=24). Plane bases are
|
||||
* MEMF_FAST-allocated long-aligned, and y*40 is also a multiple
|
||||
* of 4, so rowP alignment is determined by byteFirst alone --
|
||||
* computed once, not per-row. */
|
||||
* AllocMem(MEMF_CHIP)-returned and thus long-aligned, and y*40 is
|
||||
* also a multiple of 4, so rowP alignment is determined by
|
||||
* byteFirst alone -- computed once, not per-row. */
|
||||
if (leftMask == 0xFFu && rightMask == 0xFFu) {
|
||||
uint16_t nBytes = (uint16_t)(byteLast - byteFirst + 1u);
|
||||
uint8_t alignBytes = (uint8_t)((4u - (byteFirst & 3u)) & 3u);
|
||||
|
|
@ -1108,8 +1233,8 @@ void halFillRectPlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t
|
|||
|
||||
/* Hoist middle-region alignment outside both per-plane and
|
||||
* per-row loops. midStart = planeBase + y*40 + byteFirst + 1.
|
||||
* Plane bases are MEMF_FAST long-aligned and y*40 is a
|
||||
* multiple of 4, so midStart's alignment is determined by
|
||||
* Plane bases are AllocMem(MEMF_CHIP) long-aligned and y*40 is
|
||||
* a multiple of 4, so midStart's alignment is determined by
|
||||
* (byteFirst + 1) & 3 alone -- constant across planes/rows. */
|
||||
uint8_t midAlignBytes = (uint8_t)((4u - ((byteFirst + 1u) & 3u)) & 3u);
|
||||
uint16_t midRem;
|
||||
|
|
@ -1316,55 +1441,57 @@ void halTileCopyMaskedPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, cons
|
|||
|
||||
|
||||
// Phase 9 sprite save/restore plane-backup hooks.
|
||||
// Sprite save at (x, y, w, h) writes 4 plane stripes into backup
|
||||
// buffer; restore reads them back. x and w are 2-pixel aligned by
|
||||
// cross-platform code; we round x DOWN and w UP to 8-pixel boundaries
|
||||
// here so plane writes are byte-aligned. Backup layout (matches the
|
||||
// h * w/2 = 4 * h * w/8 sizing the cross-platform code allocates):
|
||||
// bytes [0 .. h*bpr ): plane 0 rows
|
||||
// bytes [h*bpr .. 2*h*bpr ): plane 1 rows
|
||||
// bytes [2*h*bpr .. 3*h*bpr ): plane 2 rows
|
||||
// bytes [3*h*bpr .. 4*h*bpr ): plane 3 rows
|
||||
// where bpr = bytesPerPlaneRow = roundedW/8.
|
||||
// Sprite save at (x, y, w, h) records the destination region into the
|
||||
// caller's backup buffer; restore writes it back. x and w are 2-pixel
|
||||
// aligned by cross-platform code.
|
||||
//
|
||||
// If the rect's rounded width is wider than the chunky-sized backup
|
||||
// would hold (h * (w/2) bytes), we silently truncate -- the planar
|
||||
// stripes for partial-byte-aligned sprites won't fit. This case is
|
||||
// rare for tile-aligned sprites; document if it bites.
|
||||
|
||||
static void amigaSpriteRoundRect(int16_t *xp, uint16_t *wp, uint16_t *bprp) {
|
||||
int16_t xIn = *xp;
|
||||
uint16_t wIn = *wp;
|
||||
int16_t xOut = (int16_t)(xIn & ~7); /* round down to 8-pixel */
|
||||
uint16_t span = (uint16_t)(((uint16_t)xIn + wIn) - (uint16_t)xOut);
|
||||
uint16_t wOut = (uint16_t)((span + 7u) & ~7u);
|
||||
*xp = xOut;
|
||||
*wp = wOut;
|
||||
*bprp = (uint16_t)(wOut >> 3);
|
||||
}
|
||||
|
||||
// Backup layout matches the CHUNKY-packed format the cross-platform
|
||||
// contract sizes for ((widthTiles*4)+1)*(heightTiles*8) bytes (see
|
||||
// include/joey/sprite.h): one byte per pixel-pair, 2 pixels/byte,
|
||||
// high nibble = even (left) pixel, low nibble = odd (right) pixel,
|
||||
// (w/2) bytes per row, (w/2)*h bytes total. This is the same format
|
||||
// the Atari ST port uses and never overruns the contract buffer --
|
||||
// unlike a plane-major layout, which would round x down to an 8-pixel
|
||||
// boundary and width up and then write (4*roundedW/8)*h = up to
|
||||
// (w/2 + 3)*h bytes for an odd-of-8 x, overrunning the buffer. Each
|
||||
// pixel's 4bpp color is assembled from / written to the 4 plane bits
|
||||
// at byte x>>3, bit 0x80 >> (x & 7).
|
||||
|
||||
void halSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dstPlaneBytes) {
|
||||
const AmigaPlanarT *pd;
|
||||
uint16_t bpr;
|
||||
uint16_t planeStripe;
|
||||
uint16_t i;
|
||||
uint16_t pairs;
|
||||
uint16_t row;
|
||||
uint16_t pair;
|
||||
uint8_t *dst;
|
||||
const uint8_t *src;
|
||||
|
||||
pd = (const AmigaPlanarT *)s->portData;
|
||||
if (pd == NULL || dstPlaneBytes == NULL) {
|
||||
if (pd == NULL || dstPlaneBytes == NULL || w == 0u || h == 0u) {
|
||||
return;
|
||||
}
|
||||
amigaSpriteRoundRect(&x, &w, &bpr);
|
||||
planeStripe = (uint16_t)((uint16_t)h * bpr);
|
||||
for (i = 0; i < AMIGA_BITPLANES; i++) {
|
||||
dst = dstPlaneBytes + i * planeStripe;
|
||||
pairs = (uint16_t)(w >> 1);
|
||||
for (row = 0; row < (uint16_t)h; row++) {
|
||||
src = pd->planes[i] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW + ((uint16_t)x >> 3);
|
||||
memcpy(dst, src, bpr);
|
||||
dst += bpr;
|
||||
const uint8_t *p0 = pd->planes[0] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
|
||||
const uint8_t *p1 = pd->planes[1] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
|
||||
const uint8_t *p2 = pd->planes[2] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
|
||||
const uint8_t *p3 = pd->planes[3] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
|
||||
dst = dstPlaneBytes + (uint16_t)(row * pairs);
|
||||
for (pair = 0; pair < pairs; pair++) {
|
||||
uint16_t px = (uint16_t)(x + (int16_t)(pair << 1));
|
||||
uint16_t byteOff = (uint16_t)(px >> 3);
|
||||
uint8_t maskHi = (uint8_t)(0x80u >> (px & 7u));
|
||||
uint8_t maskLo = (uint8_t)(0x80u >> ((px + 1u) & 7u));
|
||||
uint16_t byteOffLo = (uint16_t)((px + 1u) >> 3);
|
||||
uint8_t hi = 0u;
|
||||
uint8_t lo = 0u;
|
||||
if (p0[byteOff] & maskHi) { hi = (uint8_t)(hi | 0x01u); }
|
||||
if (p1[byteOff] & maskHi) { hi = (uint8_t)(hi | 0x02u); }
|
||||
if (p2[byteOff] & maskHi) { hi = (uint8_t)(hi | 0x04u); }
|
||||
if (p3[byteOff] & maskHi) { hi = (uint8_t)(hi | 0x08u); }
|
||||
if (p0[byteOffLo] & maskLo) { lo = (uint8_t)(lo | 0x01u); }
|
||||
if (p1[byteOffLo] & maskLo) { lo = (uint8_t)(lo | 0x02u); }
|
||||
if (p2[byteOffLo] & maskLo) { lo = (uint8_t)(lo | 0x04u); }
|
||||
if (p3[byteOffLo] & maskLo) { lo = (uint8_t)(lo | 0x08u); }
|
||||
dst[pair] = (uint8_t)((hi << 4) | lo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1372,25 +1499,41 @@ void halSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w,
|
|||
|
||||
void halSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, const uint8_t *srcPlaneBytes) {
|
||||
AmigaPlanarT *pd;
|
||||
uint16_t bpr;
|
||||
uint16_t planeStripe;
|
||||
uint16_t i;
|
||||
uint16_t pairs;
|
||||
uint16_t row;
|
||||
uint8_t *dst;
|
||||
uint16_t pair;
|
||||
const uint8_t *src;
|
||||
|
||||
pd = (AmigaPlanarT *)s->portData;
|
||||
if (pd == NULL || srcPlaneBytes == NULL) {
|
||||
if (pd == NULL || srcPlaneBytes == NULL || w == 0u || h == 0u) {
|
||||
return;
|
||||
}
|
||||
amigaSpriteRoundRect(&x, &w, &bpr);
|
||||
planeStripe = (uint16_t)((uint16_t)h * bpr);
|
||||
for (i = 0; i < AMIGA_BITPLANES; i++) {
|
||||
src = srcPlaneBytes + i * planeStripe;
|
||||
pairs = (uint16_t)(w >> 1);
|
||||
for (row = 0; row < (uint16_t)h; row++) {
|
||||
dst = pd->planes[i] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW + ((uint16_t)x >> 3);
|
||||
memcpy(dst, src, bpr);
|
||||
src += bpr;
|
||||
uint8_t *p0 = pd->planes[0] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
|
||||
uint8_t *p1 = pd->planes[1] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
|
||||
uint8_t *p2 = pd->planes[2] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
|
||||
uint8_t *p3 = pd->planes[3] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
|
||||
src = srcPlaneBytes + (uint16_t)(row * pairs);
|
||||
for (pair = 0; pair < pairs; pair++) {
|
||||
uint8_t b = src[pair];
|
||||
uint8_t hi = (uint8_t)(b >> 4);
|
||||
uint8_t lo = (uint8_t)(b & 0x0Fu);
|
||||
uint16_t px = (uint16_t)(x + (int16_t)(pair << 1));
|
||||
uint16_t byteOff = (uint16_t)(px >> 3);
|
||||
uint8_t maskHi = (uint8_t)(0x80u >> (px & 7u));
|
||||
uint8_t notHi = (uint8_t)(~maskHi);
|
||||
uint16_t byteOffLo = (uint16_t)((px + 1u) >> 3);
|
||||
uint8_t maskLo = (uint8_t)(0x80u >> ((px + 1u) & 7u));
|
||||
uint8_t notLo = (uint8_t)(~maskLo);
|
||||
p0[byteOff] = (uint8_t)((p0[byteOff] & notHi) | ((hi & 0x01u) ? maskHi : 0u));
|
||||
p1[byteOff] = (uint8_t)((p1[byteOff] & notHi) | ((hi & 0x02u) ? maskHi : 0u));
|
||||
p2[byteOff] = (uint8_t)((p2[byteOff] & notHi) | ((hi & 0x04u) ? maskHi : 0u));
|
||||
p3[byteOff] = (uint8_t)((p3[byteOff] & notHi) | ((hi & 0x08u) ? maskHi : 0u));
|
||||
p0[byteOffLo] = (uint8_t)((p0[byteOffLo] & notLo) | ((lo & 0x01u) ? maskLo : 0u));
|
||||
p1[byteOffLo] = (uint8_t)((p1[byteOffLo] & notLo) | ((lo & 0x02u) ? maskLo : 0u));
|
||||
p2[byteOffLo] = (uint8_t)((p2[byteOffLo] & notLo) | ((lo & 0x04u) ? maskLo : 0u));
|
||||
p3[byteOffLo] = (uint8_t)((p3[byteOffLo] & notLo) | ((lo & 0x08u) ? maskLo : 0u));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1422,8 +1565,12 @@ void halSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uin
|
|||
* 1. reads 4 chunky bytes (one row of one 8-pixel tile column)
|
||||
* 2. converts to 4 plane bytes + a shape mask (1 bit per pixel
|
||||
* where the chunky nibble is non-transparent)
|
||||
* 3. shifts the per-plane bytes by (dx & 7) so the sprite can land
|
||||
* at any pixel x, not just 8-px boundaries
|
||||
* 3. shifts the per-plane bytes by (x & 7) so the sprite can land
|
||||
* at any pixel x, not just 8-px boundaries; the shift is derived
|
||||
* from the ORIGINAL signed x so a left-edge clip keeps the true
|
||||
* source-to-dest sub-byte phase
|
||||
* 3a. each plane-byte write is guarded by a signed [0,40) dest-byte
|
||||
* range check, which handles left, right, top and bottom clip
|
||||
* 4. writes the left byte (and the spilled right byte for shifted
|
||||
* sprites) via AND-mask + OR to all 4 plane buffers
|
||||
*
|
||||
|
|
@ -1433,9 +1580,7 @@ void halSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uin
|
|||
* code per cel that the old emitter generated. */
|
||||
void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t y) {
|
||||
AmigaPlanarT *pd;
|
||||
int16_t dx;
|
||||
int16_t dy;
|
||||
int16_t sx;
|
||||
int16_t sy;
|
||||
int16_t w;
|
||||
int16_t h;
|
||||
|
|
@ -1445,14 +1590,13 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
|
|||
int16_t tileY;
|
||||
int16_t inTileY;
|
||||
int16_t startByteX;
|
||||
int16_t dstByteIdx;
|
||||
uint16_t wTiles;
|
||||
uint16_t rowEnd;
|
||||
uint8_t shift;
|
||||
uint8_t bit;
|
||||
uint8_t p0, p1, p2, p3, mask;
|
||||
uint8_t leftMask, notMask;
|
||||
uint8_t rshift, rightMask;
|
||||
uint16_t dstByteOff;
|
||||
uint16_t dstRowBase;
|
||||
const uint8_t *tile;
|
||||
const uint8_t *chunky;
|
||||
|
|
@ -1465,37 +1609,34 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
|
|||
wTiles = sp->widthTiles;
|
||||
w = (int16_t)(wTiles * 8u);
|
||||
h = (int16_t)(sp->heightTiles * 8u);
|
||||
dx = x;
|
||||
dy = y;
|
||||
sx = 0;
|
||||
sy = 0;
|
||||
|
||||
/* Clip dst rect against surface (sx/sy track offset INTO sprite). */
|
||||
if (dx < 0) { sx = (int16_t)(-dx); w = (int16_t)(w - sx); dx = 0; }
|
||||
/* Vertical clip only (sy tracks offset INTO sprite); horizontal
|
||||
* clip is handled per-byte by the dstByteIdx range guards below so
|
||||
* the source sub-byte phase is preserved on a left-edge clip. */
|
||||
if (dy < 0) { sy = (int16_t)(-dy); h = (int16_t)(h - sy); dy = 0; }
|
||||
if (dx >= SURFACE_WIDTH || dy >= SURFACE_HEIGHT || w <= 0 || h <= 0) {
|
||||
if (x >= SURFACE_WIDTH || dy >= SURFACE_HEIGHT || (int16_t)(x + w) <= 0 || h <= 0) {
|
||||
return;
|
||||
}
|
||||
if (dx + w > SURFACE_WIDTH) { w = (int16_t)(SURFACE_WIDTH - dx); }
|
||||
if (dy + h > SURFACE_HEIGHT) { h = (int16_t)(SURFACE_HEIGHT - dy); }
|
||||
|
||||
shift = (uint8_t)(dx & 7u);
|
||||
startByteX = (int16_t)(dx >> 3);
|
||||
/* Derive shift / startByteX from the ORIGINAL signed dest x so a
|
||||
* partial left clip keeps the true source-to-dest sub-byte phase.
|
||||
* shift in 0..7 (floored modulo), startByteX = floor(x / 8). For
|
||||
* x = -3: shift = 5, startByteX = -1 -- source column 0 lands
|
||||
* shifted by 5 into dest bytes -1 (off-surface, skipped) and 0. */
|
||||
shift = (uint8_t)(((x % 8) + 8) % 8);
|
||||
startByteX = (int16_t)((x - (int16_t)shift) / 8);
|
||||
|
||||
for (row = 0; row < h; row++) {
|
||||
spritePy = (int16_t)(sy + row);
|
||||
tileY = (int16_t)(spritePy >> 3);
|
||||
inTileY = (int16_t)(spritePy & 7);
|
||||
dstRowBase = (uint16_t)((uint16_t)(dy + row) * AMIGA_BYTES_PER_ROW);
|
||||
rowEnd = (uint16_t)(dstRowBase + AMIGA_BYTES_PER_ROW);
|
||||
|
||||
for (byteCol = 0; byteCol < (int16_t)wTiles; byteCol++) {
|
||||
int16_t spriteBx = (int16_t)((sx >> 3) + byteCol);
|
||||
if (spriteBx >= (int16_t)wTiles) {
|
||||
break;
|
||||
}
|
||||
|
||||
tile = sp->tileData + (uint32_t)((tileY * wTiles + spriteBx) * 32u);
|
||||
tile = sp->tileData + (uint32_t)((tileY * wTiles + byteCol) * 32u);
|
||||
chunky = tile + inTileY * 4u;
|
||||
|
||||
/* c2p: 8 pixels (4 chunky bytes) -> 4 plane bytes + mask. */
|
||||
|
|
@ -1515,9 +1656,13 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
|
|||
continue;
|
||||
}
|
||||
|
||||
dstByteOff = (uint16_t)(dstRowBase + (uint16_t)(startByteX + byteCol));
|
||||
/* Signed dest byte index for this source column. Guard each
|
||||
* write against [0, AMIGA_BYTES_PER_ROW) in SIGNED math so
|
||||
* a negative startByteX never wraps to a huge unsigned. */
|
||||
dstByteIdx = (int16_t)(startByteX + byteCol);
|
||||
leftMask = (uint8_t)(mask >> shift);
|
||||
if (leftMask != 0u && dstByteOff < rowEnd) {
|
||||
if (leftMask != 0u && dstByteIdx >= 0 && dstByteIdx < (int16_t)AMIGA_BYTES_PER_ROW) {
|
||||
uint16_t dstByteOff = (uint16_t)(dstRowBase + (uint16_t)dstByteIdx);
|
||||
notMask = (uint8_t)(~leftMask);
|
||||
pd->planes[0][dstByteOff] = (uint8_t)((pd->planes[0][dstByteOff] & notMask) | (uint8_t)(p0 >> shift));
|
||||
pd->planes[1][dstByteOff] = (uint8_t)((pd->planes[1][dstByteOff] & notMask) | (uint8_t)(p1 >> shift));
|
||||
|
|
@ -1526,11 +1671,11 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
|
|||
}
|
||||
|
||||
if (shift != 0u) {
|
||||
int16_t rIdx = (int16_t)(dstByteIdx + 1);
|
||||
rshift = (uint8_t)(8u - shift);
|
||||
rightMask = (uint8_t)(mask << rshift);
|
||||
if (rightMask != 0u) {
|
||||
uint16_t rOff = (uint16_t)(dstByteOff + 1u);
|
||||
if (rOff < rowEnd) {
|
||||
if (rightMask != 0u && rIdx >= 0 && rIdx < (int16_t)AMIGA_BYTES_PER_ROW) {
|
||||
uint16_t rOff = (uint16_t)(dstRowBase + (uint16_t)rIdx);
|
||||
notMask = (uint8_t)(~rightMask);
|
||||
pd->planes[0][rOff] = (uint8_t)((pd->planes[0][rOff] & notMask) | (uint8_t)(p0 << rshift));
|
||||
pd->planes[1][rOff] = (uint8_t)((pd->planes[1][rOff] & notMask) | (uint8_t)(p1 << rshift));
|
||||
|
|
@ -1541,7 +1686,6 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Phase 9 plane-to-chunky derivation for jlTileSnap. Reads 8 plane
|
||||
|
|
@ -1645,16 +1789,20 @@ void halTilePasteMonoPlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8
|
|||
void halSurfaceCopyPlanes(jlSurfaceT *dst, const jlSurfaceT *src) {
|
||||
AmigaPlanarT *dstPd;
|
||||
AmigaPlanarT *srcPd;
|
||||
uint16_t i;
|
||||
|
||||
dstPd = (AmigaPlanarT *)dst->portData;
|
||||
srcPd = (AmigaPlanarT *)src->portData;
|
||||
if (dstPd == NULL || srcPd == NULL) {
|
||||
return;
|
||||
}
|
||||
for (i = 0; i < AMIGA_BITPLANES; i++) {
|
||||
memcpy(dstPd->planes[i], srcPd->planes[i], AMIGA_PLANE_SIZE);
|
||||
}
|
||||
// Straight blitter A->D copy of every whole plane (200 rows x 20
|
||||
// words, contiguous). Both source and dest planes are CHIP RAM now
|
||||
// (halSurfaceAllocPortData) so the blitter can reach both. The
|
||||
// helper owns the blitter, copies all four planes, and WaitBlit's
|
||||
// before DisownBlitter so the dest planes are complete before any
|
||||
// CPU read.
|
||||
amigaBlitterCopyPlanes(dstPd->planes, srcPd->planes,
|
||||
AMIGA_BLT_WORDS_PER_ROW, SURFACE_HEIGHT);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1758,21 +1906,19 @@ static void amigaPlanarSetPixel(AmigaPlanarT *pd, int16_t x, int16_t y, uint8_t
|
|||
bool halFastDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) {
|
||||
AmigaPlanarT *pd;
|
||||
uint8_t nibLo;
|
||||
if (s != jlStageGet()) {
|
||||
if (s->portData == NULL) {
|
||||
return false;
|
||||
}
|
||||
nibLo = (uint8_t)(colorIndex & 0x0Fu);
|
||||
pd = (AmigaPlanarT *)s->portData;
|
||||
if (pd != NULL) {
|
||||
amigaPlanarSetPixel(pd, (int16_t)x, (int16_t)y, nibLo);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Bresenham's diagonal line, planar-only walk. Same algorithm as
|
||||
// cross-platform jlDrawLine's fallback so the pixel set matches the
|
||||
// chunky walker (draw68kLine) bit-for-bit.
|
||||
// cross-platform jlDrawLine's fallback so the pixel set matches it
|
||||
// bit-for-bit.
|
||||
static void amigaPlanarLine(AmigaPlanarT *pd, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t color) {
|
||||
int16_t dx;
|
||||
int16_t dy;
|
||||
|
|
@ -1815,29 +1961,25 @@ static void amigaPlanarLine(AmigaPlanarT *pd, int16_t x0, int16_t y0, int16_t x1
|
|||
|
||||
bool halFastDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex) {
|
||||
AmigaPlanarT *pd;
|
||||
if (s != jlStageGet()) {
|
||||
if (s->portData == NULL) {
|
||||
return false;
|
||||
}
|
||||
pd = (AmigaPlanarT *)s->portData;
|
||||
if (pd != NULL) {
|
||||
amigaPlanarLine(pd, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool halFastDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
|
||||
AmigaPlanarT *pd;
|
||||
if (s != jlStageGet()) {
|
||||
if (s->portData == NULL) {
|
||||
return false;
|
||||
}
|
||||
pd = (AmigaPlanarT *)s->portData;
|
||||
if (pd != NULL) {
|
||||
surface68kAmigaCircleOutline(pd->planes[0], pd->planes[1],
|
||||
pd->planes[2], pd->planes[3],
|
||||
(uint16_t)cx, (uint16_t)cy, r,
|
||||
(uint8_t)(colorIndex & 0x0Fu));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1904,11 +2046,10 @@ bool halFastFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_
|
|||
int16_t spanX;
|
||||
uint16_t spanW;
|
||||
|
||||
if (s != jlStageGet()) {
|
||||
if (s->portData == NULL) {
|
||||
return false;
|
||||
}
|
||||
pd = (AmigaPlanarT *)s->portData;
|
||||
if (pd != NULL) {
|
||||
/* Inline per-span plane fill -- avoids 4 halFillRectPlanes
|
||||
* dispatches per midpoint iter (~320 dispatches for r=40). */
|
||||
x = (int16_t)r;
|
||||
|
|
@ -1931,7 +2072,6 @@ bool halFastFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_
|
|||
err = (int16_t)(err + y + y - x - x + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -2375,26 +2515,20 @@ void *halSurfaceAllocPortData(jlSurfaceT *s, bool isStage) {
|
|||
* is gPlanes[], CHIP, updated only by halPresent). Non-stage uses
|
||||
* these as the surface's only planes.
|
||||
*
|
||||
* MEMF_FAST (no MEMF_CHIP) explicitly demands fast RAM. The
|
||||
* shadow planes are CPU-only (no blitter/copper access); freeing
|
||||
* them from chip RAM cuts halPresent's chip-bus contention in
|
||||
* half (read FAST + write CHIP instead of read CHIP + write CHIP)
|
||||
* and gives RMW drawing primitives 2-3x speedup. If fast RAM is
|
||||
* unavailable (bare A500, no expansion), we fall back to chip
|
||||
* via the loop below. */
|
||||
* MEMF_CHIP because the bulk plane ops (clear, full-row fill,
|
||||
* surface copy) now run on the BLITTER, which can only reach chip
|
||||
* RAM. This is a deliberate performance tradeoff: chip RMW is
|
||||
* slower than fast RAM for per-pixel CPU draws (line / circle /
|
||||
* single pixel), but the blitter wins big on the full-plane
|
||||
* clear/fill/copy paths and runs in parallel with the CPU. Net is
|
||||
* workload-dependent; see the blitter helpers above. The planes
|
||||
* must be MEMF_CHIP unconditionally now -- a fast-RAM fallback
|
||||
* would put the blitter helpers out of reach and corrupt output. */
|
||||
for (i = 0; i < AMIGA_BITPLANES; i++) {
|
||||
pd->planes[i] = (uint8_t *)AllocMem((ULONG)AMIGA_PLANE_SIZE,
|
||||
(ULONG)(MEMF_FAST | MEMF_CLEAR));
|
||||
if (pd->planes[i] == NULL) {
|
||||
/* No fast RAM available; fall back to chip. */
|
||||
pd->planes[i] = (uint8_t *)AllocMem((ULONG)AMIGA_PLANE_SIZE,
|
||||
(ULONG)(MEMF_CHIP | MEMF_CLEAR));
|
||||
jlLogF("amiga: shadow plane %u in CHIP (fast unavailable, addr=$%08lX)",
|
||||
jlLogF("amiga: shadow plane %u in CHIP (blitter, addr=$%08lX)",
|
||||
(unsigned)i, (unsigned long)pd->planes[i]);
|
||||
} else {
|
||||
jlLogF("amiga: shadow plane %u in FAST (addr=$%08lX)",
|
||||
(unsigned)i, (unsigned long)pd->planes[i]);
|
||||
}
|
||||
if (pd->planes[i] == NULL) {
|
||||
/* Roll back any planes already allocated. */
|
||||
while (i > 0u) {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,12 @@
|
|||
#define ST_YM_SELECT ((volatile uint8_t *)0xFFFF8800L)
|
||||
#define ST_YM_DATA ((volatile uint8_t *)0xFFFF8802L)
|
||||
|
||||
// YM mixer (reg 7) high bits 6+7 are the I/O port directions TOS
|
||||
// leaves configured (port A output, port B = centronics printer
|
||||
// output). Always preserved when we rewrite the mixer.
|
||||
#define YM_MIXER_IO_DIR 0xC0u
|
||||
#define YM_REG_MIXER 7u
|
||||
|
||||
#define MFP_TA_BIT 0x20
|
||||
#define MFP_TACR_STOP 0x00
|
||||
#define MFP_TACR_DIV200 0x07
|
||||
|
|
@ -81,8 +87,23 @@ static void (*gOldTimerAVec)(void) = NULL;
|
|||
// Mixed in over libxmp's MOD output during halAudioFrameTick.
|
||||
static AudioSfxSlotT gSfxSlots[JOEY_AUDIO_SFX_SLOTS];
|
||||
|
||||
// Single shadow of YM mixer reg 7's low 6 bits (tone+noise disables).
|
||||
// Both the PWM (installTimerA) and PSG-tone (applyYmVoice) paths
|
||||
// drive the mixer through this one shadow so they share one source of
|
||||
// truth: 0x3F = all tones and noise off (PWM-DAC default). The YM
|
||||
// can't be read back, so this shadow IS the authoritative state.
|
||||
static volatile uint8_t gYmToneMask = 0x3Fu;
|
||||
|
||||
// ----- Internal helpers -----
|
||||
|
||||
// Write YM mixer reg 7 from the gYmToneMask shadow, preserving the
|
||||
// TOS I/O-port-direction high bits. Supervisor context required.
|
||||
static void ymWriteMixer(void) {
|
||||
*ST_YM_SELECT = YM_REG_MIXER;
|
||||
*ST_YM_DATA = (uint8_t)(YM_MIXER_IO_DIR | gYmToneMask);
|
||||
}
|
||||
|
||||
|
||||
static long installTimerA(void) {
|
||||
uint16_t i;
|
||||
|
||||
|
|
@ -114,8 +135,8 @@ static long installTimerA(void) {
|
|||
// disable bits over an assumed-safe TOS-default mask. Bit 6 set
|
||||
// (port A output) matches stock TOS; bit 7 set (port B output)
|
||||
// matches the centronics-printer direction TOS configures.
|
||||
*ST_YM_SELECT = 7;
|
||||
*ST_YM_DATA = 0xFF; // all tones + noises off; I/O ports A+B output (TOS default)
|
||||
gYmToneMask = 0x3Fu; // all tones + noise disabled (PWM-DAC default)
|
||||
ymWriteMixer(); // -> 0xFF: tones+noise off, I/O ports A+B output
|
||||
*ST_YM_SELECT = 8;
|
||||
*ST_YM_DATA = 0; // channel A volume = 0 to avoid a pop at start
|
||||
*ST_YM_SELECT = 9;
|
||||
|
|
@ -271,7 +292,7 @@ void halAudioTone(uint16_t freqHz) {
|
|||
static volatile uint8_t gYmVoice;
|
||||
static volatile uint16_t gYmFreqHz;
|
||||
static volatile uint8_t gYmAtten;
|
||||
static volatile uint8_t gYmToneMask = 0x3Fu; // bits 0-5 all off (no tones, no noise)
|
||||
// gYmToneMask is the shared YM mixer shadow, defined in module state.
|
||||
|
||||
|
||||
static long applyYmVoice(void) {
|
||||
|
|
@ -287,8 +308,7 @@ static long applyYmVoice(void) {
|
|||
// Silence: disable tone on this channel (mixer bit 0/1/2 set),
|
||||
// zero volume. Don't touch other channels' bits.
|
||||
gYmToneMask = (uint8_t)(gYmToneMask | ((uint8_t)1u << gYmVoice));
|
||||
*ST_YM_SELECT = 7;
|
||||
*ST_YM_DATA = (uint8_t)(0xC0u | gYmToneMask); // preserve I/O port-direction high bits
|
||||
ymWriteMixer();
|
||||
*ST_YM_SELECT = volReg;
|
||||
*ST_YM_DATA = 0;
|
||||
return 0;
|
||||
|
|
@ -313,8 +333,7 @@ static long applyYmVoice(void) {
|
|||
|
||||
// Enable tone on this channel (clear its bit in mixer).
|
||||
gYmToneMask = (uint8_t)(gYmToneMask & ~((uint8_t)1u << gYmVoice));
|
||||
*ST_YM_SELECT = 7;
|
||||
*ST_YM_DATA = (uint8_t)(0xC0u | gYmToneMask);
|
||||
ymWriteMixer();
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -331,7 +350,10 @@ bool halAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) {
|
|||
|
||||
|
||||
void halAudioCriticalEnter(void) {
|
||||
// No audio ISR registered -> no critical section needed.
|
||||
// No tick-callback ISR is registered (halAudioTickRegister returns
|
||||
// false). The Timer A MOD ISR synchronizes with the producer via
|
||||
// the gNeedRefill flags, not this critical section, so masking
|
||||
// here would only starve that ~12 kHz audio ISR for no benefit.
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -340,7 +362,7 @@ void halAudioCriticalExit(void) {
|
|||
|
||||
|
||||
void halAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) {
|
||||
if (voice >= 3u) {
|
||||
if (voice >= JOEY_AUDIO_VOICES) {
|
||||
return;
|
||||
}
|
||||
// The PWM-via-volume engine in halAudioInit hammers YM reg 8
|
||||
|
|
@ -383,10 +405,24 @@ void halAudioStopSfx(uint8_t slot) {
|
|||
// Drains ISR-raised refill flags by re-running libxmp's mixer into
|
||||
// the consumed half then overlaying any active SFX. Called from the
|
||||
// game loop, never from IRQ.
|
||||
//
|
||||
// Soft-real-time constraint: the producer (this function) must refill
|
||||
// a consumed half within MIX_HALF / MIX_RATE = 1024 / 12288 ~= 83 ms
|
||||
// of the ISR raising its flag, or the ISR drains into the half still
|
||||
// being rewritten (audible tearing). A full MOD-frame mix plus
|
||||
// audioSfxOverlayMix can approach that budget under load, so callers
|
||||
// should run the game loop comfortably faster than ~12 Hz.
|
||||
void halAudioFrameTick(void) {
|
||||
if (!gReady) {
|
||||
return;
|
||||
}
|
||||
// Underrun guard: both flags set means the producer fell a full
|
||||
// buffer behind, so the ISR is now draining a half this tick is
|
||||
// about to rewrite. Silence first so the in-flight torn read plays
|
||||
// as silence rather than garbage, then refill both halves below.
|
||||
if (gNeedRefill[0] && gNeedRefill[1]) {
|
||||
silenceMixBuffer();
|
||||
}
|
||||
if (gNeedRefill[0]) {
|
||||
if (gXmpStarted) {
|
||||
xmp_play_buffer(gXmpCtx, (void *)gMixBuf, MIX_HALF, gXmpLoopCount);
|
||||
|
|
|
|||
|
|
@ -24,19 +24,17 @@
|
|||
| d5.w = cx (cached)
|
||||
| a4 = cy (cached, sign-extended)
|
||||
| a3 = base
|
||||
| a5 = bitMaskWordLut
|
||||
| a5 = _gStBitMaskWordLut
|
||||
| d0,d1,d6,d7 = scratch
|
||||
|
|
||||
| Scratch block (24 bytes) at sp+0..23:
|
||||
| sp+0..3: xp1 record [groupOff_w, bitMask_b, notMask_b]
|
||||
| groupOff = (x >> 4) * 8 (byte offset of group within row)
|
||||
| bitMask = byte representation of 1 << (15 - (x & 15))
|
||||
| ... wait, bitMask must be a WORD on ST not a byte.
|
||||
|
|
||||
| Actually layout differs from Amiga: ST needs a WORD bit mask, not
|
||||
| a byte. Per-record layout (8 bytes):
|
||||
| groupOff_word (2 bytes), bitMask_word (2 bytes), notMask_word
|
||||
| (2 bytes), pad (2 bytes)
|
||||
| Scratch block (40 bytes) at sp+0..39. Each xp record is 8 bytes but
|
||||
| only 2 words are actually stored: groupOff_word at +0 and
|
||||
| bitMask_word at +2 (+4/+6 are unused padding kept only for the
|
||||
| 8-byte stride). notMask is NOT cached -- PLOT_FIXED derives it per
|
||||
| plot via not.w on the bitMask. On ST the bit mask is a WORD (the
|
||||
| group is 4 plane words), unlike Amiga's byte mask.
|
||||
| xp record: groupOff = (x >> 4) * 8 (byte offset of group in row)
|
||||
| bitMask = 1 << (15 - (x & 15)) (word, from LUT)
|
||||
|
|
||||
| sp+0..7: xp1 record (cx + bx)
|
||||
| sp+8..15: xp2 record (cx - bx)
|
||||
|
|
@ -205,9 +203,11 @@ _surface68kStCircleOutline:
|
|||
movem.l %d2-%d7/%a2-%a6,-(%sp)
|
||||
lea -SP_LOCAL(%sp),%sp
|
||||
|
||||
| Load base (a3) and bitMaskLut (a5).
|
||||
| Load base (a3) and bitMaskLut (a5). The mask LUT is
|
||||
| the shared _gStBitMaskWordLut in lineSpan.s; reference
|
||||
| absolute (same pattern as _gStRowOffsetLut below).
|
||||
move.l SP_BASE(%sp),%a3
|
||||
lea bitMaskWordLut(%pc),%a5
|
||||
lea _gStBitMaskWordLut,%a5
|
||||
|
||||
| Cache cx in d5, cy (sign-extended) in a4.
|
||||
move.w SP_CX(%sp),%d5
|
||||
|
|
@ -278,11 +278,5 @@ _surface68kStCircleOutline:
|
|||
rts
|
||||
|
||||
|
||||
.align 2
|
||||
| 16 word entries: bitMaskWordLut[i] = 1 << (15 - i), for i in 0..15.
|
||||
bitMaskWordLut:
|
||||
.word 0x8000, 0x4000, 0x2000, 0x1000
|
||||
.word 0x0800, 0x0400, 0x0200, 0x0100
|
||||
.word 0x0080, 0x0040, 0x0020, 0x0010
|
||||
.word 0x0008, 0x0004, 0x0002, 0x0001
|
||||
| (yLut now lives in lineSpan.s as the shared _gStRowOffsetLut)
|
||||
| (bitMaskWordLut and yLut both live in lineSpan.s as the shared
|
||||
| _gStBitMaskWordLut / _gStRowOffsetLut.)
|
||||
|
|
|
|||
|
|
@ -51,13 +51,13 @@
|
|||
lsr.w #4,%d0 | groupLast
|
||||
sub.w %d1,%d0 | numGroups
|
||||
move.w %d0,4(%sp)
|
||||
| leftMask via LUT[bitFirst]; a5 = leftMaskLut base
|
||||
| leftMask via LUT[bitFirst]; a5 = _gStLeftMaskLut base
|
||||
move.w 0(%sp),%d0
|
||||
and.w #15,%d0
|
||||
add.w %d0,%d0
|
||||
move.w (%a5,%d0.w),%d1
|
||||
move.w %d1,0(%sp)
|
||||
| rightMask via LUT[bitLast]; a6 = rightMaskLut base
|
||||
| rightMask via LUT[bitLast]; a6 = _gStRightMaskLut base
|
||||
move.w 2(%sp),%d0
|
||||
and.w #15,%d0
|
||||
add.w %d0,%d0
|
||||
|
|
@ -216,8 +216,10 @@ _surface68kStFillCircle:
|
|||
|
||||
| LUT bases. a5/a6 = mask LUTs (used by COMPUTE_PAIR_MASKS).
|
||||
| a2 = shared _gStRowOffsetLut (used by SPAN_BODY for y*160).
|
||||
lea leftMaskLut(%pc),%a5
|
||||
lea rightMaskLut(%pc),%a6
|
||||
| All three LUTs are the shared definitions in lineSpan.s;
|
||||
| reference them absolute (single source of truth).
|
||||
lea _gStLeftMaskLut,%a5
|
||||
lea _gStRightMaskLut,%a6
|
||||
lea _gStRowOffsetLut,%a2
|
||||
|
||||
| loLong = ((c&1)?0xFFFF0000:0) | ((c&2)?0x0000FFFF:0)
|
||||
|
|
@ -299,17 +301,5 @@ _surface68kStFillCircle:
|
|||
rts
|
||||
|
||||
|
||||
.align 2
|
||||
| leftMaskLut[i] = (1 << (16 - i)) - 1, indexed by bitFirst (0..15)
|
||||
leftMaskLut:
|
||||
.word 0xFFFF, 0x7FFF, 0x3FFF, 0x1FFF
|
||||
.word 0x0FFF, 0x07FF, 0x03FF, 0x01FF
|
||||
.word 0x00FF, 0x007F, 0x003F, 0x001F
|
||||
.word 0x000F, 0x0007, 0x0003, 0x0001
|
||||
|
||||
| rightMaskLut[i] = ~((1 << (15 - i)) - 1), indexed by bitLast (0..15)
|
||||
rightMaskLut:
|
||||
.word 0x8000, 0xC000, 0xE000, 0xF000
|
||||
.word 0xF800, 0xFC00, 0xFE00, 0xFF00
|
||||
.word 0xFF80, 0xFFC0, 0xFFE0, 0xFFF0
|
||||
.word 0xFFF8, 0xFFFC, 0xFFFE, 0xFFFF
|
||||
| (leftMaskLut/rightMaskLut now live in lineSpan.s as the shared
|
||||
| _gStLeftMaskLut / _gStRightMaskLut, referenced absolute above.)
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@
|
|||
#include "surfaceInternal.h"
|
||||
#include "spriteInternal.h"
|
||||
#include "joey/tile.h"
|
||||
#include "draw68k_inline.h"
|
||||
|
||||
// ----- Constants -----
|
||||
|
||||
|
|
@ -63,10 +62,12 @@
|
|||
// Amiga's 4 separate plane buffers -- same total bytes, very different
|
||||
// access pattern.
|
||||
//
|
||||
// The stage gets its own SHADOW planar buffer (NOT aliased to
|
||||
// gScreenBase) so drawing primitives don't appear until jlStagePresent
|
||||
// memcpy's shadow -> screen. Same rationale as Amiga's per-stage
|
||||
// shadow planes.
|
||||
// The stage gets its own SHADOW planar buffer (NOT aliased to a
|
||||
// displayed page) so drawing primitives don't appear until
|
||||
// jlStagePresent copies the union(prev,current) dirty rows from the
|
||||
// shadow into the off-screen page and flips the shifter to it. Same
|
||||
// rationale as Amiga's per-stage shadow planes feeding its 2-buffer
|
||||
// page flip.
|
||||
typedef struct {
|
||||
uint8_t *base; // 32000-byte interleaved planar buffer
|
||||
uint8_t *raw; // unaligned malloc result for free()
|
||||
|
|
@ -76,6 +77,17 @@ typedef struct {
|
|||
// Shifter palette registers: 16 words at $FFFF8240..$FFFF825F.
|
||||
#define ST_PALETTE_REGS ((volatile uint16_t *)0xFFFF8240L)
|
||||
|
||||
// Shifter video base address registers. The shifter latches the base
|
||||
// at the start of each frame's display (during vblank), so a write
|
||||
// here mid-visible-frame takes effect on the NEXT frame -- a deferred,
|
||||
// tear-free page flip with no TOS involvement. STF exposes only the
|
||||
// high and mid bytes; there is no low byte, which is why the ST screen
|
||||
// must be 256-byte aligned (the low 8 bits of the base are hardwired
|
||||
// to zero). Both bytes are odd-addressed byte registers; byte access
|
||||
// to an odd address is legal on 68000 (only word/long faults).
|
||||
#define ST_VID_BASE_HI ((volatile uint8_t *)0xFFFF8201L) // bits 23..16
|
||||
#define ST_VID_BASE_MID ((volatile uint8_t *)0xFFFF8203L) // bits 15..8
|
||||
|
||||
// MFP hardware addresses.
|
||||
#define ST_MFP_TBCR ((volatile uint8_t *)0xFFFFFA1BL) // Timer B control
|
||||
#define ST_MFP_TBDR ((volatile uint8_t *)0xFFFFFA21L) // Timer B data
|
||||
|
|
@ -136,6 +148,7 @@ static inline __attribute__((always_inline)) uint8_t stPlanarGetPixel(const StPl
|
|||
}
|
||||
static uint16_t quantizeColorToSt(uint16_t orgb);
|
||||
static void flattenScbPalettes(const jlSurfaceT *src);
|
||||
static void setShifterVideoBase(const uint8_t *page);
|
||||
static void writeDiagnostics(void);
|
||||
static long writePrevPaletteRegs(void);
|
||||
|
||||
|
|
@ -147,19 +160,51 @@ static void refreshPaletteStateIfNeeded(const
|
|||
|
||||
// ----- Module state -----
|
||||
|
||||
// Screen buffer: enough for 320x200x4bpp planar plus padding for
|
||||
// runtime 256-byte alignment. TOS .PRG format only supports 2-byte
|
||||
// object-file alignment, so we overallocate and align the pointer
|
||||
// manually in halInit.
|
||||
static uint8_t gScreenBuffer[SURFACE_PIXELS_SIZE + ST_SCREEN_ALIGN];
|
||||
// Screen buffer: TWO 320x200x4bpp planar pages for page-flipping, plus
|
||||
// padding for runtime 256-byte alignment. TOS .PRG format only supports
|
||||
// 2-byte object-file alignment, so we overallocate and align the first
|
||||
// page pointer manually in halInit; the second page sits exactly one
|
||||
// page (SURFACE_PIXELS_SIZE = 32000, itself a multiple of 256) after
|
||||
// the first, so it stays 256-byte aligned as the shifter requires.
|
||||
static uint8_t gScreenBuffer[(2 * SURFACE_PIXELS_SIZE) + ST_SCREEN_ALIGN];
|
||||
|
||||
// Two physical screen pages. gFrontIdx selects the one the shifter is
|
||||
// currently displaying; the OFF-screen page (gFrontIdx ^ 1) is the
|
||||
// flip target written by the next present. Drawing primitives target
|
||||
// the stage's own shadow planar buffer (StPlanarT.base), NOT a page,
|
||||
// so the union(prev,cur) dirty copy at present brings the off-screen
|
||||
// page fully up to date before the flip -- mirrors the Amiga 2-buffer
|
||||
// gPrevStage* staleness pattern (shadow -> off-screen buffer -> flip).
|
||||
static uint8_t *gScreenPage[2] = { NULL, NULL };
|
||||
static uint8_t gFrontIdx = 0;
|
||||
|
||||
static uint8_t *gScreenBase = NULL;
|
||||
static void *gPrevPhysbase = NULL;
|
||||
static void *gPrevLogbase = NULL;
|
||||
static int16_t gPrevRez = 0;
|
||||
static uint16_t gPrevPalette[SURFACE_COLORS_PER_PALETTE];
|
||||
static bool gModeSet = false;
|
||||
|
||||
// Snapshot of the previous frame's dirty bands. With two physical
|
||||
// pages the off-screen page holds content from TWO frames ago, so each
|
||||
// present must refresh the union(prev, current) of dirty rows from the
|
||||
// shadow -- not just this frame's dirty rows -- or stale streaks from
|
||||
// two frames back survive on the page we are about to display. Seeded
|
||||
// to "all dirty" so the first two presents do full-screen refreshes
|
||||
// and converge both pages to the current shadow. After that we pay
|
||||
// only the small per-row union(prev,current) copy.
|
||||
static uint8_t gPrevStageMinWord[SURFACE_HEIGHT];
|
||||
static uint8_t gPrevStageMaxWord[SURFACE_HEIGHT];
|
||||
|
||||
// "The previous frame's snapshot recorded at least one dirty row."
|
||||
// Lets halPresent skip the whole 200-row union scan + 200-row snapshot
|
||||
// copy on consecutive idle frames: if neither the previous frame nor
|
||||
// the current frame touched any row, union(prev,current) is empty, the
|
||||
// off-screen page already matches the displayed page (the settling
|
||||
// flip happened on the first idle frame), and the snapshot would only
|
||||
// copy clean->clean. Seeded true to match the "all dirty" bootstrap of
|
||||
// gPrevStage* above so the first two presents still run.
|
||||
static bool gPrevAnyDirty = true;
|
||||
|
||||
// Per-scanline pre-quantized palette table. Indexed by display line;
|
||||
// each row is a 16-word palette ready to be copied straight into the
|
||||
// shifter registers. Written at present() time, read by the Timer B
|
||||
|
|
@ -303,12 +348,18 @@ static uint16_t quantizeColorToSt(uint16_t orgb) {
|
|||
// auto-reload keep counting for the next fire. We deliberately do
|
||||
// NOT stop/reload/restart the timer here: that sequence would cost
|
||||
// 1-2 HBL edges each fire, and those losses compound across 7+
|
||||
// transitions into a visible "last band short" drift. Updating
|
||||
// TBDR in place is enough for variable-length bands -- the new
|
||||
// value takes effect on the fire AFTER next, which is acceptable
|
||||
// when adjacent bands have similar lengths; uniform bands (like
|
||||
// pattern.c) don't need TBDR updates at all so stay perfectly
|
||||
// aligned with no drift.
|
||||
// transitions into a visible "last band short" drift.
|
||||
//
|
||||
// MFP event-count auto-reload latches the CURRENT TBDR at each
|
||||
// underflow, so a value written here does not take effect on the
|
||||
// NEXT fire (that interval was already reloaded at the underflow
|
||||
// that triggered this ISR) but on the fire AFTER next. To time
|
||||
// variable-height bands correctly we therefore program one fire
|
||||
// ahead: in the ISR that transitions to `band` we load TBDR with
|
||||
// the band+1 -> band+2 delta (the interval that the band+1
|
||||
// underflow will reload and use to time the band+2 transition).
|
||||
// Uniform bands (like pattern.c) have all deltas equal, so the
|
||||
// one-ahead value is identical and they stay perfectly aligned.
|
||||
static void timerBIsr(void) {
|
||||
uint16_t band;
|
||||
uint8_t palIdx;
|
||||
|
|
@ -344,14 +395,19 @@ static void timerBIsr(void) {
|
|||
dst[15] = src[15];
|
||||
|
||||
if (band + 1 < gBandCount) {
|
||||
// Update TBDR for the fire-after-next (auto-reload at
|
||||
// the NEXT fire still uses the old value). Don't stop
|
||||
// the timer.
|
||||
nextDelta = gBandStart[band + 1] - gBandStart[band];
|
||||
// A transition to band+1 is still pending; keep Timer B
|
||||
// running. The band+1 underflow has already had its reload
|
||||
// value latched (set one fire earlier), so program TBDR
|
||||
// now with the band+1 -> band+2 delta, which the band+1
|
||||
// underflow will reload and use to time band+2. Don't
|
||||
// stop the timer.
|
||||
if (band + 2 < gBandCount) {
|
||||
nextDelta = gBandStart[band + 2] - gBandStart[band + 1];
|
||||
if (nextDelta == 0 || nextDelta > 255) {
|
||||
nextDelta = 1;
|
||||
}
|
||||
*ST_MFP_TBDR = (uint8_t)nextDelta;
|
||||
}
|
||||
*ST_MFP_ISRA = MFP_TB_CLEAR;
|
||||
return;
|
||||
}
|
||||
|
|
@ -410,6 +466,19 @@ static void vblIsr(void) {
|
|||
*ST_MFP_TBDR = (uint8_t)delta;
|
||||
*ST_MFP_ISRA = MFP_TB_CLEAR;
|
||||
*ST_MFP_TBCR = MFP_TBCR_EVENT;
|
||||
// The restart above loads the counter from TBDR (= the band
|
||||
// 0 -> 1 delta), so the first fire is correctly timed. Now
|
||||
// pre-program TBDR with the band 1 -> 2 delta so the band-1
|
||||
// underflow auto-reloads with the right interval (one fire
|
||||
// ahead, matching timerBIsr). Writing TBDR while counting
|
||||
// only changes the next reload, not the in-flight count.
|
||||
if (gBandCount > 2) {
|
||||
delta = gBandStart[2] - gBandStart[1];
|
||||
if (delta == 0 || delta > 255) {
|
||||
delta = 1;
|
||||
}
|
||||
*ST_MFP_TBDR = (uint8_t)delta;
|
||||
}
|
||||
} else {
|
||||
*ST_MFP_TBCR = MFP_TBCR_STOP;
|
||||
*ST_MFP_ISRA = MFP_TB_CLEAR;
|
||||
|
|
@ -417,6 +486,23 @@ static void vblIsr(void) {
|
|||
}
|
||||
|
||||
|
||||
// Point the shifter at a screen page by writing the high and mid base
|
||||
// bytes. The low byte does not exist on STF (base is 256-aligned), so
|
||||
// two byte writes fully specify the address. The shifter latches the
|
||||
// base at the next frame start, so calling this mid-visible-frame (as
|
||||
// halPresent does, after the app's jlWaitVBL) makes the flip take
|
||||
// effect on the following frame with no tearing. No TOS / Setscreen
|
||||
// involvement, which matters because our VBL ISR replaced TOS's and
|
||||
// does not run TOS's Setscreen-latch path.
|
||||
static void setShifterVideoBase(const uint8_t *page) {
|
||||
uintptr_t addr;
|
||||
|
||||
addr = (uintptr_t)page;
|
||||
*ST_VID_BASE_HI = (uint8_t)((addr >> 16) & 0xFFu);
|
||||
*ST_VID_BASE_MID = (uint8_t)((addr >> 8) & 0xFFu);
|
||||
}
|
||||
|
||||
|
||||
static long writePrevPaletteRegs(void) {
|
||||
uint16_t i;
|
||||
for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
|
||||
|
|
@ -448,30 +534,45 @@ static void writeDiagnostics(void) {
|
|||
|
||||
bool halInit(const jlConfigT *config) {
|
||||
uintptr_t addr;
|
||||
uint16_t i;
|
||||
|
||||
(void)config;
|
||||
|
||||
// Align screen buffer to 256 bytes inside the static storage.
|
||||
// Align page 0 to 256 bytes inside the static storage; page 1
|
||||
// follows exactly one 32000-byte (256-aligned) page later, so it
|
||||
// is 256-aligned too. The shifter only displays 256-byte-aligned
|
||||
// bases, so both pages must satisfy that.
|
||||
addr = (uintptr_t)gScreenBuffer;
|
||||
addr = (addr + (ST_SCREEN_ALIGN - 1)) & ~((uintptr_t)ST_SCREEN_ALIGN - 1);
|
||||
gScreenBase = (uint8_t *)addr;
|
||||
gScreenPage[0] = (uint8_t *)addr;
|
||||
gScreenPage[1] = gScreenPage[0] + SURFACE_PIXELS_SIZE;
|
||||
gFrontIdx = 0;
|
||||
|
||||
memset(gScreenBase, 0, SURFACE_PIXELS_SIZE);
|
||||
memset(gScreenPage[0], 0, SURFACE_PIXELS_SIZE);
|
||||
memset(gScreenPage[1], 0, SURFACE_PIXELS_SIZE);
|
||||
|
||||
// Bootstrap: pretend the previous frame dirtied EVERY row so the
|
||||
// first two presents do full-page refreshes and bring both pages
|
||||
// into sync with the shadow. After that we converge to the small
|
||||
// per-row union(prev,current) cost.
|
||||
for (i = 0; i < SURFACE_HEIGHT; i++) {
|
||||
gPrevStageMinWord[i] = 0u;
|
||||
gPrevStageMaxWord[i] = STAGE_DIRTY_FULL_MAX;
|
||||
}
|
||||
|
||||
gPrevPhysbase = Physbase();
|
||||
gPrevLogbase = Logbase();
|
||||
gPrevRez = Getrez();
|
||||
|
||||
// Capture current palette so we can restore exactly on shutdown.
|
||||
{
|
||||
uint16_t i;
|
||||
for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
|
||||
gPrevPalette[i] = (uint16_t)Setcolor((int16_t)i, -1);
|
||||
}
|
||||
}
|
||||
|
||||
// Switch to ST low-res: 320x200x16, mode 0.
|
||||
Setscreen((long)gScreenBase, (long)gScreenBase, 0);
|
||||
// Switch to ST low-res: 320x200x16, mode 0. Display page 0 first;
|
||||
// halPresent flips to the off-screen page via the shifter base
|
||||
// registers from here on.
|
||||
Setscreen((long)gScreenPage[0], (long)gScreenPage[0], 0);
|
||||
gModeSet = true;
|
||||
|
||||
// Force hardware palette entry 0 to black so the overscan border
|
||||
|
|
@ -502,13 +603,23 @@ const char *halLastError(void) {
|
|||
|
||||
void halPresent(const jlSurfaceT *src) {
|
||||
StPlanarT *pd;
|
||||
uint8_t *backPage;
|
||||
uint8_t backIdx;
|
||||
int16_t y;
|
||||
int16_t firstDirty;
|
||||
int16_t lastDirty;
|
||||
int16_t runStart;
|
||||
uint8_t curMin;
|
||||
uint8_t curMax;
|
||||
uint8_t prvMin;
|
||||
uint8_t prvMax;
|
||||
uint8_t minWord;
|
||||
uint8_t maxWord;
|
||||
uint16_t groupStart;
|
||||
uint16_t groupEnd;
|
||||
uint16_t byteStart;
|
||||
uint16_t byteLen;
|
||||
bool fullWidth;
|
||||
|
||||
if (src == NULL || !gModeSet) {
|
||||
return;
|
||||
|
|
@ -519,24 +630,169 @@ void halPresent(const jlSurfaceT *src) {
|
|||
}
|
||||
refreshPaletteStateIfNeeded(src);
|
||||
|
||||
// Planar buffer -> screen RAM. Each dirty word covers 4 pixels
|
||||
// Idle-frame early-out. When the previous frame's snapshot recorded
|
||||
// no dirty rows (gPrevAnyDirty == false) AND the current frame
|
||||
// touched no row, union(prev,current) is empty: both pages already
|
||||
// match the shadow (the one settling flip after the last activity
|
||||
// ran on the first idle frame), so there is nothing to copy, no
|
||||
// page to flip, and the snapshot below would only copy clean->clean.
|
||||
// Skip the whole 200-row union scan and the 200-row snapshot copy.
|
||||
// A row is dirty iff its min word stayed at or below its max word;
|
||||
// a single byte test per row with early exit on the first dirty row
|
||||
// suffices. Palette/SCB updates already ran above, so display state
|
||||
// stays correct.
|
||||
if (!gPrevAnyDirty) {
|
||||
bool anyDirty = false;
|
||||
for (y = 0; y < SURFACE_HEIGHT; y++) {
|
||||
if (gStageMinWord[y] <= gStageMaxWord[y]) {
|
||||
anyDirty = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!anyDirty) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// PAGE FLIP: draw into the stage's shadow (pd->base), bring the
|
||||
// OFF-screen page up to date with the union(prev,current) dirty
|
||||
// rows, then point the shifter at it. The off-screen page holds
|
||||
// content from TWO frames ago (strict A/B alternation), so any row
|
||||
// dirtied in either of the last two frames may differ from the
|
||||
// current shadow -- the union refresh covers exactly those rows.
|
||||
// The displayed (front) page is never touched, so there is no
|
||||
// tearing regardless of copy duration.
|
||||
backIdx = (uint8_t)(gFrontIdx ^ 1u);
|
||||
backPage = gScreenPage[backIdx];
|
||||
|
||||
// WIN 2: bound the present to the union dirty row range. The vast
|
||||
// majority of game frames touch a small vertical band; finding the
|
||||
// first/last union-dirty row up front lets the copy loop skip the
|
||||
// huge clean head/tail of the page, and lets us bail the whole copy
|
||||
// body when nothing is dirty (steady-state idle frames). The scan
|
||||
// is 200 cheap byte compares -- far less than 200 memcpy setups.
|
||||
firstDirty = -1;
|
||||
lastDirty = -1;
|
||||
for (y = 0; y < SURFACE_HEIGHT; y++) {
|
||||
curMin = gStageMinWord[y];
|
||||
curMax = gStageMaxWord[y];
|
||||
prvMin = gPrevStageMinWord[y];
|
||||
prvMax = gPrevStageMaxWord[y];
|
||||
minWord = (curMin < prvMin) ? curMin : prvMin;
|
||||
maxWord = (curMax > prvMax) ? curMax : prvMax;
|
||||
if (minWord <= maxWord) {
|
||||
if (firstDirty < 0) {
|
||||
firstDirty = y;
|
||||
}
|
||||
lastDirty = y;
|
||||
}
|
||||
}
|
||||
|
||||
if (firstDirty >= 0) {
|
||||
// Shadow -> off-screen page. Each dirty word covers 4 pixels
|
||||
// (a quarter of an 8-byte group). Round to whole groups for a
|
||||
// simple aligned memcpy, since planar groups are the natural
|
||||
// copy unit.
|
||||
for (y = 0; y < SURFACE_HEIGHT; y++) {
|
||||
minWord = gStageMinWord[y];
|
||||
maxWord = gStageMaxWord[y];
|
||||
//
|
||||
// WIN 1: backPage and pd->base share the 160-byte row stride and
|
||||
// are both contiguous, so a run of consecutive FULL-WIDTH dirty
|
||||
// rows is one contiguous span of (rows * 160) bytes. Coalesce
|
||||
// such runs into a single memcpy: mintlib memcpy bursts long-
|
||||
// aligned blocks with movem.l/move.l, so one big copy beats many
|
||||
// tiny per-row ones (fewer call setups, longer inner burst).
|
||||
// Partial-width rows keep their own per-row group-aligned memcpy,
|
||||
// so narrow dirty bands stay correct and cheap. runStart < 0
|
||||
// means "no full-width run is open". The per-row band is the
|
||||
// union(prev,current) band so a row stale on the off-screen page
|
||||
// from two frames ago is fully refreshed, not just this frame's
|
||||
// sub-span.
|
||||
runStart = -1;
|
||||
for (y = firstDirty; y <= lastDirty; y++) {
|
||||
curMin = gStageMinWord[y];
|
||||
curMax = gStageMaxWord[y];
|
||||
prvMin = gPrevStageMinWord[y];
|
||||
prvMax = gPrevStageMaxWord[y];
|
||||
minWord = (curMin < prvMin) ? curMin : prvMin;
|
||||
maxWord = (curMax > prvMax) ? curMax : prvMax;
|
||||
if (minWord > maxWord) {
|
||||
// Clean row breaks any open full-width run.
|
||||
if (runStart >= 0) {
|
||||
memcpy(&backPage [(uint16_t)runStart * ST_BYTES_PER_ROW],
|
||||
&pd->base [(uint16_t)runStart * ST_BYTES_PER_ROW],
|
||||
(uint16_t)(y - runStart) * (uint16_t)ST_BYTES_PER_ROW);
|
||||
runStart = -1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Defense-in-depth: an upstream clipping bug could leave a
|
||||
// maxWord past the row; clamp so the groupEnd-driven byteLen
|
||||
// can never push the memcpy into the next scanline.
|
||||
if (maxWord >= SURFACE_WORDS_PER_ROW) {
|
||||
maxWord = (uint8_t)(SURFACE_WORDS_PER_ROW - 1);
|
||||
}
|
||||
groupStart = (uint16_t)(minWord >> 2);
|
||||
groupEnd = (uint16_t)((maxWord >> 2) + 1);
|
||||
byteStart = (uint16_t)(groupStart * ST_BYTES_PER_GROUP);
|
||||
byteLen = (uint16_t)((groupEnd - groupStart) * ST_BYTES_PER_GROUP);
|
||||
memcpy(&gScreenBase[(uint16_t)y * ST_BYTES_PER_ROW + byteStart],
|
||||
fullWidth = (groupStart == 0 && byteLen == (uint16_t)ST_BYTES_PER_ROW);
|
||||
|
||||
if (fullWidth) {
|
||||
// Extend (or open) the contiguous full-width run; defer
|
||||
// the copy until a non-full-width row or the range end
|
||||
// flushes it.
|
||||
if (runStart < 0) {
|
||||
runStart = y;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Partial-width row: flush any open full-width run first so
|
||||
// the page stays in row order, then copy just this row's band.
|
||||
if (runStart >= 0) {
|
||||
memcpy(&backPage [(uint16_t)runStart * ST_BYTES_PER_ROW],
|
||||
&pd->base [(uint16_t)runStart * ST_BYTES_PER_ROW],
|
||||
(uint16_t)(y - runStart) * (uint16_t)ST_BYTES_PER_ROW);
|
||||
runStart = -1;
|
||||
}
|
||||
memcpy(&backPage [(uint16_t)y * ST_BYTES_PER_ROW + byteStart],
|
||||
&pd->base [(uint16_t)y * ST_BYTES_PER_ROW + byteStart],
|
||||
byteLen);
|
||||
}
|
||||
|
||||
// Flush a full-width run that reached the end of the dirty range.
|
||||
if (runStart >= 0) {
|
||||
memcpy(&backPage [(uint16_t)runStart * ST_BYTES_PER_ROW],
|
||||
&pd->base [(uint16_t)runStart * ST_BYTES_PER_ROW],
|
||||
(uint16_t)(lastDirty + 1 - runStart) * (uint16_t)ST_BYTES_PER_ROW);
|
||||
}
|
||||
|
||||
// The off-screen page now holds the complete current image.
|
||||
// Point the shifter at it; the latch happens at the next frame
|
||||
// start (we are mid-visible-frame here, after the app's
|
||||
// jlWaitVBL), so the swap is tear-free. Only flip when there was
|
||||
// something to refresh -- flipping on a fully idle frame would
|
||||
// oscillate the display between the two pages with no benefit.
|
||||
setShifterVideoBase(backPage);
|
||||
gFrontIdx = backIdx;
|
||||
}
|
||||
|
||||
// Snapshot this frame's dirty bands as "previous" for the next
|
||||
// present. jlStagePresent clears the live bands right after
|
||||
// halPresent returns, so we must grab them now. Next present unions
|
||||
// these with that frame's dirty rows to refresh whatever the other
|
||||
// page is stale on. Record whether any row was dirty so the next
|
||||
// present can take the idle-frame early-out above.
|
||||
{
|
||||
bool anyDirty = false;
|
||||
for (y = 0; y < SURFACE_HEIGHT; y++) {
|
||||
gPrevStageMinWord[y] = gStageMinWord[y];
|
||||
gPrevStageMaxWord[y] = gStageMaxWord[y];
|
||||
if (gStageMinWord[y] <= gStageMaxWord[y]) {
|
||||
anyDirty = true;
|
||||
}
|
||||
}
|
||||
gPrevAnyDirty = anyDirty;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -577,6 +833,7 @@ uint16_t halFrameHz(void) {
|
|||
// fault on user-mode access. Resolution is 5 ms, plenty for sound
|
||||
// scheduling at 60 Hz AGI ticks.
|
||||
static volatile uint32_t gMillisBase200 = 0;
|
||||
static volatile bool gMillisBaseSet = false;
|
||||
|
||||
static long readHz200(void) {
|
||||
return (long)*((volatile uint32_t *)0x4BAUL);
|
||||
|
|
@ -585,11 +842,15 @@ static long readHz200(void) {
|
|||
uint32_t halMillisElapsed(void) {
|
||||
uint32_t ticks;
|
||||
ticks = (uint32_t)Supexec(readHz200);
|
||||
if (gMillisBase200 == 0u) {
|
||||
// 0 is a legal counter value (power-on tick 0 and every ~248-day
|
||||
// wrap), so use an explicit one-shot flag instead of a value-0
|
||||
// sentinel to capture the base.
|
||||
if (!gMillisBaseSet) {
|
||||
gMillisBase200 = ticks;
|
||||
gMillisBaseSet = true;
|
||||
return 0u;
|
||||
}
|
||||
// 200 Hz counter -> ms = ticks * 5.
|
||||
// 200 Hz counter -> ms = ticks * 5. Unsigned subtraction is wrap-safe.
|
||||
return (uint32_t)((ticks - gMillisBase200) * 5u);
|
||||
}
|
||||
|
||||
|
|
@ -626,12 +887,9 @@ void halShutdown(void) {
|
|||
}
|
||||
|
||||
|
||||
// Shared 68k fast paths for the chunky surface buffer (src/shared68k/
|
||||
// surface68k.s). Same primitives used by the Amiga port -- the stage /
|
||||
// surfaces are identical 4bpp packed layout on both 68k targets.
|
||||
extern void surface68kClearLong(uint8_t *pixels, uint16_t fillByte);
|
||||
extern void surface68kFillRectFull(uint8_t *pixels, int16_t y, uint16_t h, uint16_t fillByte);
|
||||
extern void surface68kFillRectByteAligned(uint8_t *rowFirst, uint16_t midBytes, uint16_t h, uint16_t fillByte);
|
||||
// ST word-interleaved planar 68k fast paths. The surface68kSt*
|
||||
// primitives are ST-specific (plane-major group layout); they live in
|
||||
// src/shared68k/surface68k.s alongside the Amiga-only planar helpers.
|
||||
extern void surface68kStCircleOutline(uint8_t *base, uint16_t cx, uint16_t cy, uint16_t r, uint8_t color);
|
||||
extern void surface68kStDrawLine(uint8_t *base, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t color);
|
||||
extern void surface68kStFillSpan(uint8_t *base, int16_t left, int16_t right, int16_t y, uint8_t color);
|
||||
|
|
@ -654,16 +912,11 @@ bool halFastSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
|
|||
uint8_t color;
|
||||
uint32_t loLong;
|
||||
uint32_t hiLong;
|
||||
uint32_t *p32;
|
||||
uint16_t groups;
|
||||
|
||||
if (s != jlStageGet()) {
|
||||
if (s->portData == NULL) {
|
||||
return false;
|
||||
}
|
||||
pd = (StPlanarT *)s->portData;
|
||||
if (pd == NULL) {
|
||||
return true;
|
||||
}
|
||||
color = (uint8_t)(doubled & 0x0Fu);
|
||||
/* Per-group: [p0_word][p1_word][p2_word][p3_word] = 8 bytes = 2 longs.
|
||||
* loLong = (p0_word << 16) | p1_word; hiLong = (p2_word << 16) | p3_word. */
|
||||
|
|
@ -671,8 +924,6 @@ bool halFastSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
|
|||
| ((color & 2u) ? 0x0000FFFFul : 0ul);
|
||||
hiLong = ((color & 4u) ? 0xFFFF0000ul : 0ul)
|
||||
| ((color & 8u) ? 0x0000FFFFul : 0ul);
|
||||
(void)p32;
|
||||
(void)groups;
|
||||
surface68kStLongFill(pd->base,
|
||||
(uint16_t)(ST_PLANAR_SIZE / ST_BYTES_PER_GROUP),
|
||||
loLong, hiLong);
|
||||
|
|
@ -686,7 +937,7 @@ bool halFastSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
|
|||
// never runs.
|
||||
bool halFastFillRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex) {
|
||||
(void)x; (void)y; (void)w; (void)h; (void)colorIndex;
|
||||
if (s != jlStageGet()) {
|
||||
if (s->portData == NULL) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
|
@ -726,13 +977,11 @@ bool halFastTileSnap(uint8_t *dstTilePixels, const uint8_t *srcRow0) {
|
|||
bool halFastDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) {
|
||||
StPlanarT *pd;
|
||||
|
||||
if (s != jlStageGet()) {
|
||||
if (s->portData == NULL) {
|
||||
return false;
|
||||
}
|
||||
pd = (StPlanarT *)s->portData;
|
||||
if (pd != NULL) {
|
||||
stPlanarSetPixel(pd, (int16_t)x, (int16_t)y, (uint8_t)(colorIndex & 0x0Fu));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -845,11 +1094,10 @@ static void stPlanarCircleFill(StPlanarT *pd, int16_t cx, int16_t cy, uint16_t r
|
|||
|
||||
bool halFastDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex) {
|
||||
StPlanarT *pd;
|
||||
if (s != jlStageGet()) {
|
||||
if (s->portData == NULL) {
|
||||
return false;
|
||||
}
|
||||
pd = (StPlanarT *)s->portData;
|
||||
if (pd != NULL) {
|
||||
// Asm walker assumes fully on-surface; partial-clip lines fall
|
||||
// back to the C walker which clips per-pixel.
|
||||
if (x0 >= 0 && x0 < SURFACE_WIDTH && y0 >= 0 && y0 < SURFACE_HEIGHT
|
||||
|
|
@ -858,20 +1106,16 @@ bool halFastDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t
|
|||
} else {
|
||||
stPlanarLine(pd, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool halFastDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
|
||||
StPlanarT *pd;
|
||||
if (s != jlStageGet()) {
|
||||
if (s->portData == NULL) {
|
||||
return false;
|
||||
}
|
||||
pd = (StPlanarT *)s->portData;
|
||||
if (pd == NULL) {
|
||||
return true;
|
||||
}
|
||||
/* Off-surface circles fall back to the per-pixel C walker which
|
||||
* does the clip per plot; the asm assumes fully-on-surface so it
|
||||
* can drop the clip check from the inner loop. */
|
||||
|
|
@ -890,13 +1134,10 @@ bool halFastDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_
|
|||
|
||||
bool halFastFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
|
||||
StPlanarT *pd;
|
||||
if (s != jlStageGet()) {
|
||||
if (s->portData == NULL) {
|
||||
return false;
|
||||
}
|
||||
pd = (StPlanarT *)s->portData;
|
||||
if (pd == NULL) {
|
||||
return true;
|
||||
}
|
||||
// Off-surface bounding box falls back to the C span walker, which
|
||||
// clips each span; the asm assumes the whole circle is on-surface.
|
||||
if ((int32_t)cx - (int32_t)r < 0
|
||||
|
|
@ -990,7 +1231,7 @@ bool halFastFloodWalkAndScans(uint8_t *pixels, int16_t x, int16_t y, uint8_t mat
|
|||
// false would dereference NULL s->pixels.
|
||||
bool halFastTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint16_t fillWord) {
|
||||
(void)bx; (void)by; (void)fillWord;
|
||||
if (s != jlStageGet()) {
|
||||
if (s->portData == NULL) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
|
@ -998,8 +1239,9 @@ bool halFastTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint16_t fillWord) {
|
|||
|
||||
|
||||
// Phase 2: allocate a shadow word-interleaved planar buffer per
|
||||
// surface. Both stage and non-stage get their own buffer (gScreenBase
|
||||
// remains the single display target).
|
||||
// surface. Both stage and non-stage get their own buffer; the two
|
||||
// displayed pages (gScreenPage[0/1]) are the flip targets, kept
|
||||
// separate from every surface's shadow.
|
||||
//
|
||||
// LONG alignment is required, not just word: the full-row long-fill
|
||||
// path and circle.s both do `move.l` writes on this buffer, and
|
||||
|
|
@ -1605,10 +1847,20 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
|
|||
* (where half_byte = dst_byte_col & 1). This replaces the old
|
||||
* per-pixel walker which was ~30 cycles/px = ~17K cycles/draw and
|
||||
* murdered the frame rate on shifted sprites (passenger walk,
|
||||
* flame, anything else not on an 8-px boundary). */
|
||||
* flame, anything else not on an 8-px boundary).
|
||||
*
|
||||
* Phase / placement is driven from the ORIGINAL signed x so a
|
||||
* sub-byte left clip (x negative and not a multiple of 8) keeps
|
||||
* its true source-to-destination phase. shift = x & 7 picks the
|
||||
* low 3 bits of x (correct two's-complement modulo-8 even when x
|
||||
* is negative); startByteX = x >> 3 (arithmetic) is the signed
|
||||
* destination byte column for source byte column 0. Each source
|
||||
* byte column is walked in full and the per-byte guards drop the
|
||||
* off-surface portions, so the left byte and its right-carry spill
|
||||
* are clipped independently. */
|
||||
{
|
||||
uint8_t shift = (uint8_t)(spritePxStart & 7);
|
||||
int16_t startByteX = (int16_t)(spritePxStart >> 3);
|
||||
uint8_t shift = (uint8_t)(x & 7);
|
||||
int16_t startByteX = (int16_t)(x >> 3);
|
||||
int16_t byteCol;
|
||||
|
||||
rowBase = pd->base + (uint16_t)spritePyStart * ST_BYTES_PER_ROW;
|
||||
|
|
@ -1618,11 +1870,7 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
|
|||
int16_t inTileY = (int16_t)(spritePy & 7);
|
||||
|
||||
for (byteCol = 0; byteCol < (int16_t)wTiles; byteCol++) {
|
||||
int16_t spriteBx = (int16_t)((sx >> 3) + byteCol);
|
||||
if (spriteBx >= (int16_t)wTiles) {
|
||||
break;
|
||||
}
|
||||
const uint8_t *tile = sp->tileData + (uint32_t)((tileY * wTiles + spriteBx) * 32u);
|
||||
const uint8_t *tile = sp->tileData + (uint32_t)((tileY * wTiles + byteCol) * 32u);
|
||||
const uint8_t *chunky = tile + inTileY * 4u;
|
||||
|
||||
/* c2p: 8 pixels (4 chunky bytes) -> 4 plane bytes + mask. */
|
||||
|
|
@ -1645,16 +1893,18 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
|
|||
|
||||
/* Compute ST address for the dst byte at byte_col_dst.
|
||||
* Plane bytes are interleaved word-wise inside groups of
|
||||
* 16 pixels = 8 bytes (4 planes x 2 bytes). */
|
||||
* 16 pixels = 8 bytes (4 planes x 2 bytes). The left byte
|
||||
* and the right-carry spill are guarded independently so
|
||||
* a negative dstByteCol (sub-byte left clip) still lands
|
||||
* its spill into on-surface byte dstByteCol+1. */
|
||||
int16_t dstByteCol = (int16_t)(startByteX + byteCol);
|
||||
if (dstByteCol < 0 || dstByteCol >= (int16_t)(SURFACE_WIDTH / 8)) {
|
||||
continue;
|
||||
}
|
||||
{
|
||||
uint8_t leftMask = (uint8_t)(mask >> shift);
|
||||
if (leftMask != 0u
|
||||
&& dstByteCol >= 0
|
||||
&& dstByteCol < (int16_t)(SURFACE_WIDTH / 8)) {
|
||||
uint16_t group = (uint16_t)(dstByteCol >> 1);
|
||||
uint16_t half = (uint16_t)(dstByteCol & 1);
|
||||
uint8_t leftMask = (uint8_t)(mask >> shift);
|
||||
if (leftMask != 0u) {
|
||||
uint8_t notM = (uint8_t)(~leftMask);
|
||||
uint8_t *p = rowBase + group * ST_BYTES_PER_GROUP + half;
|
||||
p[0] = (uint8_t)((p[0] & notM) | (uint8_t)(p0 >> shift));
|
||||
|
|
@ -1665,9 +1915,10 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
|
|||
if (shift != 0u) {
|
||||
uint8_t rshift = (uint8_t)(8u - shift);
|
||||
uint8_t rightMask = (uint8_t)(mask << rshift);
|
||||
if (rightMask != 0u) {
|
||||
int16_t rdc = (int16_t)(dstByteCol + 1);
|
||||
if (rdc < (int16_t)(SURFACE_WIDTH / 8)) {
|
||||
if (rightMask != 0u
|
||||
&& rdc >= 0
|
||||
&& rdc < (int16_t)(SURFACE_WIDTH / 8)) {
|
||||
uint16_t rgroup = (uint16_t)(rdc >> 1);
|
||||
uint16_t rhalf = (uint16_t)(rdc & 1);
|
||||
uint8_t notM = (uint8_t)(~rightMask);
|
||||
|
|
@ -1680,7 +1931,6 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
rowBase += ST_BYTES_PER_ROW;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
| d7 = sx (long; moveq #1 or #-1, low word used for .w add)
|
||||
| a4 = sy (long; sign-extended)
|
||||
| a3 = base
|
||||
| a5 = bitMaskWordLut
|
||||
| a5 = _gStBitMaskWordLut
|
||||
| a2 = scratch (per-pixel: base + byteOff)
|
||||
| d0,d1 = scratch
|
||||
|
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
|
||||
|
||||
| ---- DL_PLOT: 4-plane word RMW for hardcoded color ----
|
||||
| Inputs: d2.w = x, d3.w = y, a3 = base, a5 = bitMaskWordLut
|
||||
| Inputs: d2.w = x, d3.w = y, a3 = base, a5 = _gStBitMaskWordLut
|
||||
| Trashes: d0, d1, a2
|
||||
|
||||
.macro DL_PLOT color
|
||||
|
|
@ -127,7 +127,7 @@ _surface68kStDrawLine:
|
|||
|
||||
| Load base & luts.
|
||||
move.l SP_BASE(%sp),%a3
|
||||
lea bitMaskWordLut(%pc),%a5
|
||||
lea _gStBitMaskWordLut(%pc),%a5
|
||||
| a6 = yLut base (yp -> yp*160) for use in DL_PLOT.
|
||||
lea _gStRowOffsetLut(%pc),%a6
|
||||
|
||||
|
|
@ -224,8 +224,12 @@ _surface68kStDrawLine:
|
|||
|
||||
|
||||
.align 2
|
||||
| 16 word entries: bitMaskWordLut[i] = 1 << (15 - i), for i in 0..15.
|
||||
bitMaskWordLut:
|
||||
| 16 word entries: _gStBitMaskWordLut[i] = 1 << (15 - i), for i in
|
||||
| 0..15. Exported (like _gStRowOffsetLut) so circle.s can reference
|
||||
| it via absolute addressing -- single source of truth for the per-
|
||||
| pixel bit mask shared by jlDrawLine and jlDrawCircle.
|
||||
.globl _gStBitMaskWordLut
|
||||
_gStBitMaskWordLut:
|
||||
.word 0x8000, 0x4000, 0x2000, 0x1000
|
||||
.word 0x0800, 0x0400, 0x0200, 0x0100
|
||||
.word 0x0080, 0x0040, 0x0020, 0x0010
|
||||
|
|
@ -795,7 +799,7 @@ _surface68kStFillRectMulti:
|
|||
move.w %d0,%d2
|
||||
and.w #15,%d2
|
||||
add.w %d2,%d2
|
||||
lea frmLeftMaskLut(%pc),%a2
|
||||
lea _gStLeftMaskLut(%pc),%a2
|
||||
move.w (%a2,%d2.w),%d2 | d2 = leftMask
|
||||
move.w %d2,%d4
|
||||
not.w %d4 | d4 = notLeftMask
|
||||
|
|
@ -804,7 +808,7 @@ _surface68kStFillRectMulti:
|
|||
move.w %d1,%d3
|
||||
and.w #15,%d3
|
||||
add.w %d3,%d3
|
||||
lea frmRightMaskLut(%pc),%a2
|
||||
lea _gStRightMaskLut(%pc),%a2
|
||||
move.w (%a2,%d3.w),%d3 | d3 = rightMask
|
||||
move.w %d3,%d5
|
||||
not.w %d5 | d5 = notRightMask
|
||||
|
|
@ -890,15 +894,20 @@ _surface68kStFillRectMulti:
|
|||
|
||||
|
||||
.align 2
|
||||
| Same LUTs as in jlFillCircle.s; duplicated locally so each .o file's
|
||||
| PC-rel lea can reach them within its own .text segment.
|
||||
frmLeftMaskLut:
|
||||
| Leading/trailing edge-mask LUTs, exported (like _gStRowOffsetLut)
|
||||
| as the single source of truth shared with jlFillCircle.s via
|
||||
| absolute addressing.
|
||||
| _gStLeftMaskLut[i] = (1 << (16 - i)) - 1, indexed by bitFirst
|
||||
| _gStRightMaskLut[i] = ~((1 << (15 - i)) - 1), indexed by bitLast
|
||||
.globl _gStLeftMaskLut
|
||||
_gStLeftMaskLut:
|
||||
.word 0xFFFF, 0x7FFF, 0x3FFF, 0x1FFF
|
||||
.word 0x0FFF, 0x07FF, 0x03FF, 0x01FF
|
||||
.word 0x00FF, 0x007F, 0x003F, 0x001F
|
||||
.word 0x000F, 0x0007, 0x0003, 0x0001
|
||||
|
||||
frmRightMaskLut:
|
||||
.globl _gStRightMaskLut
|
||||
_gStRightMaskLut:
|
||||
.word 0x8000, 0xC000, 0xE000, 0xF000
|
||||
.word 0xF800, 0xFC00, 0xFE00, 0xFF00
|
||||
.word 0xFF80, 0xFFC0, 0xFFE0, 0xFFF0
|
||||
|
|
@ -906,6 +915,14 @@ frmRightMaskLut:
|
|||
|
||||
|
||||
.align 2
|
||||
| ST surface stride. Single asm-side source of truth, mirroring
|
||||
| include/joey/surface.h SURFACE_BYTES_PER_ROW (160). A future stride
|
||||
| change must touch both; the guard below fails the assemble if this
|
||||
| drifts from 160.
|
||||
.equ ST_BYTES_PER_ROW, 160
|
||||
.if ST_BYTES_PER_ROW != 160
|
||||
.error "ST_BYTES_PER_ROW must stay 160 (== surface.h SURFACE_BYTES_PER_ROW)"
|
||||
.endif
|
||||
| Shared y -> y*160 LUT. Used by jlDrawLine (DL_PLOT), jlDrawCircle
|
||||
| (YP_REC), jlFillCircle (SPAN_BODY). 200 words = 400 bytes.
|
||||
| Replaces a 44-cyc lsl.w #5 + lsl.w #7 + add.w shift chain with
|
||||
|
|
@ -915,7 +932,7 @@ frmRightMaskLut:
|
|||
_gStRowOffsetLut:
|
||||
.set li_y, 0
|
||||
.rept 200
|
||||
.word li_y * 160
|
||||
.word li_y * ST_BYTES_PER_ROW
|
||||
.set li_y, li_y + 1
|
||||
.endr
|
||||
|
||||
|
|
@ -924,19 +941,22 @@ _gStRowOffsetLut:
|
|||
|
|
||||
| Bulk long-fill helper for full-row fills (jlSurfaceClear, jlFillRect
|
||||
| 320x200). Writes numGroups groups of 8 bytes (loLong, hiLong)
|
||||
| starting at dst. Uses movem.l d2-d7 (3 groups = 24 bytes per
|
||||
| batch) plus a tail pair to amortize loop overhead.
|
||||
| starting at dst. Register-pumps movem.l d2-d7/a1-a6 (12 longs =
|
||||
| 6 groups = 48 bytes per batch) so the per-batch lea/subq/bne loop
|
||||
| overhead is amortised over twice the data of the old 24-byte batch.
|
||||
| a1 is caller-saved scratch (free); a2-a6 and d2-d7 are callee-saved
|
||||
| and restored on exit.
|
||||
|
|
||||
| void surface68kStLongFill(uint8_t *dst,
|
||||
| uint16_t numGroups,
|
||||
| uint32_t loLong,
|
||||
| uint32_t hiLong);
|
||||
|
|
||||
| Per-batch cost: movem.l (56 cyc) + subq (8) + bne (10) = 74 cyc
|
||||
| for 24 bytes -- ~3 cyc/byte vs ~5 cyc/byte for the straight C
|
||||
| do-while of two move.l writes.
|
||||
| STF has no blitter, so this CPU movem.l burst is the fast path; the
|
||||
| fill is memory-bandwidth bound, so the win is in cutting loop
|
||||
| overhead (one lea/subq/bne per 48 bytes instead of per 24).
|
||||
|
||||
.equ SP_LF_SAVED, 24 | d2-d7 = 6 longs
|
||||
.equ SP_LF_SAVED, 44 | d2-d7 + a2-a6 = 11 longs
|
||||
.equ SP_LF_OFF, (SP_LF_SAVED + 4)
|
||||
.equ SP_LF_DST, SP_LF_OFF + 0
|
||||
.equ SP_LF_NGROUPS, SP_LF_OFF + 4 + 2
|
||||
|
|
@ -947,45 +967,51 @@ _gStRowOffsetLut:
|
|||
.globl _surface68kStLongFill
|
||||
|
||||
_surface68kStLongFill:
|
||||
movem.l %d2-%d7,-(%sp)
|
||||
movem.l %d2-%d7/%a2-%a6,-(%sp)
|
||||
|
||||
move.l SP_LF_DST(%sp),%a0
|
||||
move.l SP_LF_LO(%sp),%d2
|
||||
move.l SP_LF_HI(%sp),%d3
|
||||
move.w SP_LF_NGROUPS(%sp),%d0
|
||||
|
||||
| Set up d2-d7 = lo, hi, lo, hi, lo, hi (movem writes
|
||||
| in d-reg order, so this gives the right alternation
|
||||
| for 3 consecutive 8-byte groups).
|
||||
| Spread the {lo,hi} pattern across 12 registers in the
|
||||
| exact order movem.l writes them (d2-d7 then a1-a6, low
|
||||
| to high address) = 6 consecutive 8-byte groups:
|
||||
| lo -> d2,d4,d6,a1,a3,a5 ; hi -> d3,d5,d7,a2,a4,a6
|
||||
move.l %d2,%d4
|
||||
move.l %d2,%d6
|
||||
move.l %d2,%a1
|
||||
move.l %d2,%a3
|
||||
move.l %d2,%a5
|
||||
move.l %d3,%d5
|
||||
move.l %d3,%d7
|
||||
move.l %d3,%a2
|
||||
move.l %d3,%a4
|
||||
move.l %d3,%a6
|
||||
|
||||
| numBatches = numGroups / 3 (quotient), tail = remainder
|
||||
| numBatches = numGroups / 6 (quotient), tail = remainder
|
||||
ext.l %d0
|
||||
divu.w #3,%d0
|
||||
divu.w #6,%d0
|
||||
move.l %d0,%d1
|
||||
swap %d1 | d1.w = remainder
|
||||
swap %d1 | d1.w = remainder (0..5)
|
||||
tst.w %d0 | quotient
|
||||
beq.s .Llf_tail
|
||||
.Llf_loop:
|
||||
movem.l %d2-%d7,(%a0)
|
||||
lea 24(%a0),%a0
|
||||
movem.l %d2-%d7/%a1-%a6,(%a0) | 48 bytes = 6 groups
|
||||
lea 48(%a0),%a0
|
||||
subq.w #1,%d0
|
||||
bne.s .Llf_loop
|
||||
|
||||
.Llf_tail:
|
||||
| Remainder: 0, 1, or 2 groups of 8 bytes
|
||||
| Remainder: 0..5 groups of 8 bytes
|
||||
tst.w %d1
|
||||
beq.s .Llf_done
|
||||
.Llf_tailLoop:
|
||||
move.l %d2,(%a0)+
|
||||
move.l %d3,(%a0)+
|
||||
subq.w #1,%d1
|
||||
beq.s .Llf_done
|
||||
move.l %d2,(%a0)+
|
||||
move.l %d3,(%a0)+
|
||||
bne.s .Llf_tailLoop
|
||||
|
||||
.Llf_done:
|
||||
movem.l (%sp)+,%d2-%d7
|
||||
movem.l (%sp)+,%d2-%d7/%a2-%a6
|
||||
rts
|
||||
|
|
|
|||
|
|
@ -94,9 +94,15 @@
|
|||
|
||||
// ----- Mix configuration -----
|
||||
|
||||
// MIX_RATE is the nominal target; the SB's 8-bit time-constant DAC can
|
||||
// only clock at 1000000/(256-tc) Hz, which is not exactly 22050. The
|
||||
// SB hardware rate is the fixed one, so the effective rate derived from
|
||||
// the same time constant (see gEffRate, set in halAudioInit) is the
|
||||
// single source of truth handed to libxmp and the SFX resampler.
|
||||
#define MIX_RATE 22050
|
||||
#define MIX_DMA_BUFFER 4096 // total; two 2048-byte halves
|
||||
#define MIX_HALF (MIX_DMA_BUFFER / 2)
|
||||
#define SB_DSP_CLOCK_HZ 1000000u // 8-bit DSP time-constant base
|
||||
|
||||
#define ISR_LOCK_SIZE 4096
|
||||
|
||||
|
|
@ -109,6 +115,12 @@ static uint8_t gDspMajor = 0;
|
|||
static uint8_t gDspMinor = 0;
|
||||
static bool gDspPresent = false;
|
||||
|
||||
// Effective SB playback rate derived from the DSP time constant. Set
|
||||
// once in halAudioInit and used as the single rate source of truth for
|
||||
// libxmp's mixer and the SFX resampler, so they match what the DAC
|
||||
// actually clocks (~22222 Hz for a 22050 Hz target) instead of drifting.
|
||||
static uint16_t gEffRate = MIX_RATE;
|
||||
|
||||
// Conventional-memory DMA buffer.
|
||||
static int gDmaSelector = 0;
|
||||
static uint32_t gDmaLinear = 0;
|
||||
|
|
@ -153,6 +165,7 @@ static void dspWrite(uint8_t value);
|
|||
static void freeDmaBuffer(void);
|
||||
static void haltDspDma(void);
|
||||
static void parseBlasterEnv(void);
|
||||
static void picIrqPortBit(uint8_t irq, uint16_t *port, uint8_t *bit);
|
||||
static void picMaskIrq(uint8_t irq);
|
||||
static void picUnmaskIrq(uint8_t irq);
|
||||
static int picVectorForIrq(uint8_t irq);
|
||||
|
|
@ -324,13 +337,13 @@ static void programDmaController(void) {
|
|||
}
|
||||
|
||||
|
||||
// 8-bit DSP time constant: tc = 256 - 1000000 / rate. SB plays at
|
||||
// 1000000 / (256 - tc) Hz, so we round to the nearest representable
|
||||
// rate. For 22050 Hz: tc = 256 - 45 = 211.
|
||||
// 8-bit DSP time constant: tc = 256 - SB_DSP_CLOCK_HZ / rate. SB plays
|
||||
// at SB_DSP_CLOCK_HZ / (256 - tc) Hz, so we round to the nearest
|
||||
// representable rate. For 22050 Hz: tc = 256 - 45 = 211.
|
||||
static uint8_t dspTimeConst(uint16_t rate) {
|
||||
uint32_t tc;
|
||||
|
||||
tc = 256u - (1000000u / rate);
|
||||
tc = 256u - (SB_DSP_CLOCK_HZ / rate);
|
||||
if (tc < 4) {
|
||||
tc = 4;
|
||||
}
|
||||
|
|
@ -364,6 +377,19 @@ static void haltDspDma(void) {
|
|||
}
|
||||
|
||||
|
||||
// Decode an IRQ number to its PIC Interrupt Mask Register port and the
|
||||
// mask bit within that register. IRQ 0-7 live in PIC1, 8-15 in PIC2.
|
||||
static void picIrqPortBit(uint8_t irq, uint16_t *port, uint8_t *bit) {
|
||||
if (irq < 8) {
|
||||
*port = PIC1_IMR;
|
||||
*bit = (uint8_t)(1u << irq);
|
||||
} else {
|
||||
*port = PIC2_IMR;
|
||||
*bit = (uint8_t)(1u << (irq - 8));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Unmask the IRQ line in the PIC's Interrupt Mask Register so the IRQ
|
||||
// can reach the CPU. Some environments (DOSBox in particular) leave the
|
||||
// SB IRQ masked at boot; without this the hooked vector never fires
|
||||
|
|
@ -373,13 +399,7 @@ static void picUnmaskIrq(uint8_t irq) {
|
|||
uint8_t bit;
|
||||
uint8_t mask;
|
||||
|
||||
if (irq < 8) {
|
||||
port = PIC1_IMR;
|
||||
bit = (uint8_t)(1u << irq);
|
||||
} else {
|
||||
port = PIC2_IMR;
|
||||
bit = (uint8_t)(1u << (irq - 8));
|
||||
}
|
||||
picIrqPortBit(irq, &port, &bit);
|
||||
mask = inportb(port);
|
||||
mask = (uint8_t)(mask & ~bit);
|
||||
outportb(port, mask);
|
||||
|
|
@ -398,13 +418,7 @@ static void picMaskIrq(uint8_t irq) {
|
|||
uint8_t bit;
|
||||
uint8_t mask;
|
||||
|
||||
if (irq < 8) {
|
||||
port = PIC1_IMR;
|
||||
bit = (uint8_t)(1u << irq);
|
||||
} else {
|
||||
port = PIC2_IMR;
|
||||
bit = (uint8_t)(1u << (irq - 8));
|
||||
}
|
||||
picIrqPortBit(irq, &port, &bit);
|
||||
mask = inportb(port);
|
||||
mask = (uint8_t)(mask | bit);
|
||||
outportb(port, mask);
|
||||
|
|
@ -474,6 +488,11 @@ bool halAudioInit(void) {
|
|||
gDspMajor = (uint8_t)major;
|
||||
gDspMinor = (uint8_t)minor;
|
||||
|
||||
// Lock in the effective playback rate from the same time constant
|
||||
// the SB DAC will receive, so libxmp and the SFX resampler agree
|
||||
// with the hardware instead of using the nominal MIX_RATE.
|
||||
gEffRate = (uint16_t)(SB_DSP_CLOCK_HZ / (256u - dspTimeConst(MIX_RATE)));
|
||||
|
||||
if (!allocDmaBuffer()) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -484,6 +503,20 @@ bool halAudioInit(void) {
|
|||
return false;
|
||||
}
|
||||
|
||||
// Allocate the iret wrapper first so its failure path needs no
|
||||
// unlock; the page locks below are only required once the vector
|
||||
// is actually hooked.
|
||||
vector = picVectorForIrq(gSbIrq);
|
||||
_go32_dpmi_get_protected_mode_interrupt_vector(vector, &gOldIrqVec);
|
||||
gNewIrqVec.pm_offset = (unsigned long)sbIsr;
|
||||
gNewIrqVec.pm_selector = _go32_my_cs();
|
||||
if (_go32_dpmi_allocate_iret_wrapper(&gNewIrqVec) != 0) {
|
||||
xmp_free_context(gXmpCtx);
|
||||
gXmpCtx = NULL;
|
||||
freeDmaBuffer();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Lock ISR code + the data sbIsr touches at IRQ time. The mixer
|
||||
// refill no longer runs in interrupt context, so refillHalf and
|
||||
// libxmp internals do not need locking.
|
||||
|
|
@ -493,16 +526,6 @@ bool halAudioInit(void) {
|
|||
_go32_dpmi_lock_data((void *)&gSbBase, sizeof(gSbBase));
|
||||
_go32_dpmi_lock_data((void *)&gSbIrq, sizeof(gSbIrq));
|
||||
|
||||
vector = picVectorForIrq(gSbIrq);
|
||||
_go32_dpmi_get_protected_mode_interrupt_vector(vector, &gOldIrqVec);
|
||||
gNewIrqVec.pm_offset = (unsigned long)sbIsr;
|
||||
gNewIrqVec.pm_selector = _go32_my_cs();
|
||||
if (_go32_dpmi_allocate_iret_wrapper(&gNewIrqVec) != 0) {
|
||||
xmp_free_context(gXmpCtx);
|
||||
gXmpCtx = NULL;
|
||||
freeDmaBuffer();
|
||||
return false;
|
||||
}
|
||||
_go32_dpmi_set_protected_mode_interrupt_vector(vector, &gNewIrqVec);
|
||||
gIrqHooked = true;
|
||||
|
||||
|
|
@ -569,7 +592,7 @@ void halAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
|
|||
return;
|
||||
}
|
||||
gXmpLoaded = true;
|
||||
if (xmp_start_player(gXmpCtx, MIX_RATE,
|
||||
if (xmp_start_player(gXmpCtx, gEffRate,
|
||||
XMP_FORMAT_8BIT | XMP_FORMAT_UNSIGNED | XMP_FORMAT_MONO) != 0) {
|
||||
xmp_release_module(gXmpCtx);
|
||||
gXmpLoaded = false;
|
||||
|
|
@ -588,7 +611,7 @@ void halAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1
|
|||
if (!gDspPresent || slot >= JOEY_AUDIO_SFX_SLOTS) {
|
||||
return;
|
||||
}
|
||||
audioSfxSlotArm(&gSfxSlots[slot], sample, length, rateHz, MIX_RATE);
|
||||
audioSfxSlotArm(&gSfxSlots[slot], sample, length, rateHz, gEffRate);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -596,7 +619,7 @@ void halAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uin
|
|||
if (!gDspPresent || slot >= JOEY_AUDIO_SFX_SLOTS) {
|
||||
return;
|
||||
}
|
||||
audioSfxSlotArmStream(&gSfxSlots[slot], fill, ctx, rateHz, MIX_RATE);
|
||||
audioSfxSlotArmStream(&gSfxSlots[slot], fill, ctx, rateHz, gEffRate);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// DOS HAL: VGA mode 13h, 8-bit DAC, nibble-expand present.
|
||||
// DOS HAL: VGA mode 13h, 6-bit DAC, nibble-expand present.
|
||||
//
|
||||
// JoeyLib surfaces are 4bpp chunky with a 200-entry SCB table selecting
|
||||
// one of 16 palettes per scanline. Mode 13h is 8bpp; the three pairs of
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
#include <string.h>
|
||||
#include <time.h> // uclock / UCLOCKS_PER_SEC
|
||||
|
||||
#include <dos.h>
|
||||
#include <dpmi.h>
|
||||
#include <go32.h>
|
||||
#include <pc.h>
|
||||
|
|
@ -40,6 +41,7 @@
|
|||
static void crashHandler(int sig);
|
||||
static void expandAndWriteLine(const jlSurfaceT *src, int16_t y, int16_t x, uint16_t w, uint8_t *dst);
|
||||
static void installCrashLog(void);
|
||||
static void rebuildExpandLut(uint8_t palBase);
|
||||
static void uploadPalette(const jlSurfaceT *src);
|
||||
static void uploadPaletteIfNeeded(const jlSurfaceT *src);
|
||||
|
||||
|
|
@ -49,48 +51,74 @@ static uint8_t *gVgaMem = NULL;
|
|||
static bool gNearEnabled = false;
|
||||
static FILE *gCrashLog = NULL;
|
||||
|
||||
// Frame counter via $3DA bit 3 polling; rising edge marks the start
|
||||
// of vertical retrace. Caller polls fast enough that no edge is
|
||||
// missed (UBER's hot loop is far below 70 Hz period even on a 386).
|
||||
static uint16_t gFrameCount;
|
||||
static uint8_t gPrevInVret;
|
||||
|
||||
// Cached palette from the last present. VGA DAC programming is ~768
|
||||
// outportb calls; skip it when the source palette is unchanged.
|
||||
static uint16_t gCachedPalette[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE];
|
||||
static bool gCacheValid = false;
|
||||
|
||||
// Scratch row buffer for halPresent. expandAndWriteLine builds the
|
||||
// 8bpp form of a dirty band here, then a single memcpy lowers to REP
|
||||
// MOVS to push the whole band into VGA RAM in one bus-batched run.
|
||||
// The original per-pixel loop wrote one byte at a time, which stalled
|
||||
// the CPU on every ISA cycle (~125 ns at 8 MHz bus); REP MOVS pipelines
|
||||
// 16-bit ISA writes back-to-back, much better on slow CPUs.
|
||||
static uint8_t gExpandRow[SURFACE_WIDTH];
|
||||
// Expand LUT for halPresent. Maps a 4bpp-packed source byte to the
|
||||
// 2-byte little-endian mode-13h output pair for the current scanline's
|
||||
// SCB. The low byte (= dst[x], lower VGA address = LEFT pixel) holds
|
||||
// the packed byte's HIGH nibble; the high byte (= dst[x+1] = RIGHT
|
||||
// pixel) holds the LOW nibble. Storing lut[byte] as one 16-bit x86
|
||||
// write therefore lands the left pixel at the lower address, matching
|
||||
// the old two-byte sequence exactly. Rebuilt only when the SCB-derived
|
||||
// palBase changes from the previous row (most stages run long runs of
|
||||
// equal SCB), so the per-row cost is one compare, not a 256-entry fill.
|
||||
static uint16_t gExpandLut[256];
|
||||
static uint8_t gExpandLutBase = 0;
|
||||
static bool gExpandLutValid = false;
|
||||
|
||||
// ----- Internal helpers (alphabetical) -----
|
||||
|
||||
static void expandAndWriteLine(const jlSurfaceT *src, int16_t y, int16_t x, uint16_t w, uint8_t *dst) {
|
||||
uint8_t palBase;
|
||||
const uint8_t *srcBytes;
|
||||
uint8_t *out;
|
||||
uint16_t *out;
|
||||
uint16_t pairs;
|
||||
uint16_t i;
|
||||
uint8_t byte;
|
||||
|
||||
palBase = (uint8_t)(src->scb[y] << 4);
|
||||
if (!gExpandLutValid || palBase != gExpandLutBase) {
|
||||
rebuildExpandLut(palBase);
|
||||
}
|
||||
// Dirty bands are chunky-word aligned (halPresent computes
|
||||
// pixelX = minWord*4, pixelW = ...*4), so x is even and 1 source
|
||||
// byte (= 2 nibbles) maps to 2 destination bytes -- no per-pixel
|
||||
// odd/even shift branch needed.
|
||||
// byte (= 2 nibbles) maps to 2 destination bytes. The even x keeps
|
||||
// &dst[x] 16-bit aligned, so each pair is one aligned 16-bit store
|
||||
// straight into VGA RAM -- no scratch row, no trailing memcpy.
|
||||
srcBytes = &src->pixels[y * SURFACE_BYTES_PER_ROW + (x >> 1)];
|
||||
out = gExpandRow;
|
||||
out = (uint16_t *)&dst[x];
|
||||
pairs = (uint16_t)(w >> 1);
|
||||
|
||||
for (i = 0; i < pairs; i++) {
|
||||
byte = *srcBytes++;
|
||||
*out++ = (uint8_t)(palBase | (byte >> 4));
|
||||
*out++ = (uint8_t)(palBase | (byte & 0x0Fu));
|
||||
*out++ = gExpandLut[*srcBytes++];
|
||||
}
|
||||
}
|
||||
|
||||
// Single bus-batched copy to VGA. memcpy in DJGPP libc lowers to
|
||||
// REP MOVS, which queues 16-bit ISA write cycles back-to-back
|
||||
// instead of stalling the CPU on every byte store.
|
||||
memcpy(&dst[x], gExpandRow, w);
|
||||
|
||||
static void rebuildExpandLut(uint8_t palBase) {
|
||||
uint16_t byte;
|
||||
uint8_t leftPixel;
|
||||
uint8_t rightPixel;
|
||||
|
||||
for (byte = 0; byte < 256u; byte++) {
|
||||
// Packed high nibble = LEFT pixel (lower x); packed low nibble
|
||||
// = RIGHT pixel. The little-endian word puts the LEFT pixel in
|
||||
// the low byte so a 16-bit store lands it at the lower (even)
|
||||
// VGA address -- byte-for-byte identical to the old pair store.
|
||||
leftPixel = (uint8_t)(palBase | (byte >> 4));
|
||||
rightPixel = (uint8_t)(palBase | (byte & 0x0Fu));
|
||||
gExpandLut[byte] = (uint16_t)((uint16_t)rightPixel << 8 | (uint16_t)leftPixel);
|
||||
}
|
||||
gExpandLutBase = palBase;
|
||||
gExpandLutValid = true;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -102,6 +130,12 @@ static void uploadPalette(const jlSurfaceT *src) {
|
|||
uint8_t g;
|
||||
uint8_t b;
|
||||
|
||||
// The whole index+data burst relies on the DAC's auto-increment
|
||||
// index latch staying in step across the 768 data writes. Mask
|
||||
// interrupts so no installed ISR can desync the latch by touching
|
||||
// $3C8/$3C9 mid-burst. Invariant: no installed ISR may write the
|
||||
// VGA DAC ports while a palette upload is in progress.
|
||||
disable();
|
||||
outportb(DAC_INDEX_PORT, 0);
|
||||
for (p = 0; p < SURFACE_PALETTE_COUNT; p++) {
|
||||
for (c = 0; c < SURFACE_COLORS_PER_PALETTE; c++) {
|
||||
|
|
@ -109,11 +143,14 @@ static void uploadPalette(const jlSurfaceT *src) {
|
|||
r = (uint8_t)((rgb >> 8) & 0x0F);
|
||||
g = (uint8_t)((rgb >> 4) & 0x0F);
|
||||
b = (uint8_t)(rgb & 0x0F);
|
||||
// VGA DAC is 6-bit per channel; expand JoeyLib's 4-bit
|
||||
// channel to 6 bits (0..63).
|
||||
outportb(DAC_DATA_PORT, (uint8_t)((r << 2) | (r >> 2)));
|
||||
outportb(DAC_DATA_PORT, (uint8_t)((g << 2) | (g >> 2)));
|
||||
outportb(DAC_DATA_PORT, (uint8_t)((b << 2) | (b >> 2)));
|
||||
}
|
||||
}
|
||||
enable();
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -256,6 +293,13 @@ void halPresent(const jlSurfaceT *src) {
|
|||
if (minWord > maxWord) {
|
||||
continue;
|
||||
}
|
||||
// Defense-in-depth: an upstream clipping bug that wrote
|
||||
// maxWord >= SURFACE_WORDS_PER_ROW would push the expand copy
|
||||
// past the 320-byte VGA row into the next scanline. Clamp to
|
||||
// the in-contract maximum word index.
|
||||
if (maxWord >= SURFACE_WORDS_PER_ROW) {
|
||||
maxWord = (uint8_t)(SURFACE_WORDS_PER_ROW - 1);
|
||||
}
|
||||
pixelX = (int16_t)((uint16_t)minWord << 2);
|
||||
pixelW = (uint16_t)(((uint16_t)maxWord - minWord + 1u) << 2);
|
||||
expandAndWriteLine(src, y, pixelX, pixelW, &gVgaMem[y * VGA_STRIDE]);
|
||||
|
|
@ -279,12 +323,6 @@ void halWaitVBL(void) {
|
|||
}
|
||||
|
||||
|
||||
// Frame counter via $3DA bit 3 polling; rising edge marks the start
|
||||
// of vertical retrace. Caller polls fast enough that no edge is
|
||||
// missed (UBER's hot loop is far below 70 Hz period even on a 386).
|
||||
static uint16_t gFrameCount = 0;
|
||||
static uint8_t gPrevInVret = 0;
|
||||
|
||||
uint16_t halFrameCount(void) {
|
||||
uint8_t now;
|
||||
|
||||
|
|
@ -323,8 +361,11 @@ extern uint16_t gAudioIsrHz;
|
|||
extern volatile bool gAudioIsrActive;
|
||||
|
||||
static uclock_t gMillisBase = 0;
|
||||
static bool gMillisBaseSet = false;
|
||||
|
||||
uint32_t halMillisElapsed(void) {
|
||||
uclock_t delta;
|
||||
|
||||
if (gAudioIsrActive && gAudioIsrHz > 0u) {
|
||||
// Each ISR fire = 1000/hz ms. Hardware-IRQ-locked, so
|
||||
// wall-clock-correct and monotonic. uint64 promotion in
|
||||
|
|
@ -333,11 +374,20 @@ uint32_t halMillisElapsed(void) {
|
|||
return (uint32_t)(((uint64_t)gAudioIsrFires * 1000uL)
|
||||
/ (uint32_t)gAudioIsrHz);
|
||||
}
|
||||
if (gMillisBase == 0) {
|
||||
// Use an explicit one-shot flag rather than gMillisBase == 0, so a
|
||||
// legitimate uclock() value of 0 on the first call cannot re-arm
|
||||
// the base every call.
|
||||
if (!gMillisBaseSet) {
|
||||
gMillisBase = uclock();
|
||||
gMillisBaseSet = true;
|
||||
return 0u;
|
||||
}
|
||||
return (uint32_t)((uclock() - gMillisBase) / (UCLOCKS_PER_SEC / 1000u));
|
||||
// Pre-scale by 1000 before dividing by UCLOCKS_PER_SEC instead of
|
||||
// dividing by the truncated UCLOCKS_PER_SEC/1000 (== 1193, not
|
||||
// 1193.18), which would run the clock ~0.015% fast. The uint64
|
||||
// intermediate keeps delta*1000 from overflowing.
|
||||
delta = uclock() - gMillisBase;
|
||||
return (uint32_t)(((uint64_t)delta * 1000uL) / (uint64_t)UCLOCKS_PER_SEC);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -269,20 +269,30 @@ static void joystickPoll(void) {
|
|||
gJoyButtonState[JOYSTICK_1][JOY_BUTTON_0] = (btn & BIOS_BTN_B1) == 0;
|
||||
gJoyButtonState[JOYSTICK_1][JOY_BUTTON_1] = (btn & BIOS_BTN_B2) == 0;
|
||||
|
||||
// Announce presence from the successful switches read that produced
|
||||
// live button state, independent of the axis decode below -- matches
|
||||
// the ST/IIgs convention. Doing it here (not after the position read)
|
||||
// keeps connected status consistent with the button state.
|
||||
gJoyConnected[JOYSTICK_0] = true;
|
||||
gJoyConnected[JOYSTICK_1] = true;
|
||||
|
||||
memset(&r, 0, sizeof(r));
|
||||
r.h.ah = BIOS_FN_GAMEPORT;
|
||||
r.x.dx = BIOS_GAMEPORT_DX_POS;
|
||||
__dpmi_int(BIOS_INT, &r);
|
||||
if (r.x.flags & BIOS_FLAGS_CARRY) {
|
||||
// A failed position read leaves the stick connected (buttons are
|
||||
// live), but report centered axes rather than stale values.
|
||||
gJoyAxisX[JOYSTICK_0] = 0;
|
||||
gJoyAxisY[JOYSTICK_0] = 0;
|
||||
gJoyAxisX[JOYSTICK_1] = 0;
|
||||
gJoyAxisY[JOYSTICK_1] = 0;
|
||||
return;
|
||||
}
|
||||
gJoyAxisX[JOYSTICK_0] = scaleAxis(r.x.ax);
|
||||
gJoyAxisY[JOYSTICK_0] = scaleAxis(r.x.bx);
|
||||
gJoyAxisX[JOYSTICK_1] = scaleAxis(r.x.cx);
|
||||
gJoyAxisY[JOYSTICK_1] = scaleAxis(r.x.dx);
|
||||
|
||||
gJoyConnected[JOYSTICK_0] = true;
|
||||
gJoyConnected[JOYSTICK_1] = true;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -296,6 +306,12 @@ static void mousePoll(void) {
|
|||
memset(&r, 0, sizeof(r));
|
||||
r.x.ax = MOUSE_FN_GET_STATE;
|
||||
__dpmi_int(MOUSE_INT, &r);
|
||||
// Assumes a mode-13h-aware driver (DOSBox / DOSBox-X) that returns
|
||||
// native 0..SURFACE_WIDTH-1 X. Some real MS-compatible drivers keep
|
||||
// an internal 640-wide virtual space in mode 13h where fn 7/8 only
|
||||
// clamp min/max (not scale), so X comes back doubled and would need
|
||||
// a >>1; that scale-detection is not done here. Y is unaffected
|
||||
// (only X is virtualized in 320-wide modes).
|
||||
gMouseX = (int16_t)r.x.cx;
|
||||
gMouseY = (int16_t)r.x.dx;
|
||||
btn = r.x.bx;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,8 @@
|
|||
// Apple IIgs audio HAL -- single source linked into every IIgs demo.
|
||||
// Earlier we split this into an audio.c no-op stub and an audio_full.c
|
||||
// real implementation, filtering audio_full.c out of non-AUDIO source
|
||||
// sets, because pulling Memory Manager + the 34 KB NTP replayer into
|
||||
// every binary blew the ORCA Linker's blank-segment / "Expression too
|
||||
// complex" budget. Now that we know how to name load segments (see
|
||||
// ORCA/C ch. 30 "segment statement"), we put every function in this
|
||||
// file into a named AUDIOIMPL load segment; the GS/OS loader places
|
||||
// it in its own bank, so non-AUDIO binaries pay only for the data
|
||||
// references, not the implementation code.
|
||||
// Memory Manager + the 34 KB NTP replayer only need to be present in
|
||||
// AUDIO binaries; link-time dead-stripping drops this implementation
|
||||
// code from non-AUDIO binaries, which pay only for the data
|
||||
// references they actually use.
|
||||
//
|
||||
// The NinjaTrackerPlus replayer is Merlin32-assembled at build time
|
||||
// to ntpplayer.bin and baked into this TU as gNtpPlayerBytes via the
|
||||
|
|
@ -18,25 +13,27 @@
|
|||
|
||||
#include <string.h>
|
||||
|
||||
#include <types.h>
|
||||
#include <memory.h>
|
||||
// Memory Manager via <iigs/toolbox.h>. Handles/pointers are void* and the
|
||||
// owner ID comes from MMStartUp(); _ownerid maps to MMStartUp() and
|
||||
// _toolErr collapses to 0 so each `|| _toolErr != 0` guard reduces to the
|
||||
// NULL-handle failure check. Memory Manager attribute bits are defined
|
||||
// here (values from Apple's Memory Manager reference).
|
||||
#include <iigs/toolbox.h>
|
||||
typedef void *Handle;
|
||||
typedef void *Pointer;
|
||||
#define _ownerid (MMStartUp())
|
||||
#define _toolErr 0
|
||||
#define attrPage 0x0004
|
||||
#define attrNoCross 0x0010
|
||||
#define attrFixed 0x4000
|
||||
#define attrLocked 0x8000
|
||||
|
||||
#include "hal.h"
|
||||
#include "joey/audio.h"
|
||||
|
||||
// Place every function defined below in the shared DRAWPRIMS overflow
|
||||
// load segment so the linker keeps the implementation code out of
|
||||
// _ROOT in every binary that includes this TU. (See ORCA/C ch. 30
|
||||
// "segment statement". Reusing the same segment as draw.c / tile.c
|
||||
// rather than picking a unique name keeps the linker's symbol-
|
||||
// resolution expressions flat.)
|
||||
//
|
||||
// The 34 KB NTP replayer bytes are NOT in this segment -- ORCA/C's
|
||||
// `segment` statement only relocates functions, not data. They live
|
||||
// in their own NTPDATA load segment, declared in build/iigs/audio/
|
||||
// ntpdata.asm (auto-generated from ntpplayer.bin by make/iigs.mk).
|
||||
// We just extern the symbols here.
|
||||
segment "DRAWPRIMS";
|
||||
// The 34 KB NTP replayer bytes live in their own data segment
|
||||
// (build/iigs/audio/ntpdata.asm, auto-generated from ntpplayer.bin by
|
||||
// make/iigs.mk). We just extern the symbols here.
|
||||
|
||||
extern const unsigned char gNtpPlayerBytes[];
|
||||
extern const unsigned long gNtpPlayerBytes_len;
|
||||
|
|
@ -99,8 +96,8 @@ static bool gNTPPlaying = false;
|
|||
|
||||
// Per-slot config cache. halAudioPlaySfx's biggest cost is the
|
||||
// per-byte XOR-with-$80 loop over the entire sample (signed -> DOC's
|
||||
// unsigned format), which on a 4 KB sample is ~120 k cyc / ~43 ms in
|
||||
// ORCA-C. Most callers play the same SFX repeatedly into the same
|
||||
// unsigned format), which on a 4 KB sample is ~120 k cyc / ~43 ms.
|
||||
// Most callers play the same SFX repeatedly into the same
|
||||
// slot. Cache (sample ptr, length, rate) per slot; on cache hit
|
||||
// (same sample re-triggered) skip the byte copy AND the struct
|
||||
// rebuild, just re-fire NTPstreamsound.
|
||||
|
|
@ -129,17 +126,14 @@ static uint16_t gSfxSlotRateHz[JOEY_AUDIO_SFX_SLOTS] = { 0 };
|
|||
// `jsl gCallStub` followed by `rep #0x30` -- no global-operand
|
||||
// references in the asm block.
|
||||
//
|
||||
// The trailing REP is required to satisfy ORCA-C's `longa on /
|
||||
// longi on` exit contract: the byte writes the call sites do to
|
||||
// patch this stub leave M=8, and NTP's RTL leaves M/X in whatever
|
||||
// state NTP chose. Without REP, the C epilogue's ADC/LDX widths
|
||||
// decode wrong and the function returns to garbage. See
|
||||
// project_iigs_inline_asm_mode.md.
|
||||
// The trailing REP restores M=16/X=16 for the function epilogue: the
|
||||
// byte writes the call sites do to patch this stub leave M=8, and
|
||||
// NTP's RTL leaves M/X in whatever state NTP chose. Without REP, the
|
||||
// C epilogue's ADC/LDX widths decode wrong and the function returns
|
||||
// to garbage. See project_iigs_inline_asm_mode.md.
|
||||
//
|
||||
// (We tried the obvious `lda gAsmGlobal / jsl gJslStub` shape first;
|
||||
// ORCA's inline assembler accepts the first absolute-global operand
|
||||
// in a block but rejects the second and third with "invalid operand".
|
||||
// Folding the loads into the stub side-steps the issue entirely.)
|
||||
// Folding the X/Y/A loads and the JSL target into the stub keeps the
|
||||
// inline asm block free of absolute-global operands entirely.
|
||||
//
|
||||
// Layout (15 bytes):
|
||||
// 00: C2 30 REP #$30 ; 16-bit M/X
|
||||
|
|
@ -183,7 +177,7 @@ static bool loadNTP(void) {
|
|||
return false;
|
||||
}
|
||||
HLock(h);
|
||||
p = *h;
|
||||
p = *(void **)h;
|
||||
if (p == NULL) {
|
||||
DisposeHandle(h);
|
||||
return false;
|
||||
|
|
@ -222,7 +216,7 @@ bool halAudioInit(void) {
|
|||
}
|
||||
HLock(modHandle);
|
||||
gModuleHandle = modHandle;
|
||||
gModuleBase = (uint32_t)*modHandle;
|
||||
gModuleBase = (uint32_t)*(void **)modHandle;
|
||||
|
||||
// Third fixed-bank handle for SFX sample storage. Same idea as
|
||||
// the module handle: NTPstreamsound wants a 24-bit pointer, and
|
||||
|
|
@ -239,7 +233,7 @@ bool halAudioInit(void) {
|
|||
}
|
||||
HLock(sfxHandle);
|
||||
gSfxHandle = sfxHandle;
|
||||
gSfxBase = (uint32_t)*sfxHandle;
|
||||
gSfxBase = (uint32_t)*(void **)sfxHandle;
|
||||
|
||||
gNTPReady = true;
|
||||
return true;
|
||||
|
|
@ -312,17 +306,19 @@ void halAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
|
|||
(uint16_t)(gModuleBase & 0xFFFFu),
|
||||
(uint16_t)((gModuleBase >> 16) & 0xFFu),
|
||||
0);
|
||||
asm {
|
||||
jsl gCallStub
|
||||
rep #0x30
|
||||
}
|
||||
__asm__ volatile (
|
||||
"jsl gCallStub\n\t"
|
||||
"rep #0x30"
|
||||
::: "memory"
|
||||
);
|
||||
|
||||
// NTPplay(loopFlag in A). 0 = loop forever, 1 = play once.
|
||||
buildCallStub(gNTPBase + 3, 0, 0, loop ? 0 : 1);
|
||||
asm {
|
||||
jsl gCallStub
|
||||
rep #0x30
|
||||
}
|
||||
__asm__ volatile (
|
||||
"jsl gCallStub\n\t"
|
||||
"rep #0x30"
|
||||
::: "memory"
|
||||
);
|
||||
gNTPPlaying = true;
|
||||
}
|
||||
|
||||
|
|
@ -353,7 +349,7 @@ void halAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1
|
|||
// for this slot? Then the slot's DOC sample bytes and stream
|
||||
// struct are already correct -- skip the 4 KB byte-XOR loop and
|
||||
// the 15-byte struct rebuild, both of which together can run
|
||||
// ~50 ms per call in ORCA-C.
|
||||
// ~50 ms per call.
|
||||
if (sample == gSfxSlotSample[slot] &&
|
||||
length == gSfxSlotLength[slot] &&
|
||||
rateHz == gSfxSlotRateHz[slot]) {
|
||||
|
|
@ -401,10 +397,11 @@ void halAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1
|
|||
(uint16_t)(structAddr & 0xFFFFu),
|
||||
(uint16_t)((structAddr >> 16) & 0xFFu),
|
||||
0);
|
||||
asm {
|
||||
jsl gCallStub
|
||||
rep #0x30
|
||||
}
|
||||
__asm__ volatile (
|
||||
"jsl gCallStub\n\t"
|
||||
"rep #0x30"
|
||||
::: "memory"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -466,10 +463,11 @@ void halAudioStopMod(void) {
|
|||
return;
|
||||
}
|
||||
buildCallStub(gNTPBase + 6, 0, 0, 0);
|
||||
asm {
|
||||
jsl gCallStub
|
||||
rep #0x30
|
||||
}
|
||||
__asm__ volatile (
|
||||
"jsl gCallStub\n\t"
|
||||
"rep #0x30"
|
||||
::: "memory"
|
||||
);
|
||||
gNTPPlaying = false;
|
||||
}
|
||||
|
||||
|
|
@ -487,17 +485,13 @@ void halAudioStopSfx(uint8_t slot) {
|
|||
// NTP has no SFX-stop entry point. We poke DOC oscillator control
|
||||
// registers directly to halt the slot's osc pair. SEI/CLI keeps
|
||||
// NTP's DOC IRQ from racing our sound_address writes.
|
||||
asm {
|
||||
sei
|
||||
}
|
||||
__asm__ volatile ("sei" ::: "memory");
|
||||
*DOC_SOUND_CONTROL = *DOC_IRQ_VOLUME;
|
||||
for (i = 0; i < SFX_OSCS_PER_SLOT; i++) {
|
||||
*DOC_SOUND_ADDRESS = (uint8_t)(DOC_OSC_CTRL_BASE + firstOsc + i);
|
||||
*DOC_SOUND_DATA = DOC_OSC_HALT_BITS;
|
||||
}
|
||||
asm {
|
||||
cli
|
||||
}
|
||||
__asm__ volatile ("cli" ::: "memory");
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,20 +7,19 @@
|
|||
// $9E00 - $9FFF 16 palettes x 16 colors x 2 bytes, $0RGB
|
||||
//
|
||||
// NEWVIDEO register at $00C029 controls SHR enable. Bit 7 turns SHR on.
|
||||
// ORCA/C must be built with 32-bit pointer mode (-w or equivalent) so
|
||||
// that the long addresses resolve to bank $E1.
|
||||
// Long (24-bit) addresses are used so they resolve to bank $E1.
|
||||
//
|
||||
// DIRTY-WALK + PEI-SLAM PRESENT
|
||||
// -----------------------------
|
||||
// halPresent walks the per-row dirty bands maintained by drawing
|
||||
// primitives in src/core/*.c. Fully-dirty rows go through the PEI
|
||||
// slam in src/port/iigs/peislam.asm (~530 cyc/row, ~55% faster than
|
||||
// memcpy/MVN); partial-dirty rows use memcpy, which ORCA-C lowers
|
||||
// slam in src/port/iigs/peislam.s (~530 cyc/row, ~55% faster than
|
||||
// memcpy/MVN); partial-dirty rows use memcpy, which lowers
|
||||
// to MVN (7 cyc/byte) -- the fastest 65816 way to move bytes into
|
||||
// bank $E1 when the dirty band is too narrow to amortize the slam's
|
||||
// per-call AUXWRITE/RAMRD/shadow toggle.
|
||||
//
|
||||
// peislam.asm declares its load segment as DRAWPRIMS so the linker
|
||||
// peislam.s declares its load segment as DRAWPRIMS so the linker
|
||||
// places it in its own bank, separate from AUDIO's _ROOT (where
|
||||
// audio_full.c + Memory Manager + stdio + NTPstreamsound already
|
||||
// crowd up against the 64 KB-per-bank limit).
|
||||
|
|
@ -33,7 +32,7 @@
|
|||
#include "hal.h"
|
||||
#include "surfaceInternal.h"
|
||||
|
||||
/* GetTick wrapper in peislam.asm: invokes the Misc Toolset GetTick
|
||||
/* GetTick wrapper in peislam.s: invokes the Misc Toolset GetTick
|
||||
* ($2503) and returns the low 16 bits of the system's tick counter
|
||||
* (firmware VBL ISR-driven). Polling $C019 from C user code missed
|
||||
* transitions for any op over ~1 ms; the system's tick counter is
|
||||
|
|
@ -48,10 +47,9 @@ static uint16_t gFrameHz = 60u;
|
|||
// hal.c is the single TU that calls into joeyDraw.asm. Cross-
|
||||
// platform draw.c / tile.c / etc. dispatch through halFast*
|
||||
// functions defined here; they never reference the asm symbols
|
||||
// directly. This avoids the cumulative ORCA-Linker-Expression-
|
||||
// too-complex-in-13/SysLib failure that hit when each cross-
|
||||
// platform TU brought its own asm extern.
|
||||
JOEYLIB_SEGMENT("DRAWPRIMS")
|
||||
// directly. Keeping the asm externs in a single TU avoids the
|
||||
// link-time fragility seen when each cross-platform TU brought its
|
||||
// own asm extern.
|
||||
|
||||
// 32 KB stack-slam fill via AUXWRITE. ~25 ms full-screen.
|
||||
extern void iigsSurfaceClearInner(uint8_t *pixels, uint16_t fillWord);
|
||||
|
|
@ -81,9 +79,10 @@ extern void iigsDrawLineInner(uint8_t *pixels, uint16_t x0, uint16_t y0, uint16_
|
|||
extern void iigsDrawCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy, uint16_t r, uint16_t nibble);
|
||||
// Stage-to-SHR full upload: pixels (MVN $01->$E1), SCB, palette.
|
||||
// Asm uses post-MVN DBR=$E1 to do sta abs,Y for SCB/palette.
|
||||
// Replaces ORCA-C's memcpy path which silently fails when called
|
||||
// from halPresent (DBR-state quirk after prior asm primitives).
|
||||
extern void iigsBlitStageToShr(uint8_t *scbPtr, uint16_t *palettePtr);
|
||||
// The asm performs the upload directly rather than a C-side memcpy,
|
||||
// which is unreliable when called from halPresent (DBR-state quirk
|
||||
// after prior asm primitives).
|
||||
extern void iigsBlitStageToShr(uint8_t *scbPtr, uint16_t *palettePtr, uint16_t uploadFlags);
|
||||
// jlFloodFill walk results: written by iigsFloodWalkAndScansInner,
|
||||
// read back by halFastFloodWalkAndScans.
|
||||
extern uint16_t gFloodSeedMatch;
|
||||
|
|
@ -102,7 +101,7 @@ extern void iigsInitRowLut(void);
|
|||
// screen presents (halPresentRect). srcOffset is the byte offset
|
||||
// within bank $01 of the FIRST byte to copy on the FIRST row;
|
||||
// subsequent rows are at srcOffset + 160, etc. ~9 cyc/byte vs
|
||||
// ORCA-C memcpy's ~30 cyc/byte.
|
||||
// C memcpy's ~30 cyc/byte.
|
||||
extern void iigsBlitRectStageToShr(uint16_t srcOffset, uint16_t copyBytes, uint16_t rowsLeft);
|
||||
// PEI-slam variant of the per-row rect blit. ~3 cyc/byte vs MVN's
|
||||
// ~9 cyc/byte. Constraints: copyBytes must be even and 2..80
|
||||
|
|
@ -165,7 +164,7 @@ static bool gModeSet = false;
|
|||
|
||||
// SCB / palette upload skipping is now driven by gStageScbDirty /
|
||||
// gStagePaletteDirty (core/surface.c). The old per-frame memcmp-
|
||||
// against-cached-copy approach was costing ~7 ms / frame on ORCA-C.
|
||||
// against-cached-copy approach was costing ~7 ms / frame.
|
||||
|
||||
// PEI slam scratch. File-scope non-static so the asm can `ext` them;
|
||||
// all accesses inside the slam use long-mode `>` addressing so they
|
||||
|
|
@ -175,8 +174,10 @@ volatile uint8_t gPeiOrigShadow;
|
|||
volatile uint16_t gPeiTempRowBase;
|
||||
volatile uint16_t gPeiCurRow; // row counter saved across slam (stack is hijacked)
|
||||
volatile uint16_t gPeiChunkRow; // in-chunk row counter saved across slam (Y reg storage)
|
||||
volatile uint16_t gPeiMvnSrc; // narrow-extent present: MVN source offset
|
||||
volatile uint16_t gPeiMvnCount; // narrow-extent present: MVN byte count
|
||||
|
||||
// peislam.asm's per-row peiSlamFullRow helper is no longer wired in;
|
||||
// peislam.s's per-row peiSlamFullRow helper is no longer wired in;
|
||||
// the present pipeline now does its own PEI-slam loop inside
|
||||
// iigsBlitStageToShr above (with dirty-row skip).
|
||||
|
||||
|
|
@ -184,7 +185,7 @@ volatile uint16_t gPeiChunkRow; // in-chunk row counter saved across sl
|
|||
|
||||
// Upload SCB / palette into bank-$E1 SHR memory only when the
|
||||
// matching dirty flag is set. Replaces a per-frame 712-byte memcmp
|
||||
// pair (~7 ms / frame on ORCA-C with -b) with a 2-cyc flag check.
|
||||
// pair (~7 ms / frame) with a 2-cyc flag check.
|
||||
// gStageScbDirty / gStagePaletteDirty live in core/surface.c; they
|
||||
// start true (forces the very first present to upload), get set true
|
||||
// again whenever jlScbSet* / jlPaletteSet mutate the stage's data, and
|
||||
|
|
@ -231,16 +232,22 @@ const char *halLastError(void) {
|
|||
|
||||
|
||||
void halPresent(const jlSurfaceT *src) {
|
||||
uint16_t uploadFlags;
|
||||
|
||||
if (src == NULL) {
|
||||
return;
|
||||
}
|
||||
// iigsBlitStageToShr does pixels (MVN $01->$E1) + SCB + palette
|
||||
// upload entirely in asm via DBR=$E1 + sta abs,Y indexed stores.
|
||||
// ORCA-C's C-side memcpy to bank $E1 has been unreliable from
|
||||
// C-side memcpy to bank $E1 has been unreliable from
|
||||
// halPresent's calling context, so we route everything through
|
||||
// the asm path. Future: re-introduce per-row dirty-band logic
|
||||
// for partial-screen updates (currently we always blit 32K).
|
||||
iigsBlitStageToShr(src->scb, &src->palette[0][0]);
|
||||
// the asm path. uploadFlags gates the optional SCB / palette
|
||||
// uploads on the dirty bits so we don't re-push 712 unchanged
|
||||
// bytes every frame: bit 0 = SCB, bit 1 = palette (pixels always).
|
||||
uploadFlags = (uint16_t)gStageScbDirty | ((uint16_t)gStagePaletteDirty << 1);
|
||||
iigsBlitStageToShr(src->scb, &src->palette[0][0], uploadFlags);
|
||||
gStagePaletteDirty = false;
|
||||
gStageScbDirty = false;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -266,7 +273,7 @@ void halShutdown(void) {
|
|||
|
||||
// halFastFillRect: macro-dispatched in core/hal.h, same as the other
|
||||
// halFast* primitives. The C wrapper that used to live here was kept
|
||||
// as load-bearing _ROOT mass to defeat ORCA-Linker bank fragility;
|
||||
// as load-bearing _ROOT mass to defeat linker bank fragility;
|
||||
// since the CORESYS migration drained _ROOT, the macro form is safe.
|
||||
|
||||
|
||||
|
|
@ -298,50 +305,9 @@ void halSurfaceFreePortData(jlSurfaceT *s, bool isStage, void *portData) {
|
|||
}
|
||||
|
||||
|
||||
// IIgs SHR is chunky-native; no bitplanes to update.
|
||||
void halFillRectPlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex) {
|
||||
(void)s;
|
||||
(void)x;
|
||||
(void)y;
|
||||
(void)w;
|
||||
(void)h;
|
||||
(void)colorIndex;
|
||||
}
|
||||
|
||||
|
||||
void halSurfaceCopyPlanes(jlSurfaceT *dst, const jlSurfaceT *src) {
|
||||
(void)dst;
|
||||
(void)src;
|
||||
}
|
||||
|
||||
|
||||
void halTileFillPlanes(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) {
|
||||
(void)s; (void)bx; (void)by; (void)colorIndex;
|
||||
}
|
||||
void halTileCopyPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy) {
|
||||
(void)dst; (void)dstBx; (void)dstBy; (void)src; (void)srcBx; (void)srcBy;
|
||||
}
|
||||
void halTileCopyMaskedPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparentIndex) {
|
||||
(void)dst; (void)dstBx; (void)dstBy; (void)src; (void)srcBx; (void)srcBy; (void)transparentIndex;
|
||||
}
|
||||
void halTilePastePlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *chunkyTile) {
|
||||
(void)dst; (void)bx; (void)by; (void)chunkyTile;
|
||||
}
|
||||
void halTilePasteMonoPlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *monoTile, uint8_t fgColor, uint8_t bgColor) {
|
||||
(void)dst; (void)bx; (void)by; (void)monoTile; (void)fgColor; (void)bgColor;
|
||||
}
|
||||
void halTileSnapPlanes(const jlSurfaceT *src, uint8_t bx, uint8_t by, uint8_t *chunkyTileOut) {
|
||||
(void)src; (void)bx; (void)by; (void)chunkyTileOut;
|
||||
}
|
||||
void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t y) {
|
||||
(void)s; (void)sp; (void)x; (void)y;
|
||||
}
|
||||
void halSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dstPlaneBytes) {
|
||||
(void)s; (void)x; (void)y; (void)w; (void)h; (void)dstPlaneBytes;
|
||||
}
|
||||
void halSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, const uint8_t *srcPlaneBytes) {
|
||||
(void)s; (void)x; (void)y; (void)w; (void)h; (void)srcPlaneBytes;
|
||||
}
|
||||
// IIgs is chunky-native: the hal*Planes hooks are compiled out by
|
||||
// no-op macros in core/hal.h (gated on JOEYLIB_PLATFORM_IIGS), so no
|
||||
// stub functions are defined here.
|
||||
|
||||
|
||||
/* Phase 9 chunky reader hooks: IIgs reads from s->pixels just like
|
||||
|
|
@ -436,7 +402,7 @@ void halWaitVBL(void) {
|
|||
// context where ToolBox interrupt setup would be intrusive.
|
||||
//
|
||||
// gFrameCount uses an explicit lda+adc+sta read-modify-write rather
|
||||
// than `gFrameCount++` because ORCA-C lowers the post-increment to
|
||||
// than `gFrameCount++` because the post-increment can lower to
|
||||
// `inc |gFrameCount` (the only INC abs form on 65816 -- there is no
|
||||
// INC long-abs). With this file in the DRAWPRIMS load segment but
|
||||
// halFrameCount called from CORESYS via JSL, DBR isn't pointing at
|
||||
|
|
@ -454,8 +420,7 @@ uint16_t halFrameHz(void) {
|
|||
}
|
||||
|
||||
|
||||
// IIgs: frame-counter approximation. ORCA-C's time() bursts the
|
||||
// stdio cluster bank limit (see feedback_orca_c_time_segment), so
|
||||
// IIgs: frame-counter approximation. time() is avoided here, so
|
||||
// we approximate wall-clock ms from the 16-bit frame counter at
|
||||
// gFrameHz (60 Hz). The 16-bit counter wraps every ~1090 sec, so
|
||||
// callers tracking deltas should use this for short intervals only
|
||||
|
|
|
|||
|
|
@ -36,8 +36,6 @@
|
|||
|
||||
#include <string.h>
|
||||
|
||||
#include <types.h>
|
||||
|
||||
#include "hal.h"
|
||||
#include "inputInternal.h"
|
||||
#include "joey/surface.h"
|
||||
|
|
@ -118,9 +116,8 @@ static int8_t thresholdPaddle(uint8_t v);
|
|||
|
||||
// ----- Module state -----
|
||||
|
||||
// ASCII -> jlKeyE, filled once at halInputInit. ORCA/C is C89 and
|
||||
// does not accept designated initializers; runtime fill keeps lookup
|
||||
// O(1) instead of a 40-plus-case switch.
|
||||
// ASCII -> jlKeyE, filled once at halInputInit. Runtime fill keeps
|
||||
// lookup O(1) instead of a 40-plus-case switch.
|
||||
static uint8_t gAsciiToKey[ASCII_TABLE_SIZE];
|
||||
|
||||
static int16_t gMouseAbsX = SURFACE_WIDTH / 2;
|
||||
|
|
@ -230,9 +227,7 @@ static int8_t thresholdPaddle(uint8_t v) {
|
|||
// Each paddle read triggers an RC scan via $C070 and then polls the
|
||||
// paddle softswitch until bit 7 clears; the iteration count
|
||||
// approximates the paddle's 0..255 position (the Apple firmware
|
||||
// PREAD routine works the same way). The two reads are inlined here
|
||||
// rather than factored into a helper because ORCA/C 2.1 trips over
|
||||
// Auto-disconnect tracking. The paddle one-shot timer takes ~3 ms to
|
||||
// PREAD routine works the same way). The paddle one-shot timer takes ~3 ms to
|
||||
// charge at full deflection; if NO joystick is wired up, the BUSY bit
|
||||
// stays set forever and the busy-wait runs the full PADDLE_TIMEOUT
|
||||
// every frame -- ~3 ms wasted per frame on a stick that isn't there.
|
||||
|
|
@ -251,10 +246,8 @@ static bool gJoyDisconnectLatched = false;
|
|||
// to the digital threshold mapping. gJoyRecalibrate is set by
|
||||
// halJoystickReset and cleared on the next successful poll, which
|
||||
// captures the new center.
|
||||
// uint8_t (not bool) so the per-element stride is 1 byte. ORCA-C's
|
||||
// _Bool is 2 bytes, which forces a ~MUL4 helper for every index
|
||||
// multiply -- even when the index is a constant the compiler doesn't
|
||||
// fold. Storage is still 0 or 1 either way.
|
||||
// uint8_t (not bool) so the per-element stride is a known 1 byte.
|
||||
// Storage is still 0 or 1 either way.
|
||||
static uint8_t gJoyCenterX [JOYSTICK_COUNT];
|
||||
static uint8_t gJoyCenterY [JOYSTICK_COUNT];
|
||||
static uint8_t gJoyCenterValid [JOYSTICK_COUNT];
|
||||
|
|
@ -290,10 +283,8 @@ static void pollJoystick(void) {
|
|||
bool yResolved;
|
||||
|
||||
// Buttons are I/O reads -- always cheap, do them every frame.
|
||||
// ORCA-C 2.2.1 doesn't constant-fold the row-stride multiply for
|
||||
// 2D arrays even when both indices are constants, so each
|
||||
// gJoyButtonState[i][j] write emits a ~MUL4 helper. Indexing
|
||||
// through a (uint8_t *) cast collapses to a literal byte offset.
|
||||
// Indexing through a (uint8_t *) cast collapses each
|
||||
// gJoyButtonState[i][j] write to a literal byte offset.
|
||||
((uint8_t *)gJoyButtonState)[JOYSTICK_0 * JOY_BUTTON_COUNT + JOY_BUTTON_0]
|
||||
= (*IIGS_BTN0 & IIGS_BUTTON_BIT) != 0;
|
||||
((uint8_t *)gJoyButtonState)[JOYSTICK_0 * JOY_BUTTON_COUNT + JOY_BUTTON_1]
|
||||
|
|
@ -324,11 +315,11 @@ static void pollJoystick(void) {
|
|||
// stick. One resolves => stick is present, reset the counter.
|
||||
//
|
||||
// gJoyConsecutiveTimeouts uses a local-var read-modify-write rather
|
||||
// than `++`. ORCA-C lowers `++` to `inc abs` (the only INC abs form
|
||||
// on 65816), which depends on DBR pointing at this static's bank.
|
||||
// Cross-segment JSL doesn't update DBR, so a caller in a different
|
||||
// load segment would silently mutate the wrong byte. Long-mode
|
||||
// lda+sta is DBR-independent.
|
||||
// than `++` so the update is DBR-independent: an `inc abs` on the
|
||||
// static would depend on DBR pointing at this static's bank, and a
|
||||
// cross-segment JSL doesn't update DBR, so a caller in a different
|
||||
// load segment could silently mutate the wrong byte. Long-mode
|
||||
// lda+sta avoids that.
|
||||
if (!xResolved && !yResolved) {
|
||||
uint16_t timeouts;
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
4248
src/port/iigs/joeyDraw.s
Normal file
4248
src/port/iigs/joeyDraw.s
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,66 +0,0 @@
|
|||
* peislam.asm - originally a PEI-slam helper, now hosts the GetTick
|
||||
* and ReadBParam trampolines. The PEI-slam logic was rolled into
|
||||
* iigsBlitStageToShr in joeyDraw.asm.
|
||||
|
||||
keep PEISLAM
|
||||
case on
|
||||
|
||||
|
||||
* Stub kept so the PEISLAM load segment stays present (the build's
|
||||
* PORT_ASM_SRCS_ALL wildcard pulls in this file by name).
|
||||
peislamStub start IIGSASM
|
||||
rtl
|
||||
end
|
||||
|
||||
|
||||
****************************************************************
|
||||
* uint16_t iigsGetTickWord(void)
|
||||
*
|
||||
* Calls Misc Toolset GetTick ($2503) and returns the low 16 bits of
|
||||
* the 32-bit tick counter. The system increments this counter from
|
||||
* the actual VBL hardware interrupt, so it stays accurate regardless
|
||||
* of caller polling rate -- C-side polling of $C019 missed transitions
|
||||
* for any op over ~1 ms.
|
||||
*
|
||||
* GetTick output convention: caller pushes 4 bytes of output space,
|
||||
* tool dispatcher writes the LongWord into them. We pull the low 16
|
||||
* bits into A (ORCA-C Word return convention -- A holds the result,
|
||||
* not Y; verified against jIIgs.asm asmGetVbl) and discard the high
|
||||
* 16 into X.
|
||||
*
|
||||
* ORCA-C cdecl ABI: caller has M=I=16. Word return in A.
|
||||
****************************************************************
|
||||
|
||||
iigsGetTickWord start IIGSASM
|
||||
pha ; output space high word
|
||||
pha ; output space low word
|
||||
ldx #$2503 ; _GetTick
|
||||
jsl $E10000
|
||||
|
||||
pla ; A = low 16 bits (return value)
|
||||
plx ; discard high 16 bits
|
||||
rtl
|
||||
end
|
||||
|
||||
|
||||
****************************************************************
|
||||
* uint16_t iigsReadHzParam(void)
|
||||
*
|
||||
* Reads battery RAM parameter hrtz50or60 ($1D) via _ReadBParam ($0C03)
|
||||
* and returns the raw value: 0 = NTSC (60 Hz), 1 = PAL (50 Hz).
|
||||
*
|
||||
* GetTick fires from the hardware VBL ISR, so its rate matches the
|
||||
* video field rate -- 60 Hz on NTSC, 50 Hz on PAL. halFrameHz must
|
||||
* report whichever this machine actually runs so wall-clock math
|
||||
* (frames * 1000 / halFrameHz) is correct on both.
|
||||
****************************************************************
|
||||
|
||||
iigsReadHzParam start IIGSASM
|
||||
pha ; output space (Word)
|
||||
pea $001D ; hrtz50or60 parameter ID
|
||||
ldx #$0C03 ; _ReadBParam
|
||||
jsl $E10000
|
||||
|
||||
pla ; A = result (ORCA-C Word return)
|
||||
rtl
|
||||
end
|
||||
56
src/port/iigs/peislam.s
Normal file
56
src/port/iigs/peislam.s
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
; peislam.s - GetTick and ReadBParam trampolines for the IIgs.
|
||||
;
|
||||
; GAS syntax for the llvm-mos w65816 toolchain. The old PEI-slam present
|
||||
; logic was rolled into iigsBlitStageToShr in joeyDraw; only the two
|
||||
; toolbox trampolines remain.
|
||||
;
|
||||
; C ABI (llvm-mos w65816): caller has M=I=16. A uint16_t return value
|
||||
; is left in A, which matches the toolbox dispatcher's word result, so
|
||||
; these wrappers need no register shuffling beyond the pop.
|
||||
|
||||
.text
|
||||
|
||||
; ----------------------------------------------------------------
|
||||
; uint16_t iigsGetTickWord(void)
|
||||
;
|
||||
; Calls Misc Toolset GetTick ($2503) and returns the low 16 bits of
|
||||
; the 32-bit tick counter. The system increments this counter from the
|
||||
; actual VBL hardware interrupt, so it stays accurate regardless of
|
||||
; caller polling rate -- C-side polling of $C019 missed transitions
|
||||
; for any op over ~1 ms.
|
||||
;
|
||||
; GetTick output convention: caller pushes 4 bytes of output space, the
|
||||
; tool dispatcher writes the LongWord into them. We pull the low 16
|
||||
; bits into A (the return value) and discard the high 16 into X.
|
||||
; ----------------------------------------------------------------
|
||||
.section .text.iigsGetTickWord,"ax"
|
||||
.globl iigsGetTickWord
|
||||
iigsGetTickWord:
|
||||
pha ; output space high word
|
||||
pha ; output space low word
|
||||
ldx #0x2503 ; _GetTick
|
||||
jsl 0xe10000
|
||||
pla ; A = low 16 bits (return value)
|
||||
plx ; discard high 16 bits
|
||||
rtl
|
||||
|
||||
; ----------------------------------------------------------------
|
||||
; uint16_t iigsReadHzParam(void)
|
||||
;
|
||||
; Reads battery RAM parameter hrtz50or60 ($1D) via _ReadBParam ($0C03)
|
||||
; and returns the raw value: 0 = NTSC (60 Hz), 1 = PAL (50 Hz).
|
||||
;
|
||||
; GetTick fires from the hardware VBL ISR, so its rate matches the
|
||||
; video field rate -- 60 Hz on NTSC, 50 Hz on PAL. halFrameHz must
|
||||
; report whichever this machine actually runs so wall-clock math
|
||||
; (frames * 1000 / halFrameHz) is correct on both.
|
||||
; ----------------------------------------------------------------
|
||||
.section .text.iigsReadHzParam,"ax"
|
||||
.globl iigsReadHzParam
|
||||
iigsReadHzParam:
|
||||
pha ; output space (Word)
|
||||
pea 0x001d ; hrtz50or60 parameter ID
|
||||
ldx #0x0c03 ; _ReadBParam
|
||||
jsl 0xe10000
|
||||
pla ; A = result (return value)
|
||||
rtl
|
||||
|
|
@ -1,201 +0,0 @@
|
|||
// Shared 68k C-side fast paths for drawing primitives.
|
||||
//
|
||||
// Header-only `static inline` so both Amiga and Atari ST hal.c can
|
||||
// pull the same code without makefile changes -- the surface buffer
|
||||
// layout is identical between the two ports (4bpp packed, 160 bytes
|
||||
// per row, 32000 bytes per surface), so the per-pixel write logic
|
||||
// is portable.
|
||||
//
|
||||
// All functions assume the caller has already verified the rect /
|
||||
// circle bounding box is fully on-surface (which is what the
|
||||
// halFast* contract guarantees). No clip checks inside the hot
|
||||
// loops, no per-pixel surfaceMarkDirtyRect calls -- the caller marks
|
||||
// the bounding box dirty once after the call.
|
||||
//
|
||||
// gcc-amigaos / gcc-atari-mint both inline these aggressively at -O2,
|
||||
// so the wrapper functions in each port's hal.c compile to a single
|
||||
// tight m68k loop with no JSR per pixel.
|
||||
|
||||
#ifndef JOEYLIB_DRAW68K_INLINE_H
|
||||
#define JOEYLIB_DRAW68K_INLINE_H
|
||||
|
||||
#include "joey/types.h"
|
||||
#include "joey/surface.h"
|
||||
|
||||
|
||||
// Plot a single pixel at (x, y) without bounds checking. nibLo is
|
||||
// the colorIndex's low nibble, nibHi is (low << 4) precomputed by
|
||||
// the caller so the hot loop doesn't do the shift per call.
|
||||
static inline void draw68kPlotPixel(uint8_t *pixels, int16_t x, int16_t y,
|
||||
uint8_t nibLo, uint8_t nibHi) {
|
||||
uint8_t *byte = &pixels[(uint16_t)y * (uint16_t)SURFACE_BYTES_PER_ROW
|
||||
+ ((uint16_t)x >> 1)];
|
||||
if (x & 1) {
|
||||
*byte = (uint8_t)((*byte & 0xF0u) | nibLo);
|
||||
} else {
|
||||
*byte = (uint8_t)((*byte & 0x0Fu) | nibHi);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Bresenham midpoint-circle outline. Plots all 8 octants per
|
||||
// iteration. Caller must verify cx +/- r and cy +/- r are all in
|
||||
// surface bounds (the halFastDrawCircle contract).
|
||||
static inline void draw68kCircleOutline(uint8_t *pixels, int16_t cx, int16_t cy,
|
||||
uint16_t r, uint8_t colorIndex) {
|
||||
int16_t x;
|
||||
int16_t y;
|
||||
int16_t err;
|
||||
uint8_t nibLo = (uint8_t)(colorIndex & 0x0Fu);
|
||||
uint8_t nibHi = (uint8_t)(nibLo << 4);
|
||||
|
||||
if (r == 0u) {
|
||||
draw68kPlotPixel(pixels, cx, cy, nibLo, nibHi);
|
||||
return;
|
||||
}
|
||||
|
||||
x = (int16_t)r;
|
||||
y = 0;
|
||||
err = (int16_t)(1 - x);
|
||||
while (x >= y) {
|
||||
draw68kPlotPixel(pixels, (int16_t)(cx + x), (int16_t)(cy + y), nibLo, nibHi);
|
||||
draw68kPlotPixel(pixels, (int16_t)(cx - x), (int16_t)(cy + y), nibLo, nibHi);
|
||||
draw68kPlotPixel(pixels, (int16_t)(cx + x), (int16_t)(cy - y), nibLo, nibHi);
|
||||
draw68kPlotPixel(pixels, (int16_t)(cx - x), (int16_t)(cy - y), nibLo, nibHi);
|
||||
draw68kPlotPixel(pixels, (int16_t)(cx + y), (int16_t)(cy + x), nibLo, nibHi);
|
||||
draw68kPlotPixel(pixels, (int16_t)(cx - y), (int16_t)(cy + x), nibLo, nibHi);
|
||||
draw68kPlotPixel(pixels, (int16_t)(cx + y), (int16_t)(cy - x), nibLo, nibHi);
|
||||
draw68kPlotPixel(pixels, (int16_t)(cx - y), (int16_t)(cy - x), nibLo, nibHi);
|
||||
y++;
|
||||
if (err <= 0) {
|
||||
err = (int16_t)(err + y + y + 1);
|
||||
} else {
|
||||
x--;
|
||||
err = (int16_t)(err + y + y - x - x + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Filled circle: for each y from 0..r, find the largest x with
|
||||
// x*x + y*y <= r*r and emit the symmetric horizontal span. Caller
|
||||
// must verify the bounding box is on-surface.
|
||||
//
|
||||
// Each span is a horizontal run on a single row, so we go straight
|
||||
// to the byte-fill path used by halFastFillRect-style code rather
|
||||
// than calling per-pixel helpers.
|
||||
static inline void draw68kCircleFill(uint8_t *pixels, int16_t cx, int16_t cy,
|
||||
uint16_t r, uint8_t colorIndex) {
|
||||
int16_t y;
|
||||
int16_t x;
|
||||
uint16_t xx;
|
||||
uint16_t yy;
|
||||
uint16_t r2;
|
||||
uint8_t nibLo = (uint8_t)(colorIndex & 0x0Fu);
|
||||
uint8_t nibHi = (uint8_t)(nibLo << 4);
|
||||
uint8_t doubled = (uint8_t)(nibHi | nibLo);
|
||||
|
||||
if (r == 0u) {
|
||||
draw68kPlotPixel(pixels, cx, cy, nibLo, nibHi);
|
||||
return;
|
||||
}
|
||||
|
||||
xx = (uint16_t)(r * r);
|
||||
r2 = xx;
|
||||
yy = 0;
|
||||
x = (int16_t)r;
|
||||
|
||||
for (y = 0; y <= (int16_t)r; y++) {
|
||||
int16_t spanLeft;
|
||||
int16_t spanRight;
|
||||
int16_t rowsRemaining;
|
||||
int16_t rowYDelta;
|
||||
|
||||
while (xx + yy > r2) {
|
||||
xx = (uint16_t)(xx - (uint16_t)((uint16_t)x + (uint16_t)x - 1u));
|
||||
x--;
|
||||
}
|
||||
|
||||
spanLeft = (int16_t)(cx - x);
|
||||
spanRight = (int16_t)(cx + x);
|
||||
rowsRemaining = (y == 0) ? 1 : 2;
|
||||
for (rowYDelta = 0; rowYDelta < rowsRemaining; rowYDelta++) {
|
||||
int16_t rowY = (rowYDelta == 0) ? (int16_t)(cy + y) : (int16_t)(cy - y);
|
||||
uint8_t *rowBase = &pixels[(uint16_t)rowY * (uint16_t)SURFACE_BYTES_PER_ROW];
|
||||
int16_t px = spanLeft;
|
||||
|
||||
/* Leading partial nibble. */
|
||||
if (px & 1) {
|
||||
uint8_t *byte = &rowBase[(uint16_t)px >> 1];
|
||||
*byte = (uint8_t)((*byte & 0xF0u) | nibLo);
|
||||
px++;
|
||||
}
|
||||
/* Middle whole bytes. */
|
||||
{
|
||||
int16_t midBytes = (int16_t)((spanRight + 1 - px) >> 1);
|
||||
if (midBytes > 0) {
|
||||
uint8_t *p = &rowBase[(uint16_t)px >> 1];
|
||||
int16_t i;
|
||||
for (i = 0; i < midBytes; i++) {
|
||||
p[i] = doubled;
|
||||
}
|
||||
px = (int16_t)(px + (midBytes << 1));
|
||||
}
|
||||
}
|
||||
/* Trailing partial nibble. */
|
||||
if (px <= spanRight) {
|
||||
uint8_t *byte = &rowBase[(uint16_t)px >> 1];
|
||||
*byte = (uint8_t)((*byte & 0x0Fu) | nibHi);
|
||||
}
|
||||
}
|
||||
|
||||
yy = (uint16_t)(yy + (uint16_t)((uint16_t)y + (uint16_t)y + 1u));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Bresenham line plot. Caller must verify both endpoints are on
|
||||
// surface (halFastDrawLine contract). Falls through to a tight
|
||||
// inner loop with no per-pixel function calls.
|
||||
static inline void draw68kLine(uint8_t *pixels, int16_t x0, int16_t y0,
|
||||
int16_t x1, int16_t y1, uint8_t colorIndex) {
|
||||
int16_t dx;
|
||||
int16_t dy;
|
||||
int16_t sx;
|
||||
int16_t sy;
|
||||
int16_t err;
|
||||
int16_t e2;
|
||||
uint8_t nibLo = (uint8_t)(colorIndex & 0x0Fu);
|
||||
uint8_t nibHi = (uint8_t)(nibLo << 4);
|
||||
|
||||
dx = (int16_t)(x1 - x0);
|
||||
if (dx < 0) {
|
||||
dx = (int16_t)(-dx);
|
||||
}
|
||||
dy = (int16_t)(y1 - y0);
|
||||
if (dy < 0) {
|
||||
dy = (int16_t)(-dy);
|
||||
}
|
||||
sx = (x0 < x1) ? 1 : -1;
|
||||
sy = (y0 < y1) ? 1 : -1;
|
||||
err = (int16_t)(dx - dy);
|
||||
|
||||
for (;;) {
|
||||
draw68kPlotPixel(pixels, x0, y0, nibLo, nibHi);
|
||||
if (x0 == x1 && y0 == y1) {
|
||||
break;
|
||||
}
|
||||
e2 = (int16_t)(err + err);
|
||||
if (e2 > -dy) {
|
||||
err = (int16_t)(err - dy);
|
||||
x0 = (int16_t)(x0 + sx);
|
||||
}
|
||||
if (e2 < dx) {
|
||||
err = (int16_t)(err + dx);
|
||||
y0 = (int16_t)(y0 + sy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endif /* JOEYLIB_DRAW68K_INLINE_H */
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
| Shared 68000 fast paths for the chunky surface buffer.
|
||||
| Shared 68000 fast path for the planar surface buffer.
|
||||
|
|
||||
| Both Amiga and Atari ST keep the JoeyLib stage / surfaces in the
|
||||
| same 4bpp packed (2 px / byte, 160 bytes / row, 32000 bytes / surface)
|
||||
| format -- the c2p / blit-to-screen step that converts to native
|
||||
| pixel layout happens only at present time. So the per-byte buffer
|
||||
| operations (fill, clear) can be shared across both ports.
|
||||
| Holds surface68kFillSpan4Planes -- the per-row body of the 4-plane
|
||||
| planar rect/circle fill. Only the Amiga port calls it (ST is
|
||||
| word-interleaved and uses its own St* routines); it lives here
|
||||
| because the file is globbed into both ports' builds. The earlier
|
||||
| chunky clear/fill primitives that this file once also held were
|
||||
| removed once both ports went native-planar.
|
||||
|
|
||||
| Calling convention: cdecl (gcc-amigaos / gcc-atari-mint).
|
||||
| d0/d1/a0/a1 are caller-save (scratch).
|
||||
|
|
@ -16,245 +17,6 @@
|
|||
.text
|
||||
|
||||
|
||||
| ----------------------------------------------------------------
|
||||
| void surface68kClearLong(uint8_t *pixels, uint16_t fillByte);
|
||||
|
|
||||
| Fill 32000 bytes (SURFACE_PIXELS_SIZE) starting at `pixels` with
|
||||
| `fillByte`. Unrolled move.l with the byte expanded to a 4-byte
|
||||
| word so we can write 32 bytes per loop body instead of 1.
|
||||
| ----------------------------------------------------------------
|
||||
.globl _surface68kClearLong
|
||||
|
||||
.equ SURF_CLEAR_BYTES, 32000
|
||||
.equ SURF_CLEAR_LONGS, (SURF_CLEAR_BYTES / 4)
|
||||
.equ SURF_CLEAR_PER_ITER, 8
|
||||
.equ SURF_CLEAR_ITERS, (SURF_CLEAR_LONGS / SURF_CLEAR_PER_ITER)
|
||||
|
||||
| Save d2 (used as dbra counter). a0/a1/d0/d1 are
|
||||
| caller-save so we may clobber them freely. Stack
|
||||
| offset to args = 4 (saved d2) + 4 (return PC) = 8.
|
||||
.equ CLR_SAVED, 4
|
||||
|
||||
_surface68kClearLong:
|
||||
move.l %d2,-(%sp)
|
||||
|
||||
move.l 4+CLR_SAVED(%sp),%a0 | pixels
|
||||
| Zero d0 BEFORE the move.b -- m68k move.b only touches
|
||||
| the low byte of the destination, so d0[31..8] would
|
||||
| otherwise hold whatever garbage the caller left in d0
|
||||
| and pollute the OR-replicate chain below.
|
||||
moveq #0,%d0
|
||||
move.b 8+CLR_SAVED+3(%sp),%d0 | fillByte (low byte of int)
|
||||
|
||||
| Build a long with fillByte replicated four times.
|
||||
| d0 = $000000FB -> $0000FBFB -> $FBFBFBFB
|
||||
move.l %d0,%d1
|
||||
lsl.l #8,%d1
|
||||
or.l %d1,%d0 | d0 = $0000FBFB
|
||||
move.l %d0,%d1
|
||||
swap %d1 | d1 = $FBFB0000
|
||||
or.l %d1,%d0 | d0 = $FBFBFBFB
|
||||
|
||||
move.l %a0,%a1 | dest cursor
|
||||
move.w #(SURF_CLEAR_ITERS - 1),%d2 | dbra count
|
||||
|
||||
.LclearLoop:
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
dbra %d2,.LclearLoop
|
||||
|
||||
move.l (%sp)+,%d2
|
||||
rts
|
||||
|
||||
|
||||
| ----------------------------------------------------------------
|
||||
| void surface68kFillRectFull(uint8_t *pixels, int16_t y, uint16_t h,
|
||||
| uint16_t fillByte);
|
||||
|
|
||||
| Full-width (320 px = 160 byte) rectangle fill. `pixels` is the
|
||||
| surface base; the row at `y` is `pixels + y * 160`. Fills `h`
|
||||
| consecutive rows using move.l writes.
|
||||
| ----------------------------------------------------------------
|
||||
.globl _surface68kFillRectFull
|
||||
|
||||
.equ SURF_FRF_BYTES_PER_ROW, 160
|
||||
|
||||
| Save d2/d3 (used for fillByte replicate scratch and
|
||||
| row counter). 8 bytes saved + 4 ret PC = 12 to args.
|
||||
.equ FRF_SAVED, 8
|
||||
|
||||
_surface68kFillRectFull:
|
||||
movem.l %d2-%d3,-(%sp)
|
||||
|
||||
move.l 4+FRF_SAVED(%sp),%a0 | pixels base
|
||||
| Zero d0/d1 BEFORE move.b/move.w -- those only update
|
||||
| sub-register portions and would otherwise carry the
|
||||
| caller's garbage upper bits into the replicate OR.
|
||||
moveq #0,%d0
|
||||
moveq #0,%d1
|
||||
moveq #0,%d2
|
||||
move.w 8+FRF_SAVED+2(%sp),%d1 | y (low word)
|
||||
move.w 12+FRF_SAVED+2(%sp),%d2 | h (low word)
|
||||
move.b 16+FRF_SAVED+3(%sp),%d0 | fillByte
|
||||
|
||||
| Bail on degenerate height.
|
||||
tst.w %d2
|
||||
ble .Lfrf_done
|
||||
|
||||
| Build replicated fillByte in d0.
|
||||
move.l %d0,%d3
|
||||
lsl.l #8,%d3
|
||||
or.l %d3,%d0
|
||||
move.l %d0,%d3
|
||||
swap %d3
|
||||
or.l %d3,%d0 | d0 = byte * $01010101
|
||||
|
||||
| row pointer = pixels + y * 160 = pixels + y*128 + y*32
|
||||
ext.l %d1
|
||||
move.l %d1,%d3
|
||||
lsl.l #7,%d3 | y * 128
|
||||
lsl.l #5,%d1 | y * 32
|
||||
add.l %d3,%d1 | y * 160
|
||||
add.l %d1,%a0 | a0 = first row to fill
|
||||
|
||||
subq.w #1,%d2 | row dbra count
|
||||
|
||||
.Lfrf_rowLoop:
|
||||
move.l %a0,%a1
|
||||
| 40 longs per row, fully unrolled.
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
move.l %d0,(%a1)+
|
||||
lea SURF_FRF_BYTES_PER_ROW(%a0),%a0
|
||||
dbra %d2,.Lfrf_rowLoop
|
||||
|
||||
.Lfrf_done:
|
||||
movem.l (%sp)+,%d2-%d3
|
||||
rts
|
||||
|
||||
|
||||
| ----------------------------------------------------------------
|
||||
| void surface68kFillRectByteAligned(uint8_t *rowFirst, uint16_t midBytes,
|
||||
| uint16_t h, uint16_t fillByte);
|
||||
|
|
||||
| Per-row middle-bytes fill for a jlFillRect where the leading and
|
||||
| trailing nibble fixups have already been done (or none are
|
||||
| needed because x and w are both even). `rowFirst` points at the
|
||||
| first FULLY-INSIDE byte of the rect on row 0; subsequent rows
|
||||
| are at +160. `midBytes` is how many full bytes per row to fill.
|
||||
| ----------------------------------------------------------------
|
||||
.globl _surface68kFillRectByteAligned
|
||||
|
||||
.equ SURF_FRB_STRIDE, 160
|
||||
|
||||
| Save d2-d6 (5 regs = 20 bytes). Args at +20+4 = +24.
|
||||
.equ FRB_SAVED, 20
|
||||
|
||||
_surface68kFillRectByteAligned:
|
||||
movem.l %d2-%d6,-(%sp)
|
||||
|
||||
move.l 4+FRB_SAVED(%sp),%a0 | rowFirst
|
||||
| Zero scratch regs BEFORE the sub-register loads --
|
||||
| move.w / move.b only touch low portions, leaving
|
||||
| caller's garbage in the upper bits which would
|
||||
| otherwise pollute the replicate-OR chain below.
|
||||
moveq #0,%d0
|
||||
moveq #0,%d1
|
||||
moveq #0,%d2
|
||||
move.w 8+FRB_SAVED+2(%sp),%d1 | midBytes
|
||||
move.w 12+FRB_SAVED+2(%sp),%d2 | h
|
||||
move.b 16+FRB_SAVED+3(%sp),%d0 | fillByte
|
||||
|
||||
tst.w %d1
|
||||
beq .Lfrb_done
|
||||
tst.w %d2
|
||||
ble .Lfrb_done
|
||||
|
||||
| Build replicated fillByte in d0.
|
||||
move.l %d0,%d3
|
||||
lsl.l #8,%d3
|
||||
or.l %d3,%d0
|
||||
move.l %d0,%d3
|
||||
swap %d3
|
||||
or.l %d3,%d0
|
||||
|
||||
| Pre-compute long-count and trailing-byte residual.
|
||||
move.w %d1,%d4
|
||||
lsr.w #2,%d4 | longCount
|
||||
move.w %d1,%d5
|
||||
and.w #3,%d5 | trailBytes
|
||||
|
||||
subq.w #1,%d2 | row dbra count
|
||||
|
||||
.Lfrb_rowLoop:
|
||||
move.l %a0,%a1
|
||||
move.w %d4,%d6 | longs left
|
||||
tst.w %d6
|
||||
beq .Lfrb_tail
|
||||
subq.w #1,%d6
|
||||
.Lfrb_longLoop:
|
||||
move.l %d0,(%a1)+
|
||||
dbra %d6,.Lfrb_longLoop
|
||||
.Lfrb_tail:
|
||||
move.w %d5,%d6 | trailing bytes
|
||||
tst.w %d6
|
||||
beq .Lfrb_rowDone
|
||||
subq.w #1,%d6
|
||||
.Lfrb_byteLoop:
|
||||
move.b %d0,(%a1)+
|
||||
dbra %d6,.Lfrb_byteLoop
|
||||
.Lfrb_rowDone:
|
||||
lea SURF_FRB_STRIDE(%a0),%a0
|
||||
dbra %d2,.Lfrb_rowLoop
|
||||
|
||||
.Lfrb_done:
|
||||
movem.l (%sp)+,%d2-%d6
|
||||
rts
|
||||
|
||||
|
||||
| ----------------------------------------------------------------
|
||||
| void surface68kFillSpan4Planes(uint8_t *p0, uint8_t *p1,
|
||||
| uint8_t *p2, uint8_t *p3,
|
||||
|
|
@ -283,13 +45,15 @@ _surface68kFillRectByteAligned:
|
|||
| -- branchless: the same arithmetic produces "set" or "clear" based
|
||||
| on whether fbN is 0xFF or 0x00.
|
||||
|
|
||||
| ABI: m68k cdecl. d2-d7/a2-a6 callee-save (movem'd here).
|
||||
| Stack offset to first arg after MOVEM: 11 regs * 4 = 44 bytes saved
|
||||
| + 4 ret PC = 48.
|
||||
| ABI: m68k cdecl. d2-d7/a2-a6 callee-save. Only a2/a3 are actually
|
||||
| used by the body (planes 2/3); a4-a6 are not touched, so only
|
||||
| d2-d7/a2-a3 are movem'd here.
|
||||
| Stack offset to first arg after MOVEM: 8 regs * 4 = 32 bytes saved
|
||||
| + 4 ret PC = 36.
|
||||
| ----------------------------------------------------------------
|
||||
.globl _surface68kFillSpan4Planes
|
||||
|
||||
.equ SP_SAVED, 44
|
||||
.equ SP_SAVED, 32
|
||||
.equ SP_RPC, 4
|
||||
.equ SP_OFF, (SP_SAVED + SP_RPC)
|
||||
|
||||
|
|
@ -305,43 +69,56 @@ _surface68kFillRectByteAligned:
|
|||
.equ SP_FB2, SP_OFF + 36 + 3
|
||||
.equ SP_FB3, SP_OFF + 40 + 3
|
||||
|
||||
| Macro: per-plane work fully inlined. Args:
|
||||
| plane_an = the address register holding this plane's pointer.
|
||||
| fb_off = the stack offset for this plane's fillByte.
|
||||
| Uses d6/d7 as scratch; d1=leftMask, d2=~leftMask, d3=rightMask,
|
||||
| d4=~rightMask; d0=numMid-1 (only valid if mid_count > 0). The mid
|
||||
| loop is skipped via .LfsSkipMid_<n> when numMid was 0 at entry --
|
||||
| the per-plane caller branches to the right tail label.
|
||||
| Per-plane work is fully inlined (hand-unrolled 4x rather than
|
||||
| bsr+rts, to dodge ~12 cyc per return + the per-plane re-test of
|
||||
| numMid that an earlier build paid). Register convention across all
|
||||
| four planes:
|
||||
| d1 = leftMask, d2 = ~leftMask, d3 = rightMask, d4 = ~rightMask
|
||||
| d5 = this plane's fb byte, d6/d0/d7 = per-plane scratch.
|
||||
|
|
||||
| Hand-unrolled 4x rather than using bsr+rts to dodge ~12 cyc per
|
||||
| return + the per-plane re-test of numMid that the previous build
|
||||
| paid. The mid-loop label suffix is the plane index so all four
|
||||
| inline copies can coexist without label collisions.
|
||||
| The unmasked middle run is filled with long stores rather than
|
||||
| byte-at-a-time. fb is 0x00 or 0xFF, so the long is built with a
|
||||
| byte->word->long sign-extend (ext.w / ext.l). The mid pointer is
|
||||
| advanced to a long boundary by storing 0-3 fb bytes (align run),
|
||||
| then move.l (an)+ fills (midRem >> 2) longs, then a 0-3 byte tail
|
||||
| drains the remainder. The masked left/right edge bytes keep their
|
||||
| per-plane read-modify-write; only the unmasked middle changed.
|
||||
|
|
||||
| Plain text version of the per-plane body (translate to asm 4x with
|
||||
| different a-regs and fb stack offsets):
|
||||
|
|
||||
| move.b (an),%d6
|
||||
| move.b (an),%d6 | --- leading masked edge byte ---
|
||||
| and.b %d2,%d6
|
||||
| move.b fb,%d7
|
||||
| move.b fb,%d5
|
||||
| move.b %d5,%d7
|
||||
| and.b %d1,%d7
|
||||
| or.b %d7,%d6
|
||||
| move.b %d6,(an)+
|
||||
| < if has-middle path: >
|
||||
| move.w %d0,%d7
|
||||
| .midN:
|
||||
| move.b fb,(an)+
|
||||
| dbra %d7,.midN
|
||||
| < trailing: >
|
||||
| move.b %d5,%d7 | build replicated fill long in d7
|
||||
| ext.w %d7
|
||||
| ext.l %d7
|
||||
| move.l an,%d6 | align = (-an) & 3, clamped to numMid
|
||||
| neg.l %d6
|
||||
| and.w #3,%d6
|
||||
| move.w numMid,%d0
|
||||
| < clamp d6 to d0; d0 = midRem = numMid - align >
|
||||
| < store d6 fb bytes to reach a long boundary >
|
||||
| < midLongs = midRem >> 2; tail = midRem & 3 >
|
||||
| .longN:
|
||||
| move.l %d7,(an)+
|
||||
| dbra midLongs,.longN
|
||||
| < store tail fb bytes >
|
||||
| < trailing masked edge byte: >
|
||||
| move.b (an),%d6
|
||||
| and.b %d4,%d6
|
||||
| move.b fb,%d7
|
||||
| move.b %d5,%d7
|
||||
| and.b %d3,%d7
|
||||
| or.b %d7,%d6
|
||||
| move.b %d6,(an)
|
||||
|
||||
_surface68kFillSpan4Planes:
|
||||
movem.l %d2-%d7/%a2-%a6,-(%sp)
|
||||
movem.l %d2-%d7/%a2-%a3,-(%sp)
|
||||
|
||||
move.b SP_LMASK(%sp),%d1
|
||||
move.b %d1,%d2
|
||||
|
|
@ -356,12 +133,27 @@ _surface68kFillSpan4Planes:
|
|||
move.l SP_P3(%sp),%a3
|
||||
|
||||
| One-time numMid test. d0.w = numMid; if 0 jump to
|
||||
| the no-middle entry, otherwise pre-decrement for dbra
|
||||
| and fall into the with-middle entry. Both paths
|
||||
| unroll all 4 planes.
|
||||
| the no-middle entry. The with-middle path now fills
|
||||
| the unmasked middle run with long stores (move.l
|
||||
| (an)+) instead of byte-at-a-time, after aligning the
|
||||
| mid pointer to a long boundary and draining a 0-3
|
||||
| byte tail. The masked left/right edge bytes keep their
|
||||
| per-plane read-modify-write exactly as before.
|
||||
|
|
||||
| The mid fill byte fbN is 0x00 or 0xFF, so the long
|
||||
| replication is a byte->word->long sign-extend (ext.w
|
||||
| then ext.l): 0xFF -> 0xFFFFFFFF, 0x00 -> 0x00000000.
|
||||
|
|
||||
| Per-plane register use during the mid run:
|
||||
| d5 = fbN byte (held for tail + trailing edge)
|
||||
| d7 = fbN replicated to a long (move.l fill value)
|
||||
| d6 = scratch / align count / midLongs count
|
||||
| d0 = numMid, then midRem, then tail-byte count
|
||||
| Masks d1-d4 stay live across all four planes. Local
|
||||
| numeric labels 1f-7f are reused per plane; each
|
||||
| reference resolves to the nearest copy.
|
||||
move.w SP_NMID(%sp),%d0
|
||||
beq .LfsNoMid
|
||||
subq.w #1,%d0
|
||||
|
||||
| ---- WITH-MIDDLE PATH ----
|
||||
| Plane 0
|
||||
|
|
@ -372,10 +164,36 @@ _surface68kFillSpan4Planes:
|
|||
and.b %d1,%d7
|
||||
or.b %d7,%d6
|
||||
move.b %d6,(%a0)+
|
||||
move.w %d0,%d7
|
||||
.LfsMid0: move.b %d5,(%a0)+
|
||||
dbra %d7,.LfsMid0
|
||||
move.b (%a0),%d6
|
||||
move.b %d5,%d7
|
||||
ext.w %d7
|
||||
ext.l %d7
|
||||
move.l %a0,%d6
|
||||
neg.l %d6
|
||||
and.w #3,%d6
|
||||
move.w SP_NMID(%sp),%d0
|
||||
cmp.w %d0,%d6
|
||||
bls.s 1f
|
||||
move.w %d0,%d6
|
||||
1: sub.w %d6,%d0
|
||||
tst.w %d6
|
||||
beq.s 2f
|
||||
3: move.b %d5,(%a0)+
|
||||
subq.w #1,%d6
|
||||
bne.s 3b
|
||||
2: move.w %d0,%d6
|
||||
lsr.w #2,%d6
|
||||
and.w #3,%d0
|
||||
tst.w %d6
|
||||
beq.s 5f
|
||||
subq.w #1,%d6
|
||||
4: move.l %d7,(%a0)+
|
||||
dbra %d6,4b
|
||||
5: tst.w %d0
|
||||
beq.s 7f
|
||||
6: move.b %d5,(%a0)+
|
||||
subq.w #1,%d0
|
||||
bne.s 6b
|
||||
7: move.b (%a0),%d6
|
||||
and.b %d4,%d6
|
||||
move.b %d5,%d7
|
||||
and.b %d3,%d7
|
||||
|
|
@ -390,10 +208,36 @@ _surface68kFillSpan4Planes:
|
|||
and.b %d1,%d7
|
||||
or.b %d7,%d6
|
||||
move.b %d6,(%a1)+
|
||||
move.w %d0,%d7
|
||||
.LfsMid1: move.b %d5,(%a1)+
|
||||
dbra %d7,.LfsMid1
|
||||
move.b (%a1),%d6
|
||||
move.b %d5,%d7
|
||||
ext.w %d7
|
||||
ext.l %d7
|
||||
move.l %a1,%d6
|
||||
neg.l %d6
|
||||
and.w #3,%d6
|
||||
move.w SP_NMID(%sp),%d0
|
||||
cmp.w %d0,%d6
|
||||
bls.s 1f
|
||||
move.w %d0,%d6
|
||||
1: sub.w %d6,%d0
|
||||
tst.w %d6
|
||||
beq.s 2f
|
||||
3: move.b %d5,(%a1)+
|
||||
subq.w #1,%d6
|
||||
bne.s 3b
|
||||
2: move.w %d0,%d6
|
||||
lsr.w #2,%d6
|
||||
and.w #3,%d0
|
||||
tst.w %d6
|
||||
beq.s 5f
|
||||
subq.w #1,%d6
|
||||
4: move.l %d7,(%a1)+
|
||||
dbra %d6,4b
|
||||
5: tst.w %d0
|
||||
beq.s 7f
|
||||
6: move.b %d5,(%a1)+
|
||||
subq.w #1,%d0
|
||||
bne.s 6b
|
||||
7: move.b (%a1),%d6
|
||||
and.b %d4,%d6
|
||||
move.b %d5,%d7
|
||||
and.b %d3,%d7
|
||||
|
|
@ -408,10 +252,36 @@ _surface68kFillSpan4Planes:
|
|||
and.b %d1,%d7
|
||||
or.b %d7,%d6
|
||||
move.b %d6,(%a2)+
|
||||
move.w %d0,%d7
|
||||
.LfsMid2: move.b %d5,(%a2)+
|
||||
dbra %d7,.LfsMid2
|
||||
move.b (%a2),%d6
|
||||
move.b %d5,%d7
|
||||
ext.w %d7
|
||||
ext.l %d7
|
||||
move.l %a2,%d6
|
||||
neg.l %d6
|
||||
and.w #3,%d6
|
||||
move.w SP_NMID(%sp),%d0
|
||||
cmp.w %d0,%d6
|
||||
bls.s 1f
|
||||
move.w %d0,%d6
|
||||
1: sub.w %d6,%d0
|
||||
tst.w %d6
|
||||
beq.s 2f
|
||||
3: move.b %d5,(%a2)+
|
||||
subq.w #1,%d6
|
||||
bne.s 3b
|
||||
2: move.w %d0,%d6
|
||||
lsr.w #2,%d6
|
||||
and.w #3,%d0
|
||||
tst.w %d6
|
||||
beq.s 5f
|
||||
subq.w #1,%d6
|
||||
4: move.l %d7,(%a2)+
|
||||
dbra %d6,4b
|
||||
5: tst.w %d0
|
||||
beq.s 7f
|
||||
6: move.b %d5,(%a2)+
|
||||
subq.w #1,%d0
|
||||
bne.s 6b
|
||||
7: move.b (%a2),%d6
|
||||
and.b %d4,%d6
|
||||
move.b %d5,%d7
|
||||
and.b %d3,%d7
|
||||
|
|
@ -426,17 +296,43 @@ _surface68kFillSpan4Planes:
|
|||
and.b %d1,%d7
|
||||
or.b %d7,%d6
|
||||
move.b %d6,(%a3)+
|
||||
move.w %d0,%d7
|
||||
.LfsMid3: move.b %d5,(%a3)+
|
||||
dbra %d7,.LfsMid3
|
||||
move.b (%a3),%d6
|
||||
move.b %d5,%d7
|
||||
ext.w %d7
|
||||
ext.l %d7
|
||||
move.l %a3,%d6
|
||||
neg.l %d6
|
||||
and.w #3,%d6
|
||||
move.w SP_NMID(%sp),%d0
|
||||
cmp.w %d0,%d6
|
||||
bls.s 1f
|
||||
move.w %d0,%d6
|
||||
1: sub.w %d6,%d0
|
||||
tst.w %d6
|
||||
beq.s 2f
|
||||
3: move.b %d5,(%a3)+
|
||||
subq.w #1,%d6
|
||||
bne.s 3b
|
||||
2: move.w %d0,%d6
|
||||
lsr.w #2,%d6
|
||||
and.w #3,%d0
|
||||
tst.w %d6
|
||||
beq.s 5f
|
||||
subq.w #1,%d6
|
||||
4: move.l %d7,(%a3)+
|
||||
dbra %d6,4b
|
||||
5: tst.w %d0
|
||||
beq.s 7f
|
||||
6: move.b %d5,(%a3)+
|
||||
subq.w #1,%d0
|
||||
bne.s 6b
|
||||
7: move.b (%a3),%d6
|
||||
and.b %d4,%d6
|
||||
move.b %d5,%d7
|
||||
and.b %d3,%d7
|
||||
or.b %d7,%d6
|
||||
move.b %d6,(%a3)
|
||||
|
||||
movem.l (%sp)+,%d2-%d7/%a2-%a6
|
||||
movem.l (%sp)+,%d2-%d7/%a2-%a3
|
||||
rts
|
||||
|
||||
.LfsNoMid:
|
||||
|
|
@ -501,5 +397,5 @@ _surface68kFillSpan4Planes:
|
|||
or.b %d7,%d6
|
||||
move.b %d6,(%a3)
|
||||
|
||||
movem.l (%sp)+,%d2-%d7/%a2-%a6
|
||||
movem.l (%sp)+,%d2-%d7/%a2-%a3
|
||||
rts
|
||||
|
|
|
|||
|
|
@ -11,29 +11,22 @@ JOEY_TOOLCHAINS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||
export JOEY_TOOLCHAINS
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# IIgs
|
||||
# IIgs (llvm-mos w65816: clang + llvm-mc + link816)
|
||||
#
|
||||
# Two separate trees:
|
||||
# - toolchains/iigs/gg-tools/bin/ Unix host binaries (iix, dumpobj, ...)
|
||||
# built from the GoldenGate source repo.
|
||||
# - toolchains/iigs/goldengate/ The IIgs filesystem iix emulates.
|
||||
# $GOLDEN_GATE points here.
|
||||
#
|
||||
# iix is the host-side command shell that runs ORCA tools (cc, Linker,
|
||||
# Asm65816, ...) via 65816 emulation against the $GOLDEN_GATE tree.
|
||||
# LLVM816_ROOT is the toolchain root (clang / llvm-mc / link816 plus the
|
||||
# prebuilt runtime). clang-build.sh drives compile/assemble/link. It can
|
||||
# be overridden to point at a vendored copy; it currently defaults to the
|
||||
# build scratchpad where the toolchain was bootstrapped.
|
||||
# TODO: vendor the toolchain under toolchains/iigs.
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
export GOLDEN_GATE="${JOEY_TOOLCHAINS}/iigs/goldengate"
|
||||
export ORCA_ROOT="${GOLDEN_GATE}"
|
||||
|
||||
: "${LLVM816_ROOT:=/tmp/claude-1000/-home-scott-claude-joeylib/bf471cbc-f6e2-4287-86b6-58e16cc489e6/scratchpad/65816-llvm-mos}"
|
||||
export LLVM816_ROOT
|
||||
export IIGS_CLANG_BUILD="${JOEY_TOOLCHAINS}/iigs/clang-build.sh"
|
||||
# merlin32 assembles the NinjaTrackerPlus audio replayer (third-party asm).
|
||||
export IIGS_AS="${JOEY_TOOLCHAINS}/iigs/merlin32/bin/merlin32"
|
||||
export IIGS_IIX="${JOEY_TOOLCHAINS}/iigs/gg-tools/bin/iix"
|
||||
export IIGS_BUILD="${JOEY_TOOLCHAINS}/iigs/iix-build.sh"
|
||||
export IIGS_INCLUDE_SHIM="${JOEY_TOOLCHAINS}/iigs/include-shim"
|
||||
# Kept as an alias for IIGS_IIX so the top-level Makefile's HAVE_IIGS
|
||||
# availability probe (`[ -x $IIGS_CC ]`) works with the common pattern
|
||||
# used for the other platforms.
|
||||
export IIGS_CC="${IIGS_IIX}"
|
||||
# Alias used by the top-level Makefile's HAVE_IIGS probe (`[ -x $IIGS_CC ]`).
|
||||
export IIGS_CC="${IIGS_CLANG_BUILD}"
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Amiga
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
# Installs all cross-compilers, assemblers, and libraries needed to build
|
||||
# JoeyLib for Apple IIgs, Amiga, Atari ST, and MS-DOS into the local
|
||||
# toolchains/ folder. Nothing is installed system-wide. Free tools are
|
||||
# fetched and built; non-free tools (ORCA/C, GoldenGate) print clear
|
||||
# fetched and built; tools that must be placed manually print clear
|
||||
# placement instructions and the script verifies they were placed
|
||||
# correctly on a subsequent run.
|
||||
#
|
||||
|
|
@ -177,7 +177,8 @@ should_install() {
|
|||
}
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# IIgs: Merlin32 (free), ORCA/C (manual), GoldenGate (manual)
|
||||
# IIgs: Merlin32 (free) + NinjaTrackerPlus audio source. The w65816
|
||||
# clang/llvm-mos toolchain is referenced via LLVM816_ROOT (see env.sh).
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
install_audio_libxmp() {
|
||||
|
|
@ -352,188 +353,6 @@ EOF
|
|||
fi
|
||||
fi
|
||||
|
||||
# GoldenGate Unix-side host tools (iix, dumpobj, profuse, ...).
|
||||
# These are built from the GoldenGate source repo at stuff/GoldenGate/.
|
||||
# They must NOT live inside the IIgs filesystem tree that
|
||||
# $GOLDEN_GATE points at, because that tree represents the emulated
|
||||
# IIgs volume (case-insensitive, with its own bin/ of IIgs utilities).
|
||||
# Install layout:
|
||||
# toolchains/iigs/gg-tools/bin/ Unix binaries on host PATH
|
||||
# toolchains/iigs/goldengate/ $GOLDEN_GATE root (IIgs filesystem)
|
||||
local gg_tools_dir="${base}/gg-tools"
|
||||
local gg_root="${base}/goldengate"
|
||||
local stuff_dir="${REPO_DIR}/stuff"
|
||||
|
||||
# Migrate any Unix tools the user may have placed at goldengate/bin/
|
||||
# over to gg-tools/bin/ so the goldengate directory can be repopulated
|
||||
# with the IIgs filesystem tree from gg-linux.tgz. iix's siblings gno
|
||||
# and orca are typically symlinks to iix -- use -e (not -f) so broken
|
||||
# symlinks from a prior partial migration also match.
|
||||
if [ -x "${gg_root}/bin/iix" ] && [ ! -x "${gg_tools_dir}/bin/iix" ]; then
|
||||
info "Relocating Unix host tools from goldengate/bin to gg-tools/bin"
|
||||
mkdir -p "${gg_tools_dir}/bin"
|
||||
# Move iix last so the symlinks that point at it resolve during mv.
|
||||
for f in dumpobj profuse mkfs-profuse opus-extractor orca gno iix; do
|
||||
if [ -e "${gg_root}/bin/${f}" ] || [ -L "${gg_root}/bin/${f}" ]; then
|
||||
mv "${gg_root}/bin/${f}" "${gg_tools_dir}/bin/"
|
||||
fi
|
||||
done
|
||||
rmdir "${gg_root}/bin" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Clean up any dangling symlinks that reference a now-moved iix.
|
||||
if [ -d "${gg_root}/bin" ]; then
|
||||
find "${gg_root}/bin" -maxdepth 1 -type l -xtype l -delete 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Recreate gno/orca symlinks next to iix in gg-tools if they're
|
||||
# missing (iix behaves differently when invoked as 'gno' or 'orca').
|
||||
if [ -x "${gg_tools_dir}/bin/iix" ]; then
|
||||
for alias in gno orca; do
|
||||
if [ ! -e "${gg_tools_dir}/bin/${alias}" ]; then
|
||||
ln -s iix "${gg_tools_dir}/bin/${alias}"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Verify Unix tools
|
||||
if [ -x "${gg_tools_dir}/bin/iix" ]; then
|
||||
ok "GoldenGate host tools present at ${gg_tools_dir}/bin/"
|
||||
STATUS[iigs_gg_tools]="ok"
|
||||
else
|
||||
STATUS[iigs_gg_tools]="missing"
|
||||
INSTRUCTIONS[iigs_gg_tools]=$(cat <<EOF
|
||||
GoldenGate Unix host tools not found at ${gg_tools_dir}/bin/.
|
||||
Build from the GoldenGate source repo (e.g. in stuff/GoldenGate):
|
||||
cd stuff/GoldenGate
|
||||
git submodule update --init
|
||||
mkdir -p build && cd build
|
||||
cmake .. && make iix dumpobj profuse mkfs-profuse opus-extractor
|
||||
Then copy the built binaries to ${gg_tools_dir}/bin/ and re-run
|
||||
./toolchains/install.sh to verify.
|
||||
EOF
|
||||
)
|
||||
fi
|
||||
|
||||
# Populate the IIgs filesystem at $GOLDEN_GATE. Sources expected in
|
||||
# stuff/:
|
||||
# gg-linux.tgz IIgs support tree (bin, etc, Home, lib, System, usr)
|
||||
# fst.tgz File System Translators and fst-rom
|
||||
# opus/ Opus II extract with Languages/Libraries/Shell/Utilities/
|
||||
local gg_linux_tgz="${stuff_dir}/gg-linux.tgz"
|
||||
local fst_tgz="${stuff_dir}/fst.tgz"
|
||||
local opus_dir="${stuff_dir}/opus"
|
||||
|
||||
if [ "${FORCE_INSTALL}" -eq 1 ]; then
|
||||
clear_done "iigs_goldengate"
|
||||
clear_done "iigs_orca"
|
||||
rm -rf "${gg_root}"
|
||||
fi
|
||||
|
||||
if is_done "iigs_goldengate" && [ -f "${gg_root}/System/rom" ] && [ -d "${gg_root}/bin" ]; then
|
||||
ok "GoldenGate IIgs filesystem already populated"
|
||||
STATUS[iigs_goldengate]="ok"
|
||||
else
|
||||
if [ ! -f "${gg_linux_tgz}" ]; then
|
||||
STATUS[iigs_goldengate]="missing"
|
||||
INSTRUCTIONS[iigs_goldengate]=$(cat <<EOF
|
||||
Cannot populate the $GOLDEN_GATE tree at ${gg_root}/.
|
||||
Expected archive: ${gg_linux_tgz}
|
||||
Place the gg-linux.tgz archive (from the GoldenGate distribution) into
|
||||
${stuff_dir}/ and re-run ./toolchains/install.sh.
|
||||
EOF
|
||||
)
|
||||
else
|
||||
info "Extracting gg-linux.tgz into goldengate/"
|
||||
mkdir -p "${gg_root}"
|
||||
# The archive roots at gg-linux/GoldenGate/; strip both levels.
|
||||
tar -xzf "${gg_linux_tgz}" -C "${gg_root}" --strip-components=2 \
|
||||
gg-linux/GoldenGate/ 2>/dev/null || {
|
||||
STATUS[iigs_goldengate]="failed"
|
||||
INSTRUCTIONS[iigs_goldengate]="Extract of ${gg_linux_tgz} failed"
|
||||
}
|
||||
|
||||
# Extract fix-filetypes.sh separately (it sits at gg-linux/ root).
|
||||
if [ ! -f "${CACHE_DIR}/fix-filetypes.sh" ]; then
|
||||
tar -xzf "${gg_linux_tgz}" -C "${CACHE_DIR}" --strip-components=1 \
|
||||
gg-linux/fix-filetypes.sh 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# FST tarball: merge fst/orca/ subtree into goldengate/.
|
||||
if [ -f "${fst_tgz}" ]; then
|
||||
info "Merging fst.tgz into goldengate/System/"
|
||||
tar -xzf "${fst_tgz}" -C "${gg_root}" --strip-components=2 \
|
||||
fst/orca/ 2>/dev/null || warn "fst.tgz extract reported issues"
|
||||
else
|
||||
warn "${fst_tgz} not found; FSTs will be missing"
|
||||
fi
|
||||
|
||||
if [ -f "${gg_root}/System/rom" ] && [ -d "${gg_root}/bin" ]; then
|
||||
mark_done "iigs_goldengate"
|
||||
ok "GoldenGate IIgs filesystem populated"
|
||||
STATUS[iigs_goldengate]="ok"
|
||||
else
|
||||
STATUS[iigs_goldengate]="failed"
|
||||
INSTRUCTIONS[iigs_goldengate]="goldengate/ did not end up with System/rom and bin/; check ${gg_linux_tgz}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Merge ORCA (from Opus II) directories into goldengate/.
|
||||
if is_done "iigs_orca" && [ -f "${gg_root}/Languages/cc" ]; then
|
||||
ok "ORCA/C merged into goldengate/"
|
||||
STATUS[iigs_orca]="ok"
|
||||
else
|
||||
if [ "${STATUS[iigs_goldengate]}" != "ok" ]; then
|
||||
STATUS[iigs_orca]="missing"
|
||||
INSTRUCTIONS[iigs_orca]="Install GoldenGate IIgs filesystem first."
|
||||
elif [ ! -d "${opus_dir}" ]; then
|
||||
STATUS[iigs_orca]="missing"
|
||||
INSTRUCTIONS[iigs_orca]=$(cat <<EOF
|
||||
ORCA source directory not found at ${opus_dir}/.
|
||||
Extract the Opus II ORCA distribution into ${opus_dir}/ so it contains:
|
||||
Languages/ Libraries/ Shell/ Utilities/
|
||||
Then re-run ./toolchains/install.sh.
|
||||
EOF
|
||||
)
|
||||
else
|
||||
info "Merging Opus II ORCA directories into goldengate/"
|
||||
local orca_ok=1
|
||||
for sub in Languages Libraries Shell Utilities; do
|
||||
if [ -d "${opus_dir}/${sub}" ]; then
|
||||
cp -rn "${opus_dir}/${sub}" "${gg_root}/" 2>/dev/null || orca_ok=0
|
||||
else
|
||||
warn "${opus_dir}/${sub} missing"
|
||||
orca_ok=0
|
||||
fi
|
||||
done
|
||||
|
||||
# Run fix-filetypes.sh which sets IIgs filetype xattrs via chtyp.
|
||||
# chtyp is an IIgs binary inside goldengate/bin and is invoked
|
||||
# by fix-filetypes through iix, so we need iix on PATH and
|
||||
# GOLDEN_GATE exported first.
|
||||
if [ "${orca_ok}" -eq 1 ] && [ -f "${CACHE_DIR}/fix-filetypes.sh" ] \
|
||||
&& [ -x "${gg_tools_dir}/bin/iix" ]; then
|
||||
info "Running fix-filetypes.sh"
|
||||
(
|
||||
cd "${gg_root}"
|
||||
export GOLDEN_GATE="${gg_root}"
|
||||
export PATH="${gg_tools_dir}/bin:${PATH}"
|
||||
bash "${CACHE_DIR}/fix-filetypes.sh" >/dev/null 2>&1
|
||||
) || warn "fix-filetypes.sh reported issues; proceeding"
|
||||
fi
|
||||
|
||||
if [ "${orca_ok}" -eq 1 ] && [ -f "${gg_root}/Languages/cc" ]; then
|
||||
mark_done "iigs_orca"
|
||||
ok "ORCA/C merged and filetypes fixed"
|
||||
STATUS[iigs_orca]="ok"
|
||||
else
|
||||
STATUS[iigs_orca]="failed"
|
||||
INSTRUCTIONS[iigs_orca]="ORCA merge did not produce ${gg_root}/Languages/cc; check ${opus_dir}/"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# NinjaTrackerPlus -- Ninjaforce's IIgs MOD player. Distributed as
|
||||
# source (ZIP) plus binary tool disk images on ninjaforce.com.
|
||||
# Redistribution is granted by the copyright holder per the user's
|
||||
|
|
|
|||
|
|
@ -45,6 +45,24 @@ PORTS = [
|
|||
]
|
||||
|
||||
|
||||
# Appended below the table when IIgs is the reference port. The IIgs is
|
||||
# a 2.8 MHz 65816 moving a 32000-byte SHR screen, so any full-screen
|
||||
# byte-move op has a hard throughput ceiling; numbers above it are a
|
||||
# measurement artifact, not real speed.
|
||||
IIGS_CEILING_NOTE = (
|
||||
"\n"
|
||||
"> **IIgs full-screen ceiling (~29 ops/sec).** A full-screen op moves\n"
|
||||
"> 32000 bytes = 16000 word-stores x 6 cyc (STA long,X / PEI, both\n"
|
||||
"> 3 cyc/byte) / 2.8 MHz ~= 34 ms ~= 29 ops/sec maximum. So\n"
|
||||
"> `jlSurfaceClear`, `jlFillRect 320x200`, and `jlStagePresent full`\n"
|
||||
"> cannot legitimately exceed ~29; a higher reading means the benchmark\n"
|
||||
"> clock was perturbed. The clock is `jlFrameCount` = the VBL-interrupt-\n"
|
||||
"> driven GetTick, so an op that holds `SEI` while it runs can over-\n"
|
||||
"> report its ops/sec. Treat full-screen IIgs numbers above ~29 as\n"
|
||||
"> inflated and cross-check against the cycle model.\n"
|
||||
)
|
||||
|
||||
|
||||
def parse_log(path):
|
||||
"""Return {op: ops/sec} from a UBER log. Last entry per op wins."""
|
||||
perf = {}
|
||||
|
|
@ -144,6 +162,8 @@ def main(argv):
|
|||
ordered = ref_ops + extras
|
||||
|
||||
table = render_table(port_data, ordered, args.ref)
|
||||
if args.ref == "iigs":
|
||||
table += "\n" + IIGS_CEILING_NOTE
|
||||
if args.out:
|
||||
with open(args.out, "w") as f:
|
||||
f.write(table + "\n")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue