mirror of
https://github.com/irmen/prog8.git
synced 2026-10-22 05:02:58 +00:00
Compare commits
100
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5702e5aa2a | ||
|
|
592637856f | ||
|
|
233ca21492 | ||
|
|
1a84616363 | ||
|
|
d32b70bb75 | ||
|
|
73241b0261 | ||
|
|
e515dee561 | ||
|
|
3d78415088 | ||
|
|
0189903a63 | ||
|
|
9c9552174e | ||
|
|
be61d37fd1 | ||
|
|
70bb199e12 | ||
|
|
09737187ad | ||
|
|
de39533fba | ||
|
|
8bc2b177a0 | ||
|
|
09eba995ae | ||
|
|
b739d7b663 | ||
|
|
5e709fca7e | ||
|
|
cc8df33418 | ||
|
|
125af552ff | ||
|
|
29254e892b | ||
|
|
a1a48405b0 | ||
|
|
acc8705ea8 | ||
|
|
bbff472457 | ||
|
|
353b0c84b7 | ||
|
|
329b3df4a3 | ||
|
|
fe90ca572d | ||
|
|
26bbd6c1fe | ||
|
|
822cfd4f35 | ||
|
|
0807d07ee5 | ||
|
|
2e3aa76a2f | ||
|
|
3beb565c84 | ||
|
|
b1642f4f7a | ||
|
|
1f8b42fae5 | ||
|
|
13828ea932 | ||
|
|
e550143ed0 | ||
|
|
47e6066a36 | ||
|
|
266f4ab380 | ||
|
|
e5954542dd | ||
|
|
8c2bc15b41 | ||
|
|
c9a7dc05d9 | ||
|
|
9d023c9568 | ||
|
|
a1ce45e409 | ||
|
|
09d469714b | ||
|
|
38db571f20 | ||
|
|
5f3b587af1 | ||
|
|
e74c34a5ab | ||
|
|
2b156ec652 | ||
|
|
e38f71fd2d | ||
|
|
6c107379df | ||
|
|
99f29cb9b9 | ||
|
|
d6ea0782e2 | ||
|
|
bdee93dc8f | ||
|
|
3d675a460e | ||
|
|
bd1747926c | ||
|
|
cff7732189 | ||
|
|
fa966086e9 | ||
|
|
ec4b47ac0d | ||
|
|
5222562493 | ||
|
|
c8cda7947a | ||
|
|
ffe855f295 | ||
|
|
8a66dabf6d | ||
|
|
b8c975704a | ||
|
|
2ef0ddd42c | ||
|
|
9a83b7455a | ||
|
|
9fce07565c | ||
|
|
d703d678b4 | ||
|
|
90a7c41586 | ||
|
|
b1a9ba929d | ||
|
|
75ab3ccfbd | ||
|
|
c031737da0 | ||
|
|
2f905ef6b6 | ||
|
|
4cbf93da85 | ||
|
|
976b64185d | ||
|
|
2050fcb4ee | ||
|
|
69525ae67f | ||
|
|
4eaba62f1e | ||
|
|
bf696bcf5e | ||
|
|
34bbb46514 | ||
|
|
f086918e6f | ||
|
|
521f7ffbe6 | ||
|
|
2d070aaec4 | ||
|
|
044a4a1789 | ||
|
|
6de6a81d4b | ||
|
|
815f4109a5 | ||
|
|
a49f080ca7 | ||
|
|
ac4ae0440f | ||
|
|
773f1f2292 | ||
|
|
efee8bde68 | ||
|
|
ddfdd9097d | ||
|
|
ef64e2f9a4 | ||
|
|
2f6ab002d8 | ||
|
|
ba76fbfe85 | ||
|
|
7f3b2e8aa6 | ||
|
|
f254fc2b62 | ||
|
|
457d55b392 | ||
|
|
b21a984dbb | ||
|
|
7427e6ddad | ||
|
|
192e0086f9 | ||
|
|
c8972c9561 |
No files matched your search
@@ -125,6 +125,55 @@ Avoid self-modifying code unless it is explicitly required. It cannot run
|
||||
from ROM and complicates debugging and tooling. Prefer lookup tables, RAM
|
||||
vectors, or alternative algorithms.
|
||||
|
||||
## Reference
|
||||
|
||||
### Primary: 6502 Family CPU Reference
|
||||
|
||||
<https://www.pagetable.com/c64ref/6502/?cpu=65c02&tab=2> is the reference to reach for.
|
||||
It covers the whole 6502 family and lets you **select the CPU variant**, which matters
|
||||
because the parts genuinely differ. The variants are `6502`, `6502rorbug`, `65dtv02`,
|
||||
`65c02`, `r65c02` (Rockwell), `65c02s`, `65ce02` and `65c816`. Change the `cpu=` query
|
||||
parameter to switch; for Prog8, use `cpu=65c02` when working on cx16 and `cpu=6502` for
|
||||
c64, c128 and pet32.
|
||||
|
||||
Per instruction it gives the operation, the opcodes, the byte length, the cycle count
|
||||
and which status flags are affected, with tabs for opcodes, instructions, addressing
|
||||
modes and a full opcode table (switchable between 4-4 and 3-3-2 layout). It can also
|
||||
show the NMOS undocumented opcodes, and each part lists what it is based on, so a
|
||||
variant's inherited instructions are visible.
|
||||
|
||||
Cycle counts use a compact notation worth knowing: a trailing `t` means +1 if a branch
|
||||
is taken, `p` means +1 if a page is crossed, and `d` means +1 when the D (decimal) flag
|
||||
is set. So `d0 2+t+p` for `bne` is 2 cycles, 3 if taken, 4 if taken across a page.
|
||||
|
||||
Consult it whenever an exact figure is needed rather than recalled: the byte length or
|
||||
cycle cost of an instruction, which flags it clobbers, whether an addressing mode exists
|
||||
on the target CPU, or whether an instruction belongs to a different part at all. Typical
|
||||
reasons are costing a peephole or code-size tradeoff, checking a hand-written routine,
|
||||
and confirming a mnemonic is valid before using it.
|
||||
|
||||
Do not assume a timing from memory or from another part. Cycle counts are exactly the
|
||||
kind of detail that is misremembered, and the difference matters when it is the basis of
|
||||
an optimization claim. Read the number off the reference for the specific CPU being
|
||||
targeted.
|
||||
|
||||
### Secondary: WDC 65C02 reference
|
||||
|
||||
<https://cx16.dk/65c02/reference.html> is a 65C02-only reference. It is worth having
|
||||
because it states each instruction's effect in a single formula (for example
|
||||
`ADC: A,Z,C,N = A+M+C`), which makes the flag behaviour quick to check, and it lists
|
||||
every status bit per instruction including the ones not affected. Use it for flag
|
||||
semantics; use the primary reference for timings and for anything CPU-specific, since
|
||||
it covers the 65C02 only and will not answer a question about the NMOS 6502.
|
||||
|
||||
### Neither reference covers the assembler
|
||||
|
||||
Both document the CPU, not 64tass. For whether 64tass accepts a mnemonic or a given
|
||||
syntax, assemble it: an instruction outside the selected CPU set fails with
|
||||
`error: general syntax` rather than being silently ignored. Note also that 64tass has
|
||||
no `w65c02s`; its CPU selection is `--m65xx` (default), `--m65c02` and `--m65ce02`, and
|
||||
Prog8 emits `.cpu 'w65c02'`.
|
||||
|
||||
## Tools
|
||||
|
||||
Prog8 invokes 64tass with `--ascii --case-sensitive --long-branch -Wall` plus
|
||||
@@ -137,3 +186,15 @@ the target output option such as `--cbm-prg`. For manual assembly:
|
||||
|
||||
For debugging, add `--vice-labels --labels=labels.txt` and/or
|
||||
`--list=listing.txt`.
|
||||
|
||||
To check an instruction's encoding or size, assemble it in isolation and read the
|
||||
output bytes. The 2-byte load address comes first, so `stx $7b` / `ldy $7b` below
|
||||
shows as `86 7b a4 7b` - two instructions, four bytes:
|
||||
|
||||
```bash
|
||||
printf '.cpu %s\n* = $0801\nlbl\tstx\t$7b\n\tldy\t$7b\n' "'w65c02'" > sz.asm
|
||||
64tass --ascii --case-sensitive --cbm-prg -o sz.prg sz.asm && xxd sz.prg
|
||||
```
|
||||
|
||||
Note that a 64tass label takes no trailing colon; `lbl:` is a syntax error reported as
|
||||
`error: label required`.
|
||||
@@ -63,6 +63,17 @@ when both operands are registers.
|
||||
is at offset `+3`.
|
||||
- Word and long accesses must be even-aligned on 68000. 68020 permits
|
||||
misalignment but it is slower.
|
||||
- **Byte `A7` auto-update steps by 2, not 1.** If A7 (== SP, the stack pointer) is the address
|
||||
register and the operand length for post-increment or post-decrement is one byte, A7 is
|
||||
decremented/incremented by 2 instead of 1, to maintain a word-aligned stack. This is the
|
||||
specified behaviour of all the 680x0, not a 68000 quirk, so it is identical everywhere and
|
||||
safe to rely on. Word and long operands step by 2 and 4.
|
||||
```asm
|
||||
move.b d0,-(sp) ; A7 steps by 2, so SP stays word-aligned
|
||||
...
|
||||
addq.l #2, sp ; and the matching pop is #2, not #1
|
||||
```
|
||||
Do not "fix" this to an explicit `subq.l #1,sp`: that leaves SP odd and cause crash.
|
||||
- The 68000 (`amiga500`) has only word multiply/divide and no scaled indexing,
|
||||
bit-field, or 32-bit multiply/divide instructions. The 68020 (`qemu68k`)
|
||||
adds those features and `extb.l`.
|
||||
@@ -82,8 +93,31 @@ when both operands are registers.
|
||||
pointer.
|
||||
- `D0-D1` and `A0-A1` are scratch registers. Preserve registers required by
|
||||
the surrounding calling convention, especially `D2-D7` and `A2-A6`.
|
||||
- Never clobber `A5`: on every M68K target it is the Prog8 stack frame pointer
|
||||
(locals at negative offsets, parameters at positive offsets). Overwriting it
|
||||
corrupts the frame of the running subroutine and breaks unwinding.
|
||||
- Never clobber `A6` on the Amiga targets (`amiga500`, `amiga1200`): it holds
|
||||
the AmigaOS library base pointer that library calls depend on. Prog8 itself
|
||||
leaves `A6` free for that reason.
|
||||
- `move.l d0,-(sp)` pushes and `move.l (sp)+,d0` pops.
|
||||
|
||||
### Prog8 M68K stack ABI
|
||||
|
||||
- Normal framed subroutines use `link a5,#-N` and `unlk a5`; locals are at
|
||||
negative `A5` offsets and incoming parameters are at positive offsets.
|
||||
- Arguments use one padded 4-byte slot each. The caller pushes arguments
|
||||
left-to-right, right-justifies byte and word values in their slots, and
|
||||
removes the complete argument area after the call. Parameter `i` of `N`
|
||||
arguments is at `8 + 4 * (N - 1 - i)` from the callee's `A5` when `i` is zero-based.
|
||||
- A normal call preserves the caller's frame. A `goto` or conditional branch
|
||||
leaving the current subroutine must execute `unlk a5` on the taken path before
|
||||
transferring control.
|
||||
- The built-in Prog8 `call(address)` is a zero-argument indirect call. It does
|
||||
not marshal stack arguments. `asmsub` and `extsub` keep their explicit
|
||||
register-annotated ABI, separate from normal framed calls.
|
||||
- Do not push byte arguments directly with `move.b ..., -(sp)`: reserve a full
|
||||
4-byte slot and store the byte at offset `3(sp)`.
|
||||
|
||||
## Prog8 Integration
|
||||
|
||||
Assembly can appear in `%asm {{ }}` blocks or `asmsub` routines. `asmsub`
|
||||
|
||||
@@ -34,6 +34,9 @@ repeating them here:
|
||||
- Use `%zeropage basicsafe` in test programs when a clean return is needed.
|
||||
- Find library routines with `prog8c -libsearch <regex>` or extract them with
|
||||
`prog8c -libdump <dir>`.
|
||||
- Options accept single-dash or double-dash (`-target` == `--target`). Use
|
||||
`-out <dir>` to place output in a directory, or `-o <file>` to name the exact
|
||||
final program artifact (all other output files then land beside it).
|
||||
|
||||
For CX16 emulator programs, use `%encoding iso`, call `txt.iso()` in `start`,
|
||||
and finish with `sys.poweroff_system()`. CBM targets use PETSCII by default;
|
||||
@@ -57,9 +60,24 @@ libraries.
|
||||
- M68000 targets are big-endian and use 32-bit pointers. Load the `m68k-coder`
|
||||
skill for assembly details.
|
||||
- `float` requires `%import floats`.
|
||||
- Variables are statically allocated and zero-initialized. There is no normal
|
||||
call stack for locals, so recursion overwrites locals unless an explicit
|
||||
hardware or software stack is used. Iterative rewrites are preferred.
|
||||
- On 6502 and virtual targets, variables are statically allocated and
|
||||
zero-initialized. There is no normal call stack for locals, so recursion
|
||||
overwrites locals unless an explicit hardware or software stack is used.
|
||||
Iterative rewrites are preferred. Arrays are cleared once at startup here,
|
||||
not on every subroutine entry.
|
||||
- On the M68000 targets (`amiga500`, `amiga1200`, `qemu68k`) ordinary subroutine
|
||||
locals and parameters live in an activation record on the CPU stack, so true
|
||||
recursion and re-entrancy work. `link a5,#-N` / `unlk a5`, frame pointer `A5`,
|
||||
locals at negative offsets, parameters at positive offsets. Block-level
|
||||
variables, globals, `@shared`, memory slabs, and objects needing a permanent
|
||||
external address stay static. Clean locals are zeroed on every call
|
||||
including arrays, so the compiler warns for framed arrays over 8 bytes; use
|
||||
`@dirty` when you assign first, otherwise you read stack garbage.
|
||||
- Normal M68K calls use uniform 4-byte, right-justified argument slots. The caller
|
||||
pushes arguments left-to-right and removes the complete argument area after the
|
||||
call. Framed parameters start at positive `A5` offsets, with the first parameter
|
||||
at `8 + 4 * (parameter_count - 1)`; locals use negative offsets. See
|
||||
`docs/source/technical.rst` for the full convention and static fallback rules.
|
||||
- `memory(name, size)` reserves static memory and returns a `pointer`.
|
||||
- `str` and arrays have compile-time byte budgets. On 6502 targets the usual
|
||||
limit is 256 bytes; M68000 targets allow up to 32768 bytes. Use `memory()`
|
||||
@@ -106,10 +124,16 @@ Use `if_cs`, `if_cc`, `if_z`, and `if_nz` for direct CPU-flag branches.
|
||||
|
||||
- Subroutines can return zero, one, or multiple values:
|
||||
`a, b = routine()` or `void routine()`.
|
||||
- On M68K, ordinary calls preserve the caller's active frame. A `goto` or branch
|
||||
that leaves the current subroutine must tear that frame down before transferring
|
||||
control. The built-in `call(address)` is a zero-argument indirect call; it does
|
||||
not marshal stack arguments.
|
||||
- Avoid `private` unless requested. Prog8 symbols are public by default.
|
||||
- `asmsub` bodies contain only one `%asm {{ ... }}` node. Parameters are type
|
||||
checked and documented, but assembly must use the mapped registers. Declare
|
||||
every modified hardware register in `clobbers`.
|
||||
- On M68K targets never clobber `A5`, the stack frame pointer. On the Amiga
|
||||
targets never clobber `A6` either, it holds the AmigaOS library base.
|
||||
- `extsub` maps a signature to a fixed external address and has no body. Use
|
||||
it for ROM, kernel, drivers, or binary-library routines.
|
||||
- For inline assembly, load `asm6502-coder` for 64tass syntax, target-specific
|
||||
@@ -128,6 +152,12 @@ Use `if_cs`, `if_cc`, `if_z`, and `if_nz` for direct CPU-flag branches.
|
||||
|
||||
## Verification
|
||||
|
||||
- A full `gradle build` is CPU- and IO-heavy so lower its priority on Unix like systems. Prefix
|
||||
it with `nice`, e.g. `nice gradle build --console=plain`, and use `nice -n 19` for
|
||||
maximum politeness. Targeted runs (`gradle :compiler:test --tests "..."`) are
|
||||
cheaper but still worth lowering the priority of.
|
||||
- Always pass `--console=plain`; the fancy console output interferes with agent
|
||||
output parsing.
|
||||
- Use the `virtual` target for behavioral tests when possible:
|
||||
`prog8c -target virtual -emu program.p8`.
|
||||
- Use `-check` for syntax and semantic checks without output generation.
|
||||
@@ -135,6 +165,22 @@ Use `if_cs`, `if_cc`, `if_z`, and `if_nz` for direct CPU-flag branches.
|
||||
- For IR execution, use `-vmtrace` when control flow needs inspection.
|
||||
- Do not modify a correct test program to work around a compiler crash. Reduce
|
||||
the case and fix the compiler instead.
|
||||
- `gradle build` already runs the full test suite; do not run a separate
|
||||
`gradle test` before it, to avoid a lengthy double run.
|
||||
- `TestCompilerOnExamples` asserts exact output sizes per example. Any change to
|
||||
the code generator shifts them, so expect a handful of
|
||||
`expected:<N> but was:<M>` failures with no stack trace pointing at the cause.
|
||||
Attribute each one before editing: rebuild with the change reverted and compare
|
||||
the two generated `.asm` files, so the size delta is demonstrably yours and not a
|
||||
side effect. Then update the expected size. A delta of a few bytes from removed
|
||||
`ldy`/`lda` instructions is normal; a large or erratic delta means something else
|
||||
changed and needs investigating.
|
||||
- When running `gradle` tests, grep for `SUCCESSFUL` to check if all tests
|
||||
passed, and `FAILED` to check for failures. Gradle does not print individual
|
||||
`PASSED` lines by default.
|
||||
- Do not write compiler unit tests that depend on the `vasm` assembler: it is
|
||||
not always installed (CI only installs `64tass`). For m68k codegen tests
|
||||
(`amiga500`, `qemu68k` targets) pass `writeAssembly = true, assemble = false`
|
||||
to the `compileText`/`compileFile` helpers. This still generates the codegen
|
||||
AST to assert on, but skips assembling, so no vasm is needed. If a test's
|
||||
assertions cannot be made from the AST alone, drop the test.
|
||||
+4
-1
@@ -45,4 +45,7 @@ compiler/lib/
|
||||
.gradle
|
||||
sd*.img
|
||||
*.d64
|
||||
|
||||
AsyncIO/
|
||||
AsyncIO.info
|
||||
m68k-amigaos-ahi/
|
||||
Libs/
|
||||
Generated
-1
@@ -4,7 +4,6 @@
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="distributionType" value="LOCAL" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
|
||||
@@ -13,17 +13,23 @@
|
||||
|
||||
## CHANGE / EDIT MODE
|
||||
|
||||
- Never implement features yourself when possible - use sub-agents!
|
||||
- Use a subagent for larger chunks of work
|
||||
- Identify changes from the plan that can be implemented in parallel, and use sub-agents to implement the features efficiently
|
||||
- When using sub-agents to implement features, act as a coordinator only
|
||||
- Use the best model for the task - premium models for complex tasks (like coding) and mid-tier models for simpler tasks, like documentation
|
||||
- After completing features (large or small), always run commands like lint, type check and next build to check code quality
|
||||
- After completing features (large or small), run the relevant quality commands (lint, type check, build) to check code quality.
|
||||
Only compiler/standard-library changes require a build: that means Kotlin sources (`.kt`), the grammar (`.g4`), or embedded
|
||||
standard library files (`compiler/res/prog8lib/**/*.p8` and `**/*.asm`). For changes limited to `examples/**`, `docs/**`, or
|
||||
other non-compiler files, verify with the target compiler directly instead (for example `prog8c -target <target> <file>`), and
|
||||
skip the build entirely
|
||||
(exception: if the change is only whitespace or code comments, a build or test run is not necessary)
|
||||
- `gradle build` already runs the full test suite; don't run a separate full `gradle test` before it, to avoid lengthy double test runs (targeted `--tests` filters to find failing tests are fine)
|
||||
|
||||
## Project Overview
|
||||
- This project is a compiler for the Prog8 programming language.
|
||||
- Prog8 is a programming language primarily targeting 8-bit retro systems with the 6502 CPU, such as the Commodore 64, Commodore 128, and Commander X16.
|
||||
- The compiler has a 6502 code generator backend, and an IR code generator.
|
||||
- The IR code is meant to be used in a new machine specific code generator backend (primarily 6502 but maybe 68000 as well later)
|
||||
- The IR code is used by machine-specific code generator backends, including 6502 and M68K.
|
||||
- The compiler includes a simple 'virtual machine' that can execute the IR code directly via interpretation.
|
||||
- Prog8 source files have .p8 extension – these are *not* LUA or PICO-8 source files in this case!
|
||||
- Prog8 source files are a "module" that can contain one or more "blocks". They can also import other modules, from internal library files or from source files on the filesystem.
|
||||
@@ -55,7 +61,8 @@ Source → parseMainModule() → processAst() → optimizeAst() → postprocessA
|
||||
### Target Differences
|
||||
- **CPU instruction set differences**: Only the CommanderX16 target (cx16) can use 65C02 instructions such as STZ. The other targets (C64, C128, PET32) can only use original 6502 instructions.
|
||||
- When writing or understanding 6502/65C02 assembly code, load the `asm6502-coder` skill for 64tass syntax and conventions.
|
||||
- When writing or understanding M68K assembly code (amiga500, qemu68k targets), load the `m68k-coder` skill for vasm mot syntax and conventions.
|
||||
- When writing or understanding M68K assembly code (amiga500, amiga1200, qemu68k targets), load the `m68k-coder` skill for vasm mot syntax and conventions.
|
||||
- M68K ordinary subroutine locals and parameters use `A5`-based stack frames. Normal calls pass padded 4-byte argument slots on the stack, and the caller cleans up; see `docs/source/technical.rst` for the complete convention and frame eligibility rules.
|
||||
|
||||
|
||||
## DEBUGGING TIP: Use `-noopt` to isolate problems
|
||||
@@ -127,7 +134,19 @@ Steps when encountering a compiler crash:
|
||||
- **NO EMOJI in user documentation**. Do not use emoji or decorative unicode symbols in documentation files. Functional unicode symbols are acceptable when they serve a clear purpose (e.g., → for arrows, ± for plus-minus, × for multiplication, ° for degrees). Avoid decorative emoji like ❌ ✅ ⚠️ 🎉 etc.
|
||||
|
||||
### Code Style Guidelines
|
||||
**Minimal comments when making changes**: When modifying existing code, add only essential comments that explain *why* a change was made or document non-obvious behavior. **Do not add verbose comments** that restate what the code does; let the code speak for itself. Existing extensive comments should be preserved, but new changes should have minimal commentary.
|
||||
**Code comments**: Comments are the exception, not the norm. A comment earns its place only by recording
|
||||
something the code itself cannot say: the reason for a non-obvious choice, or a constraint that
|
||||
a well-meaning future edit would otherwise violate.
|
||||
|
||||
- Inline comment inside a function body: 1 line, never more.
|
||||
- KDoc on a declaration: 3 lines, unless a genuine non-obvious contract cannot be stated shorter.
|
||||
- Never restate the identifier, the type, or what the following line does.
|
||||
- Prefer a good name or a well-named function over a comment that explains it.
|
||||
|
||||
Before keeping any comment, delete it mentally and ask what the reader loses. If nothing, it goes.
|
||||
|
||||
Keep existing comments as they are; do not expand, reword, or reformat them while editing
|
||||
nearby lines - unless that would cause inconsistency.
|
||||
|
||||
**Kotlin string `$` handling**: The `$` character is special in Kotlin string literals (string interpolation). If you need a literal `$` character, use Kotlin's `$$"..."` interpolation prefix instead of backslash escaping (`\$`) or `${'$'}`. The `$$"..."` syntax is a fairly recent Kotlin feature that avoids nasty escaping. For example:
|
||||
```kotlin
|
||||
@@ -198,10 +217,12 @@ When the task involves writing or understanding `.p8` (Prog8 source) or `.p8ir`
|
||||
- This compiles AND installs, but still skips running tests (faster than `gradle build`)
|
||||
|
||||
### When to run full build with tests
|
||||
- **Before final verification**: Always run `gradle build` to ensure all tests pass
|
||||
- **Before final verification**: Run `gradle build` to ensure all tests pass - but only if the change actually touched the
|
||||
compiler or standard library (`.kt`, `.g4`, or `compiler/res/prog8lib/**`). For `examples/**` or `docs/**`-only changes,
|
||||
verify with `prog8c` directly instead and skip the build
|
||||
- **After major refactoring**: Run `gradle build` to catch regressions
|
||||
- **When debugging test failures**: Use `gradle test --tests "*TestName*"` for specific tests
|
||||
- **When working on a standalone code gen module** (one that reads `.p8ir` files): Run `gradle :<module>:test --console=plain` instead of full `gradle build`. Compiler unit tests are irrelevant for IR-level changes. Do run full `gradle build` before final verification.
|
||||
- **When working on a standalone code gen module** (one that reads `.p8ir` files): Run `gradle :<module>:test --console=plain` instead of full `gradle build`. Compiler unit tests are irrelevant for IR-level changes. Do run full `gradle build` before final verification if the change touched compiler sources.
|
||||
|
||||
### Build command summary
|
||||
|
||||
@@ -218,13 +239,15 @@ When the task involves writing or understanding `.p8` (Prog8 source) or `.p8ir`
|
||||
- the prog8c compiler executable can be found in the compiler/build/install/prog8c/bin folder (this is already added to the shell's path)
|
||||
- **the `-check` switch performs a quick syntax/semantic check only; it will NOT produce any output files (no .prg, .asm, etc.)**. Use it only for fast error checking during development.
|
||||
- **the `-noopt` switch DISABLES all optimizations** - useful for debugging to determine if a problem is caused by the optimizer. **Optimizations are ENABLED by default** (no flag needed).
|
||||
- **prog8c uses single-dash command line options** (e.g., `-target`, `-noopt`, `-check`), NOT double-dash (`--target` is invalid).
|
||||
- **prog8c options accept single-dash or double-dash** (e.g., `-target` and `--target` are equivalent).
|
||||
- **the `-printast1` switch prints out the internal Compiler AST** after parsing and semantic analysis.
|
||||
- **the `-printast2` switch prints out the optimized Simple AST** just before it goes to the code generator. This is useful for debugging optimizer issues.
|
||||
- **the `-out outdir` switch sets an alternative output directory** for compiled files (.prg, .asm, .list, etc.). **By default, output files are written to the same directory as the source file**.
|
||||
- **the `-out outdir` switch sets an alternative output directory** for compiled files (.prg, .asm, .list, etc.). **By default, output files are written to the current working directory**.
|
||||
- **the `-o <file>` switch names the exact final program artifact** (also spelled `--output`); all other output files go in the same directory with the same stem. Cannot be combined with `-out` or with multiple source files.
|
||||
- **Other useful flags**: `-quiet` (suppress messages), `-warnimplicitcasts` (warn on implicit type widening), `-daemon` (keep a background compiler process alive — must be passed on every invocation)
|
||||
|
||||
### Compilation Output Files
|
||||
Names follow the source file name, or the stem of the `-o` file when that option is used.
|
||||
- `*.prg` - The final compiled program file for the target system (e.g., Commander X16)
|
||||
- `*.asm` - Generated assembly code from the Prog8 source
|
||||
- `*.list` - Generated full assembly listing file from the Prog8 source
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
;
|
||||
; As the X16 computer is a more or less fixed system, it's not very useful
|
||||
; to benchmark the computer itself with.
|
||||
;
|
||||
; The individual scores are iteration counts, and they differ by more than an
|
||||
; order of magnitude between benchmarks (maze does ~3000, game of life ~180).
|
||||
; Summing the raw counts therefore let one or two benchmarks dominate the total
|
||||
; and hide regressions in all the others, so the total is a weighted sum - see
|
||||
; benchmark_weight below.
|
||||
|
||||
|
||||
%import textio
|
||||
@@ -26,7 +32,15 @@ main {
|
||||
|
||||
str[20] benchmark_names
|
||||
uword[20] benchmark_score
|
||||
str version = "12.1"
|
||||
; Weight per benchmark, in the same order as the benchmarks are run below. The total is the
|
||||
; weighted sum of the raw iteration counts.
|
||||
; The raw counts differ by more than an order of magnitude (maze ~3064, game of life ~180), so
|
||||
; in an unweighted sum the two heaviest - maze solver and text-elite - were 57% of the total
|
||||
; while game of life was 2%. A regression in a small benchmark was therefore nearly invisible.
|
||||
; Their weights are scaled down so that every benchmark contributes roughly comparably; the
|
||||
; other eight are left at 256 (i.e. unweighted).
|
||||
uword[10] benchmark_weight = [ 64, 256, 256, 256, 256, 256, 256, 64, 256, 256 ]
|
||||
str version = "14.0"
|
||||
|
||||
|
||||
sub start() {
|
||||
@@ -96,11 +110,14 @@ main {
|
||||
txt.print(")\n\n")
|
||||
|
||||
do {
|
||||
; Weighted contribution. Done in long arithmetic on purpose: score*weight overflows a
|
||||
; uword (3064*256 is 784384, which wraps to 63488 and would corrupt the total).
|
||||
uword weighted = ((benchmark_score[benchmark_number] as long) * benchmark_weight[benchmark_number] / 256) as uword
|
||||
txt.spc()
|
||||
txt.print_uw(benchmark_score[benchmark_number])
|
||||
txt.print_uw(weighted)
|
||||
txt.column(6)
|
||||
txt.print(benchmark_names[benchmark_number])
|
||||
total_score += benchmark_score[benchmark_number]
|
||||
total_score += weighted
|
||||
txt.nl()
|
||||
benchmark_number++
|
||||
} until benchmark_names[benchmark_number]==0
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package prog8.code.assembly
|
||||
|
||||
import prog8.code.GENERATED_LABEL_PREFIX
|
||||
import prog8.code.core.CompilationOptions
|
||||
import prog8.code.core.ICompilationTarget
|
||||
import prog8.code.core.IErrorReporter
|
||||
import prog8.code.core.OutputType
|
||||
import prog8.code.core.*
|
||||
import prog8.code.target.C128Target
|
||||
import prog8.code.target.C64Target
|
||||
import prog8.code.target.PETTarget
|
||||
@@ -13,25 +10,20 @@ import java.nio.file.Path
|
||||
|
||||
class AssemblyProgram6502(
|
||||
override val name: String,
|
||||
outputDir: Path,
|
||||
private val options: CompilationOptions,
|
||||
private val compTarget: ICompilationTarget) : IAssemblyProgram {
|
||||
|
||||
override val irInstructionCount: Int = 0
|
||||
override val irChunkCount: Int = 0
|
||||
override val irRegisterCount: Int = 0
|
||||
|
||||
private val assemblyFile = outputDir.resolve("$name.asm")
|
||||
private val prgFile = outputDir.resolve("$name.prg") // CBM prg executable program
|
||||
private val xexFile = outputDir.resolve("$name.xex") // Atari xex executable program
|
||||
private val binFile = outputDir.resolve("$name.bin")
|
||||
private val viceMonListFile = outputDir.resolve(C64Target.viceMonListName(name))
|
||||
private val listFile = outputDir.resolve("$name.list")
|
||||
|
||||
override fun assemble(options: CompilationOptions, errors: IErrorReporter): Boolean {
|
||||
|
||||
val assemblerCommand: List<String>
|
||||
val outs = OutputFiles.of(options, name)
|
||||
|
||||
fun addRemainingOptions(command: MutableList<String>, program: Path, assembly: Path): List<String> {
|
||||
// keep additionalAssemblerOptions BEFORE --output: a target config may contain its own --output
|
||||
if(options.compTarget.additionalAssemblerOptions.isNotEmpty())
|
||||
command.addAll(options.compTarget.additionalAssemblerOptions)
|
||||
|
||||
@@ -44,7 +36,7 @@ class AssemblyProgram6502(
|
||||
// CBM machines .prg generation.
|
||||
|
||||
val command = mutableListOf("64tass", "--cbm-prg", "--ascii", "--case-sensitive", "--long-branch",
|
||||
"-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=$viceMonListFile")
|
||||
"-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=${outs.viceMonList()}")
|
||||
|
||||
if(options.warnSymbolShadowing)
|
||||
command.add("-Wshadow")
|
||||
@@ -55,10 +47,10 @@ class AssemblyProgram6502(
|
||||
command.add("--quiet")
|
||||
|
||||
if(options.asmListfile) {
|
||||
command.add("--list=$listFile")
|
||||
command.add("--list=${outs.list()}")
|
||||
}
|
||||
|
||||
assemblerCommand = addRemainingOptions(command, prgFile, assemblyFile)
|
||||
assemblerCommand = addRemainingOptions(command, outs.programFile, outs.asm())
|
||||
if(!options.quiet)
|
||||
println("\nCreating prg for target ${compTarget.name}.")
|
||||
}
|
||||
@@ -66,7 +58,7 @@ class AssemblyProgram6502(
|
||||
// Atari800XL .xex generation.
|
||||
|
||||
val command = mutableListOf("64tass", "--atari-xex", "--case-sensitive", "--long-branch",
|
||||
"-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=$viceMonListFile")
|
||||
"-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=${outs.viceMonList()}")
|
||||
|
||||
if(options.warnSymbolShadowing)
|
||||
command.add("-Wshadow")
|
||||
@@ -77,16 +69,16 @@ class AssemblyProgram6502(
|
||||
command.add("--quiet")
|
||||
|
||||
if(options.asmListfile)
|
||||
command.add("--list=$listFile")
|
||||
command.add("--list=${outs.list()}")
|
||||
|
||||
assemblerCommand = addRemainingOptions(command,xexFile, assemblyFile)
|
||||
assemblerCommand = addRemainingOptions(command, outs.programFile, outs.asm())
|
||||
if(!options.quiet)
|
||||
println("\nCreating xex for target ${compTarget.name}.")
|
||||
}
|
||||
OutputType.RAW -> {
|
||||
// Neo6502/headerless raw program generation.
|
||||
val command = mutableListOf("64tass", "--nostart", "--case-sensitive", "--long-branch",
|
||||
"-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=$viceMonListFile")
|
||||
"-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=${outs.viceMonList()}")
|
||||
|
||||
if(options.warnSymbolShadowing)
|
||||
command.add("-Wshadow")
|
||||
@@ -97,9 +89,9 @@ class AssemblyProgram6502(
|
||||
command.add("--quiet")
|
||||
|
||||
if(options.asmListfile)
|
||||
command.add("--list=$listFile")
|
||||
command.add("--list=${outs.list()}")
|
||||
|
||||
assemblerCommand = addRemainingOptions(command, binFile, assemblyFile)
|
||||
assemblerCommand = addRemainingOptions(command, outs.programFile, outs.asm())
|
||||
if(!options.quiet)
|
||||
println("\nCreating raw binary for target ${compTarget.name}.")
|
||||
}
|
||||
@@ -107,7 +99,7 @@ class AssemblyProgram6502(
|
||||
// CBM machines library (.bin) generation (with or without 2 byte load address header depending on the compilation target machine)
|
||||
|
||||
val command = mutableListOf("64tass", "--ascii", "--case-sensitive", "--long-branch",
|
||||
"-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=$viceMonListFile")
|
||||
"-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=${outs.viceMonList()}")
|
||||
|
||||
if(options.warnSymbolShadowing)
|
||||
command.add("-Wshadow")
|
||||
@@ -118,7 +110,7 @@ class AssemblyProgram6502(
|
||||
command.add("--quiet")
|
||||
|
||||
if(options.asmListfile)
|
||||
command.add("--list=$listFile")
|
||||
command.add("--list=${outs.list()}")
|
||||
|
||||
if(compTarget.name in listOf(C64Target.NAME, C128Target.NAME, PETTarget.NAME)) {
|
||||
if(!options.quiet)
|
||||
@@ -130,7 +122,7 @@ class AssemblyProgram6502(
|
||||
command.add("--nostart") // should be headerless bin, because basic has problems doing a normal LOAD"lib",8,1 - need to use BLOAD
|
||||
}
|
||||
|
||||
assemblerCommand = addRemainingOptions(command, binFile, assemblyFile)
|
||||
assemblerCommand = addRemainingOptions(command, outs.programFile, outs.asm())
|
||||
}
|
||||
else -> error("Unsupported output type: ${compTarget.defaultOutputType}")
|
||||
}
|
||||
@@ -159,16 +151,16 @@ class AssemblyProgram6502(
|
||||
|
||||
val result = process.waitFor()
|
||||
if (result == 0) {
|
||||
removeGeneratedLabelsFromMonlist()
|
||||
generateBreakpointList()
|
||||
removeGeneratedLabelsFromMonlist(outs)
|
||||
generateBreakpointList(outs)
|
||||
}
|
||||
return result==0
|
||||
}
|
||||
|
||||
private fun removeGeneratedLabelsFromMonlist() {
|
||||
private fun removeGeneratedLabelsFromMonlist(outs: OutputFiles) {
|
||||
val pattern = Regex("""al (\w+) \S+$GENERATED_LABEL_PREFIX.+?""")
|
||||
val lines = viceMonListFile.toFile().readLines()
|
||||
viceMonListFile.toFile().outputStream().bufferedWriter().use {
|
||||
val lines = outs.viceMonList().toFile().readLines()
|
||||
outs.viceMonList().toFile().outputStream().bufferedWriter().use {
|
||||
for (line in lines) {
|
||||
if(pattern.matchEntire(line)==null)
|
||||
it.write(line+"\n")
|
||||
@@ -176,11 +168,11 @@ class AssemblyProgram6502(
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateBreakpointList() {
|
||||
private fun generateBreakpointList(outs: OutputFiles) {
|
||||
// builds list of breakpoints, appends to monitor list file
|
||||
val breakpoints = mutableListOf<String>()
|
||||
val pattern = Regex("""al (\w+) \S+_prog8_breakpoint_\d+.?""") // gather breakpoints by the source label that's generated for them
|
||||
for (line in viceMonListFile.toFile().readLines()) {
|
||||
for (line in outs.viceMonList().toFile().readLines()) {
|
||||
val match = pattern.matchEntire(line)
|
||||
if (match != null)
|
||||
breakpoints.add("break $" + match.groupValues[1])
|
||||
@@ -189,7 +181,7 @@ class AssemblyProgram6502(
|
||||
breakpoints.add(0, "; breakpoint list now follows")
|
||||
breakpoints.add(1, "; $num breakpoints have been defined")
|
||||
breakpoints.add(2, "del")
|
||||
viceMonListFile.toFile().appendText(breakpoints.joinToString("\n") + "\n")
|
||||
outs.viceMonList().toFile().appendText(breakpoints.joinToString("\n") + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ class CallConvention(val params: List<ParamConvention>, val returns: ReturnConve
|
||||
class FParam(val name: String, vararg val possibleDatatypes: BaseDataType)
|
||||
|
||||
|
||||
private val IterableDatatypes = arrayOf(BaseDataType.STR, BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW, BaseDataType.ARRAY_POINTER)
|
||||
private val IterableDatatypes = arrayOf(BaseDataType.STR, BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW)
|
||||
private val IntegerDatatypes = arrayOf(BaseDataType.UBYTE, BaseDataType.BYTE, BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.LONG)
|
||||
private val NumericDatatypes = arrayOf(BaseDataType.UBYTE, BaseDataType.BYTE, BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.LONG, BaseDataType.FLOAT)
|
||||
|
||||
|
||||
@@ -36,7 +36,8 @@ class CompilationOptions(val output: OutputType,
|
||||
var quiet: Boolean = false,
|
||||
var profilingInstrumentation: Boolean = false,
|
||||
var symbolDefs: Map<String, String> = emptyMap(),
|
||||
var privateSymbols: Boolean = false
|
||||
var privateSymbols: Boolean = false,
|
||||
var outputFile: Path? = null
|
||||
) {
|
||||
init {
|
||||
compTarget.initializeMemoryAreas(this)
|
||||
@@ -77,6 +78,7 @@ class CompilationOptions(val output: OutputType,
|
||||
private var profilingInstrumentation: Boolean = false
|
||||
private var newCodegen: Boolean = false
|
||||
private var symbolDefs: Map<String, String> = emptyMap()
|
||||
private var outputFile: Path? = null
|
||||
|
||||
fun output(output: OutputType) = apply { this.output = output }
|
||||
fun launcher(launcher: CbmPrgLauncherType) = apply { this.launcher = launcher }
|
||||
@@ -107,6 +109,7 @@ class CompilationOptions(val output: OutputType,
|
||||
fun profilingInstrumentation(profilingInstrumentation: Boolean) = apply { this.profilingInstrumentation = profilingInstrumentation }
|
||||
fun symbolDefs(symbolDefs: Map<String, String>) = apply { this.symbolDefs = symbolDefs }
|
||||
fun newCodegen(newCodegen: Boolean) = apply { this.newCodegen = newCodegen }
|
||||
fun outputFile(outputFile: Path?) = apply { this.outputFile = outputFile }
|
||||
|
||||
fun build(): CompilationOptions {
|
||||
return CompilationOptions(
|
||||
@@ -115,7 +118,7 @@ class CompilationOptions(val output: OutputType,
|
||||
includeSourcelines, dumpVariables, dumpSymbols, newCodegen, varsHighBank, varsGolden,
|
||||
breakpointCpuInstruction, ignoreFootguns, outputDir, quiet,
|
||||
profilingInstrumentation, symbolDefs
|
||||
)
|
||||
).apply { this.outputFile = this@Builder.outputFile }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package prog8.code.core
|
||||
|
||||
import java.util.*
|
||||
import java.util.Objects
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
@@ -20,7 +22,6 @@ enum class BaseDataType {
|
||||
ARRAY_SPLITW, // pass by reference, split word layout, subtype is the element type (restricted to word types)
|
||||
POINTER, // typed pointer, subtype is whatever type is pointed to
|
||||
STRUCT_INSTANCE, // the actual instance of a struct (not directly supported in the language yet, but we need its type)
|
||||
ARRAY_POINTER, // array of pointers (uwords or longs), subtype is whatever type each element points to
|
||||
UNDEFINED;
|
||||
|
||||
|
||||
@@ -91,11 +92,10 @@ val BaseDataType.isSigned get() = this in setOf(BaseDataType.BYTE, BaseDataType.
|
||||
val BaseDataType.isUnsigned get() = !this.isSigned
|
||||
val BaseDataType.isSignedInteger get() = this.isSigned && this.isInteger
|
||||
val BaseDataType.isUnsignedInteger get() = this.isUnsigned && this.isInteger
|
||||
val BaseDataType.isArray get() = this == BaseDataType.ARRAY || this == BaseDataType.ARRAY_SPLITW || this == BaseDataType.ARRAY_POINTER
|
||||
val BaseDataType.isArray get() = this == BaseDataType.ARRAY || this == BaseDataType.ARRAY_SPLITW
|
||||
val BaseDataType.isPointer get() = this == BaseDataType.POINTER
|
||||
val BaseDataType.isStructInstance get() = this == BaseDataType.STRUCT_INSTANCE
|
||||
val BaseDataType.isPointerArray get() = this == BaseDataType.ARRAY_POINTER
|
||||
val BaseDataType.isIterable get() = this in setOf(BaseDataType.STR, BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW, BaseDataType.ARRAY_POINTER)
|
||||
val BaseDataType.isIterable get() = this in setOf(BaseDataType.STR, BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW)
|
||||
val BaseDataType.isPassByRef get() = this.isIterable && !this.isPointer
|
||||
val BaseDataType.isPassByValue get() = !this.isIterable || this.isPointer
|
||||
|
||||
@@ -126,14 +126,15 @@ interface ISubType {
|
||||
class DataType private constructor(
|
||||
val base: BaseDataType,
|
||||
val sub: BaseDataType?,
|
||||
val pointeeSub: BaseDataType?,
|
||||
var subType: ISubType?,
|
||||
var subTypeFromAntlr: List<String>? = null
|
||||
) {
|
||||
|
||||
init {
|
||||
when {
|
||||
base.isPointerArray -> {
|
||||
require(sub!=null || subType!=null || subTypeFromAntlr!=null)
|
||||
base.isArray && sub == BaseDataType.POINTER -> {
|
||||
require(pointeeSub != null || subType != null || subTypeFromAntlr != null)
|
||||
}
|
||||
base.isArray -> {
|
||||
require(sub != null)
|
||||
@@ -155,7 +156,7 @@ class DataType private constructor(
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is DataType) return false
|
||||
if (base != other.base || sub != other.sub) return false
|
||||
if (base != other.base || sub != other.sub || pointeeSub != other.pointeeSub) return false
|
||||
val st1 = subType
|
||||
val st2 = other.subType
|
||||
if (st1 == st2) return true
|
||||
@@ -163,7 +164,7 @@ class DataType private constructor(
|
||||
return st1.sameas(st2)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = Objects.hash(base, sub, subType)
|
||||
override fun hashCode(): Int = Objects.hash(base, sub, pointeeSub, subType)
|
||||
|
||||
fun setActualSubType(actualSubType: ISubType) {
|
||||
subType = actualSubType
|
||||
@@ -176,73 +177,72 @@ class DataType private constructor(
|
||||
|
||||
companion object {
|
||||
|
||||
val UBYTE = DataType(BaseDataType.UBYTE, null, null)
|
||||
val BYTE = DataType(BaseDataType.BYTE, null, null)
|
||||
val UWORD = DataType(BaseDataType.UWORD, null, null)
|
||||
val WORD = DataType(BaseDataType.WORD, null, null)
|
||||
val LONG = DataType(BaseDataType.LONG, null, null)
|
||||
val FLOAT = DataType(BaseDataType.FLOAT, null, null)
|
||||
val BOOL = DataType(BaseDataType.BOOL, null, null)
|
||||
val STR = DataType(BaseDataType.STR, BaseDataType.UBYTE, null)
|
||||
val UNDEFINED = DataType(BaseDataType.UNDEFINED, null, null)
|
||||
val UBYTE = DataType(BaseDataType.UBYTE, null, null, null)
|
||||
val BYTE = DataType(BaseDataType.BYTE, null, null, null)
|
||||
val UWORD = DataType(BaseDataType.UWORD, null, null, null)
|
||||
val WORD = DataType(BaseDataType.WORD, null, null, null)
|
||||
val LONG = DataType(BaseDataType.LONG, null, null, null)
|
||||
val FLOAT = DataType(BaseDataType.FLOAT, null, null, null)
|
||||
val BOOL = DataType(BaseDataType.BOOL, null, null, null)
|
||||
val STR = DataType(BaseDataType.STR, BaseDataType.UBYTE, null, null)
|
||||
val UNDEFINED = DataType(BaseDataType.UNDEFINED, null, null, null)
|
||||
|
||||
private val simpletypes = mapOf(
|
||||
BaseDataType.UBYTE to DataType(BaseDataType.UBYTE, null, null),
|
||||
BaseDataType.BYTE to DataType(BaseDataType.BYTE, null, null),
|
||||
BaseDataType.UWORD to DataType(BaseDataType.UWORD, null, null),
|
||||
BaseDataType.WORD to DataType(BaseDataType.WORD, null, null),
|
||||
BaseDataType.LONG to DataType(BaseDataType.LONG, null, null),
|
||||
BaseDataType.FLOAT to DataType(BaseDataType.FLOAT, null, null),
|
||||
BaseDataType.BOOL to DataType(BaseDataType.BOOL, null, null),
|
||||
BaseDataType.STR to DataType(BaseDataType.STR, BaseDataType.UBYTE, null),
|
||||
BaseDataType.POINTER to DataType(BaseDataType.POINTER, null, null),
|
||||
BaseDataType.UNDEFINED to DataType(BaseDataType.UNDEFINED, null, null)
|
||||
BaseDataType.UBYTE to DataType(BaseDataType.UBYTE, null, null, null),
|
||||
BaseDataType.BYTE to DataType(BaseDataType.BYTE, null, null, null),
|
||||
BaseDataType.UWORD to DataType(BaseDataType.UWORD, null, null, null),
|
||||
BaseDataType.WORD to DataType(BaseDataType.WORD, null, null, null),
|
||||
BaseDataType.LONG to DataType(BaseDataType.LONG, null, null, null),
|
||||
BaseDataType.FLOAT to DataType(BaseDataType.FLOAT, null, null, null),
|
||||
BaseDataType.BOOL to DataType(BaseDataType.BOOL, null, null, null),
|
||||
BaseDataType.STR to DataType(BaseDataType.STR, BaseDataType.UBYTE, null, null),
|
||||
BaseDataType.POINTER to DataType(BaseDataType.POINTER, null, null, null),
|
||||
BaseDataType.UNDEFINED to DataType(BaseDataType.UNDEFINED, null, null, null)
|
||||
)
|
||||
|
||||
fun forDt(dt: BaseDataType): DataType {
|
||||
if(dt.isStructInstance)
|
||||
return DataType(BaseDataType.STRUCT_INSTANCE, null, null)
|
||||
return DataType(BaseDataType.STRUCT_INSTANCE, null, null, null)
|
||||
return simpletypes.getValue(dt)
|
||||
}
|
||||
|
||||
fun arrayFor(elementDt: BaseDataType, memsizer: IMemSizer): DataType {
|
||||
// wether or not a word-array should be split-words, is determined later
|
||||
require(!elementDt.isPointer) { "use other array constructor for arrays of pointers" }
|
||||
// On 32-bit targets (m68k, virtual), str arrays become arrays of LONG (4-byte pointers)
|
||||
// via pointerBaseType. On 6502 targets they become arrays of UWORD (2-byte pointers).
|
||||
require(elementDt != BaseDataType.POINTER) { "use arrayOfPointersTo for arrays of pointers" }
|
||||
val actualElementDt = if(elementDt==BaseDataType.STR) {
|
||||
memsizer.pointerBaseType
|
||||
} else elementDt
|
||||
if(actualElementDt.isNumericOrBool)
|
||||
return DataType(BaseDataType.ARRAY, actualElementDt, null)
|
||||
return DataType(BaseDataType.ARRAY, actualElementDt, null, null)
|
||||
else
|
||||
throw NoSuchElementException("invalid basic element dt $elementDt")
|
||||
}
|
||||
|
||||
fun splitWordArrayFor(elementDt: BaseDataType): DataType {
|
||||
require(elementDt.isWord) { "split word array element type must be word" }
|
||||
return DataType(BaseDataType.ARRAY_SPLITW, elementDt, null)
|
||||
return DataType(BaseDataType.ARRAY_SPLITW, elementDt, null, null)
|
||||
}
|
||||
|
||||
fun arrayOfPointersTo(sub: BaseDataType): DataType = DataType(BaseDataType.ARRAY_POINTER, sub, null)
|
||||
fun arrayOfPointersTo(structType: ISubType?): DataType = DataType(BaseDataType.ARRAY_POINTER, null, structType)
|
||||
fun arrayOfPointersTo(sub: BaseDataType): DataType =
|
||||
DataType(BaseDataType.ARRAY, BaseDataType.POINTER, sub, null)
|
||||
fun arrayOfPointersTo(structType: ISubType?): DataType =
|
||||
DataType(BaseDataType.ARRAY, BaseDataType.POINTER, BaseDataType.STRUCT_INSTANCE, structType)
|
||||
fun arrayOfPointersFromAntlrTo(sub: BaseDataType?, identifier: List<String>?): DataType =
|
||||
DataType(BaseDataType.ARRAY_POINTER, sub, null, identifier)
|
||||
DataType(BaseDataType.ARRAY, BaseDataType.POINTER, sub ?: BaseDataType.STRUCT_INSTANCE, null, identifier)
|
||||
|
||||
// array of struct instances: sub is STRUCT_INSTANCE, subType carries the resolved struct
|
||||
fun arrayOfStructs(structType: ISubType): DataType = DataType(BaseDataType.ARRAY, BaseDataType.STRUCT_INSTANCE, structType)
|
||||
fun arrayOfStructs(structType: ISubType): DataType = DataType(BaseDataType.ARRAY, BaseDataType.STRUCT_INSTANCE, null, structType)
|
||||
fun arrayOfStructsFromAntlr(struct: List<String>): DataType =
|
||||
DataType(BaseDataType.ARRAY, BaseDataType.STRUCT_INSTANCE, null, subTypeFromAntlr = struct)
|
||||
DataType(BaseDataType.ARRAY, BaseDataType.STRUCT_INSTANCE, null, null, subTypeFromAntlr = struct)
|
||||
|
||||
fun pointer(base: BaseDataType): DataType = DataType(BaseDataType.POINTER, base, null)
|
||||
fun pointer(base: BaseDataType): DataType = DataType(BaseDataType.POINTER, base, null, null)
|
||||
fun pointer(dt: DataType): DataType = if(dt.isBasic)
|
||||
DataType(BaseDataType.POINTER, dt.base, null)
|
||||
DataType(BaseDataType.POINTER, dt.base, null, null)
|
||||
else
|
||||
DataType(BaseDataType.POINTER, null, dt.subType, dt.subTypeFromAntlr)
|
||||
fun pointer(structType: ISubType): DataType = DataType(BaseDataType.POINTER, null, structType)
|
||||
fun pointerFromAntlr(identifier: List<String>): DataType = DataType(BaseDataType.POINTER, null, null, identifier)
|
||||
fun structInstance(type: ISubType?): DataType = DataType(BaseDataType.STRUCT_INSTANCE, sub=null, type)
|
||||
fun structInstanceFromAntlr(struct: List<String>): DataType = DataType(BaseDataType.STRUCT_INSTANCE, null, null, subTypeFromAntlr = struct)
|
||||
DataType(BaseDataType.POINTER, null, null, dt.subType, dt.subTypeFromAntlr)
|
||||
fun pointer(structType: ISubType): DataType = DataType(BaseDataType.POINTER, null, null, structType)
|
||||
fun pointerFromAntlr(identifier: List<String>): DataType = DataType(BaseDataType.POINTER, null, null, null, identifier)
|
||||
fun structInstance(type: ISubType?): DataType = DataType(BaseDataType.STRUCT_INSTANCE, sub=null, pointeeSub=null, type)
|
||||
fun structInstanceFromAntlr(struct: List<String>): DataType = DataType(BaseDataType.STRUCT_INSTANCE, null, null, null, subTypeFromAntlr = struct)
|
||||
}
|
||||
|
||||
|
||||
@@ -267,29 +267,30 @@ class DataType private constructor(
|
||||
arrayOfStructsFromAntlr(subTypeFromAntlr!!)
|
||||
}
|
||||
|
||||
if(!target.cpu.is6502) {
|
||||
return if (base.isPointer)
|
||||
pointerArray()
|
||||
else
|
||||
arrayFor(base, target)
|
||||
}
|
||||
if(base.isPointer)
|
||||
return pointerArray()
|
||||
|
||||
if(!target.cpu.is6502)
|
||||
return arrayFor(base, target)
|
||||
|
||||
return if (splitwords && (base == BaseDataType.UWORD || base == BaseDataType.WORD || base == BaseDataType.STR))
|
||||
splitWordArrayFor(base)
|
||||
else if(base.isPointer)
|
||||
pointerArray()
|
||||
else
|
||||
arrayFor(base, target)
|
||||
}
|
||||
|
||||
fun elementType(): DataType =
|
||||
when {
|
||||
isPointerArray -> DataType(BaseDataType.POINTER, sub, subType)
|
||||
isPointerArray -> {
|
||||
if (subType != null) DataType(BaseDataType.POINTER, null, null, subType)
|
||||
else if (subTypeFromAntlr != null) DataType(BaseDataType.POINTER, null, null, null, subTypeFromAntlr)
|
||||
else DataType(BaseDataType.POINTER, pointeeSub!!, null, null)
|
||||
}
|
||||
base.isArray || base==BaseDataType.STR -> {
|
||||
if (sub == BaseDataType.STRUCT_INSTANCE && subType != null)
|
||||
return DataType(BaseDataType.STRUCT_INSTANCE, null, subType)
|
||||
return DataType(BaseDataType.STRUCT_INSTANCE, null, null, subType)
|
||||
val dt = forDt(sub!!)
|
||||
if (subType != null) DataType(BaseDataType.POINTER, null, subType) else dt
|
||||
if (subType != null) DataType(BaseDataType.POINTER, null, null, subType) else dt
|
||||
}
|
||||
else -> throw IllegalArgumentException("not an array")
|
||||
}
|
||||
@@ -307,7 +308,7 @@ class DataType private constructor(
|
||||
return untypedPointerType
|
||||
if (isArray) {
|
||||
val isSplit = base == BaseDataType.ARRAY_SPLITW ||
|
||||
(base == BaseDataType.ARRAY_POINTER && mem.memorySize(BaseDataType.POINTER) <= 2)
|
||||
(isPointerArray && mem.memorySize(BaseDataType.POINTER) <= 2)
|
||||
if (msb || isSplit)
|
||||
return pointer(BaseDataType.UBYTE)
|
||||
val elementDt = elementType()
|
||||
@@ -326,8 +327,8 @@ class DataType private constructor(
|
||||
return when {
|
||||
isUnsignedWord -> forDt(BaseDataType.UBYTE)
|
||||
sub!=null -> forDt(sub)
|
||||
subType!=null -> DataType(BaseDataType.STRUCT_INSTANCE, null, subType)
|
||||
subTypeFromAntlr!=null -> DataType(BaseDataType.STRUCT_INSTANCE, null, null, subTypeFromAntlr)
|
||||
subType!=null -> DataType(BaseDataType.STRUCT_INSTANCE, null, null, subType)
|
||||
subTypeFromAntlr!=null -> DataType(BaseDataType.STRUCT_INSTANCE, null, null, null, subTypeFromAntlr)
|
||||
else -> throw IllegalArgumentException("cannot dereference this pointer type")
|
||||
}
|
||||
}
|
||||
@@ -342,6 +343,12 @@ class DataType private constructor(
|
||||
BaseDataType.UBYTE -> "ubyte[]"
|
||||
BaseDataType.UWORD -> "uword[]"
|
||||
BaseDataType.LONG -> "long[]"
|
||||
BaseDataType.POINTER -> {
|
||||
val pointee = if (subType != null) subType!!.scopedNameString
|
||||
else if (subTypeFromAntlr != null) "$subTypeFromAntlr"
|
||||
else pointeeSub!!.name.lowercase()
|
||||
"^^$pointee[]"
|
||||
}
|
||||
BaseDataType.STRUCT_INSTANCE -> if (subType != null) "${subType!!.scopedNameString}[]" else "$subTypeFromAntlr[]"
|
||||
else -> throw IllegalArgumentException("invalid sub type")
|
||||
}
|
||||
@@ -356,9 +363,6 @@ class DataType private constructor(
|
||||
BaseDataType.POINTER -> {
|
||||
if(sub!=null) "^^${sub.name.lowercase()}" else if(subType!=null) "^^${subType!!.scopedNameString}" else "^^${subTypeFromAntlr}"
|
||||
}
|
||||
BaseDataType.ARRAY_POINTER -> {
|
||||
if(sub!=null) "^^${sub.name.lowercase()}[] (split)" else if (subType!=null) "^^${subType!!.scopedNameString}[] (split)" else "^^${subTypeFromAntlr}[] (split)"
|
||||
}
|
||||
BaseDataType.STRUCT_INSTANCE -> {
|
||||
sub?.name?.lowercase() ?: if (subType!=null) subType!!.scopedNameString else "$subTypeFromAntlr"
|
||||
}
|
||||
@@ -390,14 +394,6 @@ class DataType private constructor(
|
||||
else -> "?????"
|
||||
}
|
||||
}
|
||||
BaseDataType.ARRAY_POINTER -> {
|
||||
when {
|
||||
sub!=null -> "^^${sub.name.lowercase()}["
|
||||
subType!=null -> "^^${subType!!.scopedNameString}["
|
||||
subTypeFromAntlr!=null -> "^^${subTypeFromAntlr!!.joinToString(".")}["
|
||||
else -> "????? ["
|
||||
}
|
||||
}
|
||||
BaseDataType.ARRAY -> {
|
||||
when(sub) {
|
||||
BaseDataType.UBYTE -> "ubyte["
|
||||
@@ -407,6 +403,12 @@ class DataType private constructor(
|
||||
BaseDataType.WORD -> "@nosplit word["
|
||||
BaseDataType.LONG -> "long["
|
||||
BaseDataType.FLOAT -> "float["
|
||||
BaseDataType.POINTER -> {
|
||||
val pointee = if (subType != null) "^^${subType!!.scopedNameString}["
|
||||
else if (subTypeFromAntlr != null) "^^${subTypeFromAntlr!!.joinToString(".")}["
|
||||
else "^^${pointeeSub!!.name.lowercase()}["
|
||||
pointee
|
||||
}
|
||||
BaseDataType.STRUCT_INSTANCE -> if (subType != null) "${subType!!.scopedNameString}[" else "$subTypeFromAntlr["
|
||||
else -> throw IllegalArgumentException("invalid sub type")
|
||||
}
|
||||
@@ -430,12 +432,21 @@ class DataType private constructor(
|
||||
BaseDataType.BOOL -> targetType.base == BaseDataType.BOOL
|
||||
BaseDataType.UBYTE -> targetType.base in setOf(BaseDataType.UBYTE, BaseDataType.WORD, BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.FLOAT)
|
||||
BaseDataType.BYTE -> targetType.base in setOf(BaseDataType.BYTE, BaseDataType.WORD, BaseDataType.LONG, BaseDataType.FLOAT)
|
||||
BaseDataType.UWORD -> targetType.base in setOf(BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.FLOAT, BaseDataType.POINTER, BaseDataType.ARRAY_POINTER)
|
||||
BaseDataType.UWORD -> targetType.base in setOf(BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.FLOAT, BaseDataType.POINTER) || targetType.isPointerArray
|
||||
BaseDataType.WORD -> targetType.base in setOf(BaseDataType.WORD, BaseDataType.LONG, BaseDataType.FLOAT)
|
||||
BaseDataType.LONG -> targetType.base in setOf(BaseDataType.LONG, BaseDataType.FLOAT, BaseDataType.POINTER, BaseDataType.ARRAY_POINTER)
|
||||
BaseDataType.LONG -> targetType.base in setOf(BaseDataType.LONG, BaseDataType.FLOAT, BaseDataType.POINTER) || targetType.isPointerArray
|
||||
BaseDataType.FLOAT -> targetType.base in arrayOf(BaseDataType.FLOAT)
|
||||
BaseDataType.STR -> targetType.base in setOf(BaseDataType.STR, BaseDataType.UWORD, BaseDataType.LONG) || (targetType.isPointer && targetType.sub==BaseDataType.UBYTE)
|
||||
BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW -> targetType.base in setOf(BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW) && targetType.sub == sub
|
||||
BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW -> {
|
||||
if (isPointerArray) {
|
||||
if (!targetType.isPointerArray) false
|
||||
else if (subType != null && targetType.subType != null) subType!!.sameas(targetType.subType!!)
|
||||
else if (subType == null && targetType.subType == null) pointeeSub == targetType.pointeeSub
|
||||
else false
|
||||
} else {
|
||||
targetType.base in setOf(BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW) && targetType.sub == sub
|
||||
}
|
||||
}
|
||||
BaseDataType.POINTER -> {
|
||||
when {
|
||||
targetType.base == BaseDataType.UWORD || targetType.base == BaseDataType.LONG -> true
|
||||
@@ -443,7 +454,6 @@ class DataType private constructor(
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
BaseDataType.ARRAY_POINTER -> targetType.base in setOf(BaseDataType.ARRAY_POINTER, targetType.base == BaseDataType.ARRAY) && targetType.sub == sub
|
||||
BaseDataType.STRUCT_INSTANCE -> false // we cannot deal with actual struct instances yet in any shape or form (only getting fields from it)
|
||||
BaseDataType.UNDEFINED -> false
|
||||
}
|
||||
@@ -457,13 +467,12 @@ class DataType private constructor(
|
||||
* Note: for pointer types, size() doesn't return the size of the pointer itself
|
||||
* but the size of the thing it points to.
|
||||
*/
|
||||
fun size(memsizer: IMemSizer): Int = if(sub!=null) {
|
||||
memsizer.memorySize(sub)
|
||||
} else if(subType!=null) {
|
||||
subType!!.memsize(memsizer)
|
||||
} else {
|
||||
memsizer.memorySize(base)
|
||||
}
|
||||
fun size(memsizer: IMemSizer): Int = when {
|
||||
base == BaseDataType.ARRAY && sub == BaseDataType.POINTER -> memsizer.memorySize(BaseDataType.POINTER)
|
||||
sub!=null -> memsizer.memorySize(sub)
|
||||
subType!=null -> subType!!.memsize(memsizer)
|
||||
else -> memsizer.memorySize(base)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DataType Properties
|
||||
@@ -492,16 +501,16 @@ class DataType private constructor(
|
||||
val isPointerToByte = base.isPointer && sub?.isByteOrBool==true
|
||||
val isPointerToWord = base.isPointer && sub?.isWord==true
|
||||
val isStructInstance = base.isStructInstance
|
||||
val isPointerArray = base.isPointerArray
|
||||
val isBoolArray = base.isArray && !base.isPointerArray && sub == BaseDataType.BOOL
|
||||
val isByteArray = base.isArray && !base.isPointerArray && (sub == BaseDataType.UBYTE || sub == BaseDataType.BYTE)
|
||||
val isUnsignedByteArray = base.isArray && !base.isPointerArray && sub == BaseDataType.UBYTE
|
||||
val isSignedByteArray = base.isArray && !base.isPointerArray && sub == BaseDataType.BYTE
|
||||
val isWordArray = base.isArray && !base.isPointerArray && (sub == BaseDataType.UWORD || sub == BaseDataType.WORD)
|
||||
val isUnsignedWordArray = base.isArray && !base.isPointerArray && sub == BaseDataType.UWORD
|
||||
val isSignedWordArray = base.isArray && !base.isPointerArray && sub == BaseDataType.WORD
|
||||
val isLongArray = base.isArray && sub == BaseDataType.LONG
|
||||
val isFloatArray = base.isArray && !base.isPointerArray && sub == BaseDataType.FLOAT
|
||||
val isPointerArray = base == BaseDataType.ARRAY && sub == BaseDataType.POINTER
|
||||
val isBoolArray = base == BaseDataType.ARRAY && sub == BaseDataType.BOOL
|
||||
val isByteArray = base == BaseDataType.ARRAY && (sub == BaseDataType.UBYTE || sub == BaseDataType.BYTE)
|
||||
val isUnsignedByteArray = base == BaseDataType.ARRAY && sub == BaseDataType.UBYTE
|
||||
val isSignedByteArray = base == BaseDataType.ARRAY && sub == BaseDataType.BYTE
|
||||
val isWordArray = (base == BaseDataType.ARRAY || base == BaseDataType.ARRAY_SPLITW) && (sub == BaseDataType.UWORD || sub == BaseDataType.WORD)
|
||||
val isUnsignedWordArray = (base == BaseDataType.ARRAY || base == BaseDataType.ARRAY_SPLITW) && sub == BaseDataType.UWORD
|
||||
val isSignedWordArray = (base == BaseDataType.ARRAY || base == BaseDataType.ARRAY_SPLITW) && sub == BaseDataType.WORD
|
||||
val isLongArray = base == BaseDataType.ARRAY && sub == BaseDataType.LONG
|
||||
val isFloatArray = base == BaseDataType.ARRAY && sub == BaseDataType.FLOAT
|
||||
val isString = base == BaseDataType.STR
|
||||
val isBool = base == BaseDataType.BOOL
|
||||
val isFloat = base == BaseDataType.FLOAT
|
||||
@@ -514,7 +523,7 @@ class DataType private constructor(
|
||||
fun isSplitWordArray(memsizer: IMemSizer): Boolean {
|
||||
return if(base== BaseDataType.ARRAY_SPLITW)
|
||||
true
|
||||
else if(base==BaseDataType.ARRAY_POINTER)
|
||||
else if(base==BaseDataType.ARRAY && sub==BaseDataType.POINTER)
|
||||
memsizer.POINTER_MEM_SIZE<=2u
|
||||
else false
|
||||
}
|
||||
|
||||
@@ -87,13 +87,25 @@ interface ICompilationTarget: IStringEncoding, IMemSizer {
|
||||
val defaultLauncherType: CbmPrgLauncherType
|
||||
val supportsBankedCalls: Boolean
|
||||
|
||||
/**
|
||||
* True when the code generator allocates subroutine locals and per-activation intermediate values
|
||||
* in a stack frame instead of static memory, so a subroutine on a call-graph cycle can have more
|
||||
* than one live activation. False means two live activations would share the same storage.
|
||||
*/
|
||||
val supportsStackFrames: Boolean
|
||||
|
||||
fun initializeMemoryAreas(compilerOptions: CompilationOptions)
|
||||
fun getFloatAsmBytes(num: Number): String
|
||||
|
||||
fun convertFloatToBytes(num: Double): List<UByte>
|
||||
fun convertBytesToFloat(bytes: List<UByte>): Double
|
||||
|
||||
fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean)
|
||||
/**
|
||||
* Launches an emulator to run the compiled program artifact.
|
||||
* [programFile] is the exact artifact path including its extension; launchers must use it
|
||||
* verbatim, without stripping or appending one.
|
||||
*/
|
||||
fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean)
|
||||
fun isIOAddress(address: UInt): Boolean
|
||||
|
||||
override fun encodeString(str: String, encoding: Encoding): List<UByte>
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package prog8.code.core
|
||||
|
||||
import prog8.code.target.VMTarget
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.extension
|
||||
import kotlin.io.path.name
|
||||
|
||||
|
||||
/** VICE/box16 monitor (symbol) list file name for an artifact stem. */
|
||||
fun viceMonListName(stem: String) = "$stem.vice-mon-list"
|
||||
|
||||
|
||||
/**
|
||||
* Artifact stem of a user-supplied `-o` output file name.
|
||||
* A dot at index 0 is not an extension separator: ".pgz" keeps its whole name.
|
||||
* Only applied to `-o`; the legacy program stem (source name without extension) is used verbatim.
|
||||
*/
|
||||
fun outputStemOf(name: Path): String {
|
||||
val filename = name.name
|
||||
val dotIndex = filename.lastIndexOf('.')
|
||||
return if (dotIndex <= 0) filename else filename.substring(0, dotIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
* The final program artifact path: [CompilationOptions.outputFile] verbatim when set,
|
||||
* otherwise [defaultName] plus the extension for [CompilationOptions.output], in [CompilationOptions.outputDir].
|
||||
* RAW and LIBRARY both use ".bin"; AMIGAHUNK has no extension.
|
||||
* On the virtual target the artifact is the IR file, so ".p8ir" is always enforced on -o.
|
||||
*/
|
||||
fun CompilationOptions.programFile(defaultName: String): Path {
|
||||
// VMTarget's defaultOutputType = PRG is misleading: its artifact is the .p8ir file, never PRG.
|
||||
// The VM identifies the IR by its extension, so -o is not verbatim here: ".p8ir" is enforced.
|
||||
// Only the exact lowercase extension is recognized (same rule as -vm in CompilerMain).
|
||||
if(compTarget is VMTarget) {
|
||||
val ir = outputFile ?: return outputDir.resolve("$defaultName.p8ir")
|
||||
return if (ir.extension=="p8ir") ir else ir.resolveSibling("${ir.name}.p8ir")
|
||||
}
|
||||
outputFile?.let { return it }
|
||||
val filename = when(output) {
|
||||
OutputType.PRG -> "$defaultName.prg"
|
||||
OutputType.XEX -> "$defaultName.xex"
|
||||
OutputType.RAW -> "$defaultName.bin"
|
||||
OutputType.LIBRARY -> "$defaultName.bin"
|
||||
OutputType.ELF -> "$defaultName.elf"
|
||||
OutputType.AMIGAHUNK -> defaultName
|
||||
}
|
||||
return outputDir.resolve(filename)
|
||||
}
|
||||
|
||||
/**
|
||||
* Output artifact naming.
|
||||
* Invariant: [asm] must resolve into the same directory as `CompilationOptions.outputDir`,
|
||||
* because 64tass resolves `.binary` includes relative to the generated `.asm`.
|
||||
*/
|
||||
class OutputFiles(val programFile: Path, val stem: String, val dir: Path) {
|
||||
fun asm() = dir.resolve("$stem.asm")
|
||||
fun list() = dir.resolve("$stem.list")
|
||||
fun viceMonList() = dir.resolve(viceMonListName(stem))
|
||||
fun binFile() = dir.resolve("$stem.bin")
|
||||
fun ir() = dir.resolve("$stem.p8ir")
|
||||
fun bankedCalls() = dir.resolve("$stem.bankedcalls")
|
||||
fun obj() = dir.resolve("$stem.o")
|
||||
fun linkScript() = dir.resolve("$stem.link.ld")
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Builds the output file set. Without `-o` the stem is [defaultName] verbatim
|
||||
* (outputStemOf is only applied to the `-o` name). With `-o` the stem is derived
|
||||
* from the resolved artifact name, so it also covers the virtual target appending
|
||||
* ".p8ir". [dir] is the program file's parent, or [CompilationOptions.outputDir]
|
||||
* for a bare -o filename.
|
||||
*/
|
||||
fun of(options: CompilationOptions, defaultName: String): OutputFiles {
|
||||
val programFile = options.programFile(defaultName)
|
||||
val stem = options.outputFile?.let { outputStemOf(programFile) } ?: defaultName
|
||||
val dir = programFile.parent ?: options.outputDir
|
||||
return OutputFiles(programFile, stem, dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package prog8.code.cpu6502
|
||||
|
||||
// Horner ("shift-add") expansion of a multiply by a small compile-time constant, for the 6502 and
|
||||
// 65C02 backends. The runtime routines prog8_math.multiply_words / multiply_bytes cost the same
|
||||
// whatever the constant is (~442 and ~130 cycles), while this expansion costs O(bit length), so
|
||||
// for a small constant inlining wins. It generalises the hand-written prog8_math.mul_word_N /
|
||||
// mul_byte_N routines, which are themselves unrolled Horner chains (mul_word_3 is "AY = AY*2 + AY",
|
||||
// one Horner step for 11).
|
||||
//
|
||||
// The sequences live here rather than in either backend because the register bookkeeping is easy to
|
||||
// get subtly wrong and the two backends must not drift apart.
|
||||
//
|
||||
// The gate is on generated size, not on the constant's magnitude: cycles alone would argue for
|
||||
// expanding nearly every multiply and bloating every program. Budgets are the max instruction count
|
||||
// the expansion may take; the call sequence each replaces is 6 instructions.
|
||||
|
||||
private const val WORD_BUDGET = 32
|
||||
private const val BYTE_BUDGET = 24
|
||||
|
||||
// --- byte ---
|
||||
// Contract: A holds the multiplicand. Result in A. Only A and one scratch byte are live, so there
|
||||
// is no second register to keep in sync (unlike the word case, where a bare doubling leaves the
|
||||
// high byte stale in Y).
|
||||
|
||||
fun byteShiftAddExpansion(value: Int): List<String>? {
|
||||
if (value < 3 || value > 255)
|
||||
return null
|
||||
val bitlen = 31 - Integer.numberOfLeadingZeros(value)
|
||||
val popcount = Integer.bitCount(value)
|
||||
if (1 + (bitlen-1) + 2*(popcount-1) > BYTE_BUDGET)
|
||||
return null
|
||||
val result = ArrayList<String>(1 + 3*bitlen)
|
||||
result.add("sta P8ZP_SCRATCH_REG") // keep x for the conditional adds
|
||||
for (bit in (bitlen-1) downTo 0) {
|
||||
result.add("asl a") // r = r*2
|
||||
if (value and (1 shl bit) != 0) { // r += x
|
||||
result.add("clc")
|
||||
result.add("adc P8ZP_SCRATCH_REG")
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// --- word ---
|
||||
// Contract on entry: A = r.lo, Y = r.hi, P8ZP_SCRATCH_W1 = r, P8ZP_SCRATCH_W2 = x (all 16-bit,
|
||||
// little-endian: byte 0 is the low byte). On exit: A = result.lo, Y = result.hi.
|
||||
//
|
||||
// Two things this sequence must get right, both learned the hard way:
|
||||
// * the add step must store the new high byte back to W1+1 as well as to Y. Writing it only to Y
|
||||
// leaves W1+1 stale, and the next step's 'rol P8ZP_SCRATCH_W1+1' would then double the wrong
|
||||
// high byte.
|
||||
// * a trailing bare doubling (any even constant) updates A and W1+1 but not Y, so Y needs an
|
||||
// explicit reload at the end. It cannot be 'tay', because at that point A holds the low byte.
|
||||
|
||||
fun wordShiftAddExpansion(value: Int): List<String>? {
|
||||
if (value < 3)
|
||||
return null
|
||||
val bitlen = 31 - Integer.numberOfLeadingZeros(value)
|
||||
val popcount = Integer.bitCount(value)
|
||||
if (1 + 2*(bitlen-1) + 7*(popcount-1) > WORD_BUDGET)
|
||||
return null
|
||||
val result = ArrayList<String>(2 + 9*bitlen)
|
||||
for (bit in (bitlen-1) downTo 0) {
|
||||
result.add("asl a") // r = r*2
|
||||
result.add("rol P8ZP_SCRATCH_W1+1")
|
||||
if (value and (1 shl bit) != 0) { // r += x
|
||||
result.add("clc")
|
||||
result.add("adc P8ZP_SCRATCH_W2")
|
||||
result.add("sta P8ZP_SCRATCH_W1")
|
||||
result.add("lda P8ZP_SCRATCH_W1+1")
|
||||
result.add("adc P8ZP_SCRATCH_W2+1")
|
||||
result.add("sta P8ZP_SCRATCH_W1+1")
|
||||
result.add("tay")
|
||||
result.add("lda P8ZP_SCRATCH_W1")
|
||||
}
|
||||
}
|
||||
if (value and 1 == 0)
|
||||
result.add("ldy P8ZP_SCRATCH_W1+1")
|
||||
return result
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package prog8.code.target
|
||||
|
||||
import prog8.code.core.*
|
||||
import prog8.code.target.encodings.Encoder
|
||||
import prog8.code.target.zp.M68kZeropage
|
||||
import java.nio.file.Path
|
||||
|
||||
|
||||
@@ -11,6 +12,7 @@ class Amiga1200Target: ICompilationTarget,
|
||||
|
||||
override val name = NAME
|
||||
override val supportsBankedCalls = true
|
||||
override val supportsStackFrames = true
|
||||
override val defaultEncoding = Encoding.ISO
|
||||
override val libraryPath = null
|
||||
override val customLauncher = emptyList<String>()
|
||||
@@ -54,7 +56,7 @@ class Amiga1200Target: ICompilationTarget,
|
||||
TODO("convert bytes to float")
|
||||
}
|
||||
|
||||
override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) {
|
||||
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
|
||||
if(selectedEmulator!=1) {
|
||||
System.err.println("The amiga1200 target only supports the main emulator (Amitools's vamos).")
|
||||
return
|
||||
@@ -64,12 +66,11 @@ class Amiga1200Target: ICompilationTarget,
|
||||
CpuType.M68020 -> "68020"
|
||||
else -> error("invalid cpu type")
|
||||
}
|
||||
val exeFile = programNameWithPath.resolveSibling("${programNameWithPath.fileName}")
|
||||
val cmd =
|
||||
listOf(
|
||||
"vamos",
|
||||
"--cpu", cpuStr,
|
||||
exeFile.toString(),
|
||||
programFile.toString(),
|
||||
)
|
||||
if(!quiet)
|
||||
println("Launching Amitools's Vamos...")
|
||||
@@ -88,20 +89,6 @@ class Amiga1200Target: ICompilationTarget,
|
||||
address in 0xe80000u..0xefffffu // Zorro II autoconfig space
|
||||
|
||||
override fun initializeMemoryAreas(compilerOptions: CompilationOptions) {
|
||||
zeropage = Amiga1200Zeropage(compilerOptions)
|
||||
zeropage = M68kZeropage(compilerOptions)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class Amiga1200Zeropage(options: CompilationOptions): Zeropage(options) {
|
||||
override val SCRATCH_B1: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
override val SCRATCH_REG: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
override val SCRATCH_W1: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
override val SCRATCH_W2: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
override val SCRATCH_PTR: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package prog8.code.target
|
||||
|
||||
import prog8.code.core.*
|
||||
import prog8.code.target.encodings.Encoder
|
||||
import prog8.code.target.zp.M68kZeropage
|
||||
import java.nio.file.Path
|
||||
|
||||
|
||||
@@ -11,6 +12,7 @@ class Amiga500Target: ICompilationTarget,
|
||||
|
||||
override val name = NAME
|
||||
override val supportsBankedCalls = true
|
||||
override val supportsStackFrames = true
|
||||
override val defaultEncoding = Encoding.ISO
|
||||
override val libraryPath = null
|
||||
override val customLauncher = emptyList<String>()
|
||||
@@ -58,7 +60,10 @@ class Amiga500Target: ICompilationTarget,
|
||||
33 to "AmigaGuide",
|
||||
34 to "ARexx",
|
||||
35 to "BattClock",
|
||||
36 to "BattMem"
|
||||
36 to "BattMem",
|
||||
// from here, non-standard libraries are listed
|
||||
100 to "AsyncIO",
|
||||
101 to "AHI", // ahi.device base (amiga1200 target only, 68020+, used by the low-level ahi.* LVOs)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -91,7 +96,7 @@ class Amiga500Target: ICompilationTarget,
|
||||
TODO("convert bytes to float")
|
||||
}
|
||||
|
||||
override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) {
|
||||
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
|
||||
if(selectedEmulator!=1) {
|
||||
System.err.println("The amiga500 target only supports the main emulator (Amitools's vamos).")
|
||||
return
|
||||
@@ -101,19 +106,18 @@ class Amiga500Target: ICompilationTarget,
|
||||
CpuType.M68020 -> "68020"
|
||||
else -> error("invalid cpu type")
|
||||
}
|
||||
val exeFile = programNameWithPath.resolveSibling("${programNameWithPath.fileName}")
|
||||
val cmd =
|
||||
listOf(
|
||||
"vamos",
|
||||
"--cpu", cpuStr,
|
||||
exeFile.toString(),
|
||||
programFile.toString(),
|
||||
)
|
||||
if(!quiet)
|
||||
println("Launching Amitools's Vamos...")
|
||||
val pb = ProcessBuilder(cmd).inheritIO()
|
||||
try {
|
||||
pb.start().waitFor()
|
||||
} catch(e: Exception) {
|
||||
} catch(_: Exception) {
|
||||
System.err.println("Cannot find 'vamos' (Amitools's Amiga emulator). Install it from: https://github.com/cnvogelg/amitools")
|
||||
}
|
||||
}
|
||||
@@ -125,20 +129,6 @@ class Amiga500Target: ICompilationTarget,
|
||||
address in 0xe80000u..0xefffffu // Zorro II autoconfig space
|
||||
|
||||
override fun initializeMemoryAreas(compilerOptions: CompilationOptions) {
|
||||
zeropage = AmigaZeropage(compilerOptions)
|
||||
zeropage = M68kZeropage(compilerOptions)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class AmigaZeropage(options: CompilationOptions): Zeropage(options) {
|
||||
override val SCRATCH_B1: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
override val SCRATCH_REG: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
override val SCRATCH_W1: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
override val SCRATCH_W2: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
override val SCRATCH_PTR: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
}
|
||||
@@ -12,6 +12,7 @@ class C128Target: ICompilationTarget,
|
||||
|
||||
override val name = NAME
|
||||
override val supportsBankedCalls = true
|
||||
override val supportsStackFrames = false
|
||||
override val defaultEncoding = Encoding.PETSCII
|
||||
override val libraryPath = null
|
||||
override val customLauncher = emptyList<String>()
|
||||
@@ -28,7 +29,7 @@ class C128Target: ICompilationTarget,
|
||||
|
||||
override val FLOAT_MAX_POSITIVE = Mflpt5.FLOAT_MAX_POSITIVE
|
||||
override val FLOAT_MAX_NEGATIVE = Mflpt5.FLOAT_MAX_NEGATIVE
|
||||
override val FLOAT_MEM_SIZE = Mflpt5.FLOAT_MEM_SIZE.toUInt()
|
||||
override val FLOAT_MEM_SIZE = Mflpt5.FLOAT_MEM_SIZE
|
||||
override val POINTER_MEM_SIZE = 2u
|
||||
override val ARRAY_SIZE_LIMIT = 256u
|
||||
override val PROGRAM_LOAD_ADDRESS = 0x1c01u
|
||||
@@ -54,7 +55,7 @@ class C128Target: ICompilationTarget,
|
||||
return m5.toDouble()
|
||||
}
|
||||
|
||||
override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) {
|
||||
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
|
||||
if(selectedEmulator!=1) {
|
||||
System.err.println("The c128 target only supports the main emulator (Vice).")
|
||||
return
|
||||
@@ -63,9 +64,9 @@ class C128Target: ICompilationTarget,
|
||||
if(!quiet)
|
||||
println("\nStarting C-128 emulator x128...")
|
||||
|
||||
val viceMonlist = C64Target.viceMonListName(programNameWithPath.toString())
|
||||
val cmdline = listOf("x128", "-silent", "-moncommands", viceMonlist,
|
||||
"-autostartprgmode", "1", "-autostart-warp", "-autostart", "${programNameWithPath}.prg")
|
||||
val viceMonlist = programFile.resolveSibling(viceMonListName(outputStemOf(programFile)))
|
||||
val cmdline = listOf("x128", "-silent", "-moncommands", viceMonlist.toString(),
|
||||
"-autostartprgmode", "1", "-autostart-warp", "-autostart", programFile.toString())
|
||||
val processb = ProcessBuilder(cmdline)
|
||||
if(!quiet)
|
||||
processb.inheritIO()
|
||||
|
||||
@@ -13,6 +13,7 @@ class C64Target: ICompilationTarget,
|
||||
|
||||
override val name = NAME
|
||||
override val supportsBankedCalls = true
|
||||
override val supportsStackFrames = false
|
||||
override val defaultEncoding = Encoding.PETSCII
|
||||
override val libraryPath = null
|
||||
override val customLauncher = emptyList<String>()
|
||||
@@ -22,8 +23,6 @@ class C64Target: ICompilationTarget,
|
||||
|
||||
companion object {
|
||||
const val NAME = "c64"
|
||||
|
||||
fun viceMonListName(baseFilename: String) = "$baseFilename.vice-mon-list"
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +57,7 @@ class C64Target: ICompilationTarget,
|
||||
return m5.toDouble()
|
||||
}
|
||||
|
||||
override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) {
|
||||
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
|
||||
if(selectedEmulator!=1) {
|
||||
System.err.println("The c64 target only supports the main emulator (Vice).")
|
||||
return
|
||||
@@ -68,9 +67,9 @@ class C64Target: ICompilationTarget,
|
||||
if(!quiet)
|
||||
println("\nStarting C-64 emulator $emulator...")
|
||||
|
||||
val viceMonlist = viceMonListName(programNameWithPath.toString())
|
||||
val cmdline = listOf(emulator, "-silent", "-moncommands", viceMonlist,
|
||||
"-autostartprgmode", "1", "-autostart-warp", "-autostart", "${programNameWithPath}.prg")
|
||||
val viceMonlist = programFile.resolveSibling(viceMonListName(outputStemOf(programFile)))
|
||||
val cmdline = listOf(emulator, "-silent", "-moncommands", viceMonlist.toString(),
|
||||
"-autostartprgmode", "1", "-autostart-warp", "-autostart", programFile.toString())
|
||||
val processb = ProcessBuilder(cmdline)
|
||||
if(!quiet)
|
||||
processb.inheritIO()
|
||||
|
||||
@@ -4,6 +4,7 @@ import prog8.code.core.*
|
||||
import prog8.code.source.ImportFileSystem.expandTilde
|
||||
import prog8.code.target.encodings.Encoder
|
||||
import prog8.code.target.zp.ConfigurableZeropage
|
||||
import prog8.code.target.zp.M68kZeropage
|
||||
import java.io.IOException
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
@@ -18,6 +19,7 @@ class ConfigFileTarget(
|
||||
override val supportsBankedCalls: Boolean,
|
||||
override val defaultEncoding: Encoding,
|
||||
override val cpu: CpuType,
|
||||
override val supportsStackFrames: Boolean,
|
||||
override val PROGRAM_LOAD_ADDRESS: UInt,
|
||||
override val PROGRAM_MEMTOP_ADDRESS: UInt,
|
||||
override val BSSHIGHRAM_START: UInt,
|
||||
@@ -109,9 +111,12 @@ class ConfigFileTarget(
|
||||
CpuType.valueOf("CPU$cpuString")
|
||||
}
|
||||
val ioAddresses = parseAddressRanges("io_regions", props)
|
||||
val zpFullsafe = parseAddressRanges("zp_fullsafe", props)
|
||||
val zpKernalsafe = parseAddressRanges("zp_kernalsafe", props)
|
||||
val zpBasicsafe = parseAddressRanges("zp_basicsafe", props)
|
||||
// The m68k has no zero page, so the zeropage settings below don't apply to an m68k
|
||||
// target and are not required in its config file.
|
||||
fun zpSetting(name: String) = if(cpuType.is68k) 0u else props.getInteger(name)
|
||||
val zpFullsafe = if(cpuType.is68k) emptyList() else parseAddressRanges("zp_fullsafe", props)
|
||||
val zpKernalsafe = if(cpuType.is68k) emptyList() else parseAddressRanges("zp_kernalsafe", props)
|
||||
val zpBasicsafe = if(cpuType.is68k) emptyList() else parseAddressRanges("zp_basicsafe", props)
|
||||
|
||||
val libraryPath = expandTilde(Path(props.getString("library")))
|
||||
if(!libraryPath.isDirectory())
|
||||
@@ -140,6 +145,7 @@ class ConfigFileTarget(
|
||||
props.getProperty("supports_banked_calls", "false").toBoolean(),
|
||||
Encoding.entries.first { it.prefix==props.getString("encoding") },
|
||||
cpuType,
|
||||
props.getProperty("supports_stack_frames", cpuType.is68k.toString()).toBoolean(),
|
||||
props.getInteger("load_address"),
|
||||
props.getInteger("memtop"),
|
||||
props.getInteger("bss_highram_start"),
|
||||
@@ -153,12 +159,12 @@ class ConfigFileTarget(
|
||||
if(assemblerOptionsStr=="") emptyList() else assemblerOptionsStr.split(" "),
|
||||
varsAddress,
|
||||
ioAddresses,
|
||||
props.getInteger("zp_scratch_b1"),
|
||||
props.getInteger("zp_scratch_reg"),
|
||||
props.getInteger("zp_scratch_w1"),
|
||||
props.getInteger("zp_scratch_w2"),
|
||||
props.getInteger("zp_scratch_ptr"),
|
||||
props.getInteger("virtual_registers"),
|
||||
zpSetting("zp_scratch_b1"),
|
||||
zpSetting("zp_scratch_reg"),
|
||||
zpSetting("zp_scratch_w1"),
|
||||
zpSetting("zp_scratch_w2"),
|
||||
zpSetting("zp_scratch_ptr"),
|
||||
zpSetting("virtual_registers"),
|
||||
zpFullsafe,
|
||||
zpKernalsafe,
|
||||
zpBasicsafe,
|
||||
@@ -178,14 +184,15 @@ class ConfigFileTarget(
|
||||
override fun getFloatAsmBytes(num: Number) = TODO("floats")
|
||||
override fun convertFloatToBytes(num: Double): List<UByte> = TODO("floats")
|
||||
override fun convertBytesToFloat(bytes: List<UByte>): Double = TODO("floats")
|
||||
override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) {
|
||||
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
|
||||
throw IllegalArgumentException("Custom compiler target cannot automatically launch an emulator. Do this manually.")
|
||||
}
|
||||
|
||||
override fun isIOAddress(address: UInt): Boolean = ioAddresses.any { address in it }
|
||||
|
||||
override fun initializeMemoryAreas(compilerOptions: CompilationOptions) {
|
||||
zeropage = ConfigurableZeropage(
|
||||
// the m68k has no zero page, so the zp scratch addresses in the config file don't apply there
|
||||
zeropage = if(cpu.is68k) M68kZeropage(compilerOptions) else ConfigurableZeropage(
|
||||
zpScratchB1, zpScratchReg, zpScratchW1, zpScratchW2, zpScratchPtr,
|
||||
virtualregistersStart,
|
||||
zpBasicsafe,
|
||||
|
||||
@@ -12,6 +12,7 @@ class Cx16Target: ICompilationTarget,
|
||||
|
||||
override val name = NAME
|
||||
override val supportsBankedCalls = true
|
||||
override val supportsStackFrames = false
|
||||
override val defaultEncoding = Encoding.PETSCII
|
||||
override val libraryPath = null
|
||||
override val customLauncher = emptyList<String>()
|
||||
@@ -28,7 +29,7 @@ class Cx16Target: ICompilationTarget,
|
||||
|
||||
override val FLOAT_MAX_POSITIVE = Mflpt5.FLOAT_MAX_POSITIVE
|
||||
override val FLOAT_MAX_NEGATIVE = Mflpt5.FLOAT_MAX_NEGATIVE
|
||||
override val FLOAT_MEM_SIZE = Mflpt5.FLOAT_MEM_SIZE.toUInt()
|
||||
override val FLOAT_MEM_SIZE = Mflpt5.FLOAT_MEM_SIZE
|
||||
override val POINTER_MEM_SIZE = 2u
|
||||
override val ARRAY_SIZE_LIMIT = 256u
|
||||
override val PROGRAM_LOAD_ADDRESS = 0x0801u
|
||||
@@ -54,7 +55,7 @@ class Cx16Target: ICompilationTarget,
|
||||
return m5.toDouble()
|
||||
}
|
||||
|
||||
override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) {
|
||||
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
|
||||
val emulator: String
|
||||
val extraArgs: List<String>
|
||||
|
||||
@@ -65,7 +66,7 @@ class Cx16Target: ICompilationTarget,
|
||||
}
|
||||
2 -> {
|
||||
emulator = "box16"
|
||||
extraArgs = listOf("-sym", C64Target.viceMonListName(programNameWithPath.toString()))
|
||||
extraArgs = listOf("-sym", programFile.resolveSibling(viceMonListName(outputStemOf(programFile))).toString())
|
||||
}
|
||||
else -> {
|
||||
System.err.println("Cx16 target only supports x16emu and box16 emulators.")
|
||||
@@ -76,7 +77,7 @@ class Cx16Target: ICompilationTarget,
|
||||
if(!quiet)
|
||||
println("\nStarting Commander X16 emulator $emulator...")
|
||||
|
||||
val cmdline = listOf(emulator, "-scale", "2", "-rtc", "-run", "-prg", "${programNameWithPath}.prg") + extraArgs
|
||||
val cmdline = listOf(emulator, "-scale", "2", "-rtc", "-run", "-prg", programFile.toString()) + extraArgs
|
||||
val processb = ProcessBuilder(cmdline)
|
||||
if(!quiet)
|
||||
processb.inheritIO()
|
||||
|
||||
@@ -12,6 +12,7 @@ class PETTarget: ICompilationTarget,
|
||||
|
||||
override val name = NAME
|
||||
override val supportsBankedCalls = false
|
||||
override val supportsStackFrames = false
|
||||
override val defaultEncoding = Encoding.PETSCII
|
||||
override val libraryPath = null
|
||||
override val customLauncher = emptyList<String>()
|
||||
@@ -27,7 +28,7 @@ class PETTarget: ICompilationTarget,
|
||||
|
||||
override val FLOAT_MAX_POSITIVE = Mflpt5.FLOAT_MAX_POSITIVE
|
||||
override val FLOAT_MAX_NEGATIVE = Mflpt5.FLOAT_MAX_NEGATIVE
|
||||
override val FLOAT_MEM_SIZE = Mflpt5.FLOAT_MEM_SIZE.toUInt()
|
||||
override val FLOAT_MEM_SIZE = Mflpt5.FLOAT_MEM_SIZE
|
||||
override val POINTER_MEM_SIZE = 2u
|
||||
override val ARRAY_SIZE_LIMIT = 256u
|
||||
override val PROGRAM_LOAD_ADDRESS = 0x0401u
|
||||
@@ -53,7 +54,7 @@ class PETTarget: ICompilationTarget,
|
||||
return m5.toDouble()
|
||||
}
|
||||
|
||||
override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) {
|
||||
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
|
||||
if(selectedEmulator!=1) {
|
||||
System.err.println("The pet target only supports the main emulator (Vice).")
|
||||
return
|
||||
@@ -62,9 +63,9 @@ class PETTarget: ICompilationTarget,
|
||||
if(!quiet)
|
||||
println("\nStarting PET emulator...")
|
||||
|
||||
val viceMonlist = C64Target.viceMonListName(programNameWithPath.toString())
|
||||
val cmdline = listOf("xpet", "-model", "4032", "-ramsize", "32", "-videosize", "40", "-silent", "-moncommands", viceMonlist,
|
||||
"-autostartprgmode", "1", "-autostart-warp", "-autostart", "${programNameWithPath}.prg")
|
||||
val viceMonlist = programFile.resolveSibling(viceMonListName(outputStemOf(programFile)))
|
||||
val cmdline = listOf("xpet", "-model", "4032", "-ramsize", "32", "-videosize", "40", "-silent", "-moncommands", viceMonlist.toString(),
|
||||
"-autostartprgmode", "1", "-autostart-warp", "-autostart", programFile.toString())
|
||||
val processb = ProcessBuilder(cmdline)
|
||||
if(!quiet)
|
||||
processb.inheritIO()
|
||||
|
||||
@@ -2,6 +2,7 @@ package prog8.code.target
|
||||
|
||||
import prog8.code.core.*
|
||||
import prog8.code.target.encodings.Encoder
|
||||
import prog8.code.target.zp.M68kZeropage
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
@@ -15,6 +16,7 @@ class Qemu68kTarget: ICompilationTarget,
|
||||
|
||||
override val name = NAME
|
||||
override val supportsBankedCalls = false
|
||||
override val supportsStackFrames = true
|
||||
override val defaultEncoding = Encoding.ISO
|
||||
override val libraryPath = null
|
||||
override val customLauncher = emptyList<String>()
|
||||
@@ -55,42 +57,27 @@ class Qemu68kTarget: ICompilationTarget,
|
||||
TODO("convert bytes to float")
|
||||
}
|
||||
|
||||
override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) {
|
||||
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
|
||||
if(selectedEmulator!=1) {
|
||||
System.err.println("The qemu68k target only supports the main emulator (Qemu).")
|
||||
return
|
||||
}
|
||||
val elfFile = programNameWithPath.resolveSibling("${programNameWithPath.fileName}.elf")
|
||||
val binFile = programNameWithPath.resolveSibling("${programNameWithPath.fileName}.bin")
|
||||
val isElf = elfFile.toFile().exists()
|
||||
val cpuStr = this.cpu.toString().lowercase()
|
||||
|
||||
val cmd = if (isElf) {
|
||||
listOf(
|
||||
"qemu-system-m68k",
|
||||
"-M", "virt",
|
||||
"-cpu", cpuStr,
|
||||
"-m", "1M",
|
||||
"-kernel", elfFile.toString(),
|
||||
"-nographic"
|
||||
)
|
||||
} else if (binFile.toFile().exists()) {
|
||||
val loadAddr = PROGRAM_LOAD_ADDRESS.toInt()
|
||||
listOf(
|
||||
"qemu-system-m68k",
|
||||
"-M", "virt",
|
||||
"-cpu", cpuStr,
|
||||
"-m", "1M",
|
||||
"-device", "loader,file=${binFile},addr=0x${loadAddr.toString(16)},cpu-num=0",
|
||||
"-nographic"
|
||||
)
|
||||
} else {
|
||||
System.err.println("No .elf or .bin file found for ${programNameWithPath.fileName}")
|
||||
if(!programFile.toFile().exists()) {
|
||||
System.err.println("No program file found: $programFile")
|
||||
return
|
||||
}
|
||||
val cpuStr = this.cpu.toString().lowercase()
|
||||
|
||||
val cmd = listOf(
|
||||
"qemu-system-m68k",
|
||||
"-M", "virt",
|
||||
"-cpu", cpuStr,
|
||||
"-m", "1M",
|
||||
"-kernel", programFile.toString(),
|
||||
"-nographic"
|
||||
)
|
||||
if(!quiet) {
|
||||
val launchMsg = if (isElf) "ELF" else "raw binary"
|
||||
println("Launching QEMU (press Ctrl-A X to exit)... (from $launchMsg)")
|
||||
println("Launching QEMU (press Ctrl-A X to exit)...")
|
||||
}
|
||||
val pb = ProcessBuilder(cmd).inheritIO()
|
||||
try {
|
||||
@@ -110,16 +97,3 @@ class Qemu68kTarget: ICompilationTarget,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class M68kZeropage(options: CompilationOptions): Zeropage(options) {
|
||||
override val SCRATCH_B1: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
override val SCRATCH_REG: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
override val SCRATCH_W1: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
override val SCRATCH_W2: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
override val SCRATCH_PTR: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
}
|
||||
@@ -3,9 +3,7 @@ package prog8.code.target
|
||||
import prog8.code.core.*
|
||||
import prog8.code.target.encodings.Encoder
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.extension
|
||||
import kotlin.io.path.isReadable
|
||||
import kotlin.io.path.name
|
||||
import kotlin.io.path.readText
|
||||
|
||||
class VMTarget: ICompilationTarget,
|
||||
@@ -14,6 +12,7 @@ class VMTarget: ICompilationTarget,
|
||||
|
||||
override val name = NAME
|
||||
override val supportsBankedCalls = false
|
||||
override val supportsStackFrames = false
|
||||
override val defaultEncoding = Encoding.ISO
|
||||
override val libraryPath = null
|
||||
override val customLauncher = emptyList<String>()
|
||||
@@ -71,21 +70,24 @@ class VMTarget: ICompilationTarget,
|
||||
return Double.fromBits(b0 or b1 or b2 or b3 or b4 or b5 or b6 or b7)
|
||||
}
|
||||
|
||||
override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) {
|
||||
launchEmulatorWithTrace(programNameWithPath, quiet, traceEnabled = false)
|
||||
override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) {
|
||||
launchEmulatorWithTrace(programFile, quiet, traceEnabled = false)
|
||||
}
|
||||
|
||||
fun launchEmulatorWithTrace(programNameWithPath: Path, quiet: Boolean, traceEnabled: Boolean) {
|
||||
/**
|
||||
* Runs the given .p8ir file in the embedded virtual machine.
|
||||
* The caller must pass the exact path; no fallback probing happens here, the -vm CLI flow resolves a bare name itself.
|
||||
*/
|
||||
fun launchEmulatorWithTrace(programFile: Path, quiet: Boolean, traceEnabled: Boolean) {
|
||||
if(!quiet)
|
||||
println("\nStarting Virtual Machine...")
|
||||
|
||||
// to not have external module dependencies in our own module, we launch the virtual machine via reflection
|
||||
val vm = Class.forName("prog8.vm.VmRunner").getDeclaredConstructor().newInstance() as IVirtualMachineRunner
|
||||
val withExt = if(programNameWithPath.extension=="p8ir") programNameWithPath else programNameWithPath.resolveSibling("${programNameWithPath.name}.p8ir")
|
||||
if(withExt.isReadable())
|
||||
vm.runProgram(withExt.readText(), quiet, traceEnabled)
|
||||
if(programFile.isReadable())
|
||||
vm.runProgram(programFile.readText(), quiet, traceEnabled)
|
||||
else
|
||||
throw java.nio.file.NoSuchFileException(withExt.name, null, "not a .p8ir file")
|
||||
throw java.nio.file.NoSuchFileException(programFile.toString(), null, "not a .p8ir file")
|
||||
}
|
||||
|
||||
override fun isIOAddress(address: UInt): Boolean = false
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package prog8.code.target.zp
|
||||
|
||||
import prog8.code.core.CompilationOptions
|
||||
import prog8.code.core.Zeropage
|
||||
|
||||
/**
|
||||
* The m68k has no zero page, so there are no scratch locations to hand out.
|
||||
* Any attempt to use one is a bug in the code generator.
|
||||
*/
|
||||
class M68kZeropage(options: CompilationOptions): Zeropage(options) {
|
||||
override val SCRATCH_B1: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
override val SCRATCH_REG: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
override val SCRATCH_W1: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
override val SCRATCH_W2: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
override val SCRATCH_PTR: UInt
|
||||
get() = throw IllegalStateException("m68k shouldn't use this zeropage variable")
|
||||
}
|
||||
@@ -21,6 +21,8 @@ object DummyMemsizer : IMemSizer {
|
||||
BaseDataType.UWORD, BaseDataType.WORD -> numElements * 2
|
||||
BaseDataType.LONG -> numElements * 4
|
||||
BaseDataType.FLOAT -> numElements * 5
|
||||
BaseDataType.POINTER -> numElements * 2
|
||||
BaseDataType.STRUCT_INSTANCE -> numElements * (dt.subType?.memsize(this) ?: throw IllegalArgumentException("struct instance array has unresolved subtype"))
|
||||
else -> throw IllegalArgumentException("invalid sub type")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ class TestDataType: FunSpec({
|
||||
BaseDataType.UWORD, BaseDataType.WORD -> numElements * 2
|
||||
BaseDataType.LONG -> numElements * 4
|
||||
BaseDataType.FLOAT -> numElements * 5
|
||||
BaseDataType.POINTER -> numElements * 2
|
||||
BaseDataType.STRUCT_INSTANCE -> numElements * (dt.subType?.memsize(this) ?: throw IllegalArgumentException("struct instance array has unresolved subtype"))
|
||||
else -> throw IllegalArgumentException("invalid sub type")
|
||||
}
|
||||
}
|
||||
@@ -93,10 +95,15 @@ class TestDataType: FunSpec({
|
||||
test("isArray extension") {
|
||||
BaseDataType.ARRAY.isArray shouldBe true
|
||||
BaseDataType.ARRAY_SPLITW.isArray shouldBe true
|
||||
BaseDataType.ARRAY_POINTER.isArray shouldBe true
|
||||
BaseDataType.UBYTE.isArray shouldBe false
|
||||
}
|
||||
|
||||
test("isPointerArray property") {
|
||||
DataType.arrayOfPointersTo(BaseDataType.UBYTE).isPointerArray shouldBe true
|
||||
DataType.arrayOfPointersTo(BaseDataType.WORD).isPointerArray shouldBe true
|
||||
DataType.arrayFor(BaseDataType.UBYTE, tgt).isPointerArray shouldBe false
|
||||
}
|
||||
|
||||
test("isPointer extension") {
|
||||
BaseDataType.POINTER.isPointer shouldBe true
|
||||
BaseDataType.UBYTE.isPointer shouldBe false
|
||||
|
||||
@@ -19,7 +19,7 @@ import prog8.code.target.zp.PETZeropage
|
||||
|
||||
|
||||
/**
|
||||
* Comprehensive tests for Zeropage free list configurations across all targets.
|
||||
* Tests for Zeropage free list configurations across all targets.
|
||||
*
|
||||
* Tests verify that for each target and ZeropageType combination:
|
||||
* - The exact list of free locations is correct
|
||||
|
||||
@@ -6,6 +6,8 @@ import prog8.code.assembly.AssemblyProgram6502
|
||||
import prog8.code.assembly.IAssemblyProgram
|
||||
import prog8.code.ast.*
|
||||
import prog8.code.core.*
|
||||
import prog8.code.cpu6502.byteShiftAddExpansion
|
||||
import prog8.code.cpu6502.wordShiftAddExpansion
|
||||
import prog8.code.source.ImportFileSystem
|
||||
import prog8.code.source.SourceCode
|
||||
import prog8.code.target.Cx16Target
|
||||
@@ -45,8 +47,36 @@ class AsmGen6502Internal (
|
||||
preassignedCallSiteIds: Map<PtAsmSub, UByte> = emptyMap()
|
||||
) {
|
||||
|
||||
internal val optimizedByteMultiplications = setOf(3,5,6,7,9,10,11,12,13,14,15,20,25,40,50,80,100)
|
||||
internal val optimizedWordMultiplications = setOf(3,5,6,7,9,10,12,15,20,25,40,50,80,100,320,640)
|
||||
// 40, 50, 80 and 100 are deliberately absent: prog8_math's mul_byte_40/50/80/100 are wrong.
|
||||
// They mask the multiplicand with `and #7` or `and #3` and index a small table, which only
|
||||
// works if x*k mod 256 has that period. The true period is 256/gcd(k,256), i.e. 32, 128, 16
|
||||
// and 64 entries respectively, so the tables are far too small (e.g. mul_byte_100 returns
|
||||
// (x&3)*100, giving 0 for x=4 where the answer is 144). The generic expansion below computes
|
||||
// them correctly in 8-11 instructions.
|
||||
internal val optimizedByteMultiplications = setOf(3,5,6,7,9,10,11,12,13,14,15,20,25)
|
||||
// 320 and 640 are deliberately absent: mul_word_320/640 compute (low byte of x)*320/*640 and
|
||||
// ignore the multiplicand's high byte, so they are wrong for any x >= 256 (the comment
|
||||
// "msb in Y doesn't matter" is false). The generic expansion handles them in 27-29 instructions.
|
||||
internal val optimizedWordMultiplications = setOf(3,5,6,7,9,10,12,15,20,25,40,50,80,100)
|
||||
|
||||
// Thin wrappers over the shared expansion in codeCore, so this backend and codeGenNew6502 emit
|
||||
// an identical sequence. Works on the raw bit pattern, exactly like the hand-written
|
||||
// prog8_math.mul_word_N / mul_byte_N routines, so signed and unsigned agree because both
|
||||
// truncate mod 2^n and no sign extension is applied afterwards.
|
||||
internal fun emitWordShiftAdd(value: Int): Boolean {
|
||||
val expansion = wordShiftAddExpansion(value) ?: return false
|
||||
out(" sta P8ZP_SCRATCH_W2 | sty P8ZP_SCRATCH_W2+1") // x
|
||||
out(" sta P8ZP_SCRATCH_W1 | sty P8ZP_SCRATCH_W1+1") // r = x
|
||||
expansion.forEach { out(" $it") }
|
||||
return true
|
||||
}
|
||||
|
||||
internal fun emitByteShiftAdd(value: Int): Boolean {
|
||||
val expansion = byteShiftAddExpansion(value) ?: return false
|
||||
expansion.forEach { out(" $it") }
|
||||
return true
|
||||
}
|
||||
|
||||
internal val loopEndLabels = ArrayDeque<String>()
|
||||
private val zeropage = options.compTarget.zeropage
|
||||
private val allocator = VariableAllocator(symbolTable, options, errors, program)
|
||||
@@ -80,7 +110,7 @@ class AsmGen6502Internal (
|
||||
programGen.generate()
|
||||
|
||||
if(errors.noErrors()) {
|
||||
val output = options.outputDir.resolve("${program.name}.asm")
|
||||
val output = OutputFiles.of(options, program.name).asm()
|
||||
val asmLines = assembly.toMutableList()
|
||||
if(options.compTarget.name==Cx16Target.NAME) {
|
||||
scanInvalid65816instructions(asmLines)
|
||||
@@ -100,7 +130,7 @@ class AsmGen6502Internal (
|
||||
if(options.dumpVariables)
|
||||
dumpVariables()
|
||||
|
||||
return AssemblyProgram6502(program.name, options.outputDir, options.compTarget)
|
||||
return AssemblyProgram6502(program.name, options, options.compTarget)
|
||||
} else {
|
||||
errors.report()
|
||||
return null
|
||||
@@ -287,12 +317,12 @@ class AsmGen6502Internal (
|
||||
return Pair("#<$label", "#>$label")
|
||||
} else if (e.value != null) {
|
||||
val value = e.value!!.toInt()
|
||||
return Pair("#<${(value and 0xff).toHex()}", "#>${((value shr 8) and 0xff).toHex()}")
|
||||
return Pair("#<${value.toHex()}", "#>${value.toHex()}")
|
||||
}
|
||||
}
|
||||
if (e is PtNumber) {
|
||||
val value = e.number.toInt()
|
||||
return Pair("#<${(value and 0xff).toHex()}", "#>${((value shr 8) and 0xff).toHex()}")
|
||||
return Pair("#<${value.toHex()}", "#>${value.toHex()}")
|
||||
}
|
||||
if (e is PtAddressOf && e.identifier != null && !e.isFromArrayElement && e.dereference == null) {
|
||||
var symbol = asmVariableName(e.identifier!!)
|
||||
@@ -1982,6 +2012,16 @@ $repeatLabel""")
|
||||
return
|
||||
}
|
||||
|
||||
if(sub.signature.returns.isEmpty()) {
|
||||
// a void subroutine may return the result of a void function call (e.g. "return voidfunc(args)"):
|
||||
// perform the call for its side effects, then return
|
||||
val fcall = ret.children.single() as? PtFunctionCall
|
||||
?: throw AssemblyError("void subroutine can only return the result of a void function call ${ret.position}")
|
||||
functioncallAsmGen.translateFunctionCall(fcall)
|
||||
out(" rts")
|
||||
return
|
||||
}
|
||||
|
||||
val returnDt = sub.signature.returns.single()
|
||||
if (returnDt.isNumericOrBool || returnDt.isPointer) {
|
||||
assignExpressionToRegister(returnvalue, returnRegs.single().first.registerOrPair!!, returnDt.isSigned)
|
||||
@@ -2016,14 +2056,19 @@ $repeatLabel""")
|
||||
val tgt = AsmAssignTarget(TargetStorageKind.REGISTER, this, targetDt, null, it.first.position, register = it.second.first.registerOrPair!!)
|
||||
assignExpressionTo(it.first as PtExpression, tgt)
|
||||
}
|
||||
// if the float is not the first value, assign it now: the float load (MOVFM) clobbers A/Y,
|
||||
// and the first non-float value is passed in AY, so it must be loaded after the float
|
||||
if(floatIdx > 0 && floatAssign != null) {
|
||||
assignExpressionToRegister(floatAssign.first as PtExpression, floatAssign.second.first.registerOrPair!!, false)
|
||||
}
|
||||
if(nonFloatAssigns.isNotEmpty()) {
|
||||
nonFloatAssigns.first().also {
|
||||
assignExpressionToRegister(it.first as PtExpression, it.second.first.registerOrPair!!, (it.first as PtExpression).type.isSigned)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Assign float return LAST (so FAC1 has the correct value when we RTS)
|
||||
if(floatAssign != null) {
|
||||
if(floatIdx == 0 && floatAssign != null) {
|
||||
assignExpressionToRegister(floatAssign.first as PtExpression, floatAssign.second.first.registerOrPair!!, false)
|
||||
}
|
||||
}
|
||||
@@ -2048,11 +2093,17 @@ $repeatLabel""")
|
||||
throw AssemblyError("%asmbinary inside non-library/non-filesystem module not yet supported")
|
||||
val sourcePath = Path(incbin.definingBlock()!!.source.origin)
|
||||
val includedPath = sourcePath.resolveSibling(incbin.file)
|
||||
val pathForAssembler = options.outputDir // #54: 64tass needs the path *relative to the .asm file*
|
||||
.sanitize()
|
||||
.relativize(includedPath.sanitize())
|
||||
.normalize() // avoid assembler warnings (-Wportable; only some, not all)
|
||||
.toString().replace('\\', '/')
|
||||
val pathForAssembler = try {
|
||||
options.outputDir // #54: 64tass needs the path *relative to the .asm file*
|
||||
.sanitize()
|
||||
.relativize(includedPath.sanitize())
|
||||
.normalize() // avoid assembler warnings (-Wportable; only some, not all)
|
||||
.toString().replace('\\', '/')
|
||||
} catch (_: IllegalArgumentException) {
|
||||
// Path.relativize throws when the two paths have different roots (different drive/volume)
|
||||
errors.err("cannot determine relative path for %asmbinary file '${incbin.file}' (at ${includedPath}): it must be on the same drive/volume as the generated assembly in ${options.outputDir}", incbin.position)
|
||||
return
|
||||
}
|
||||
out(" .binary \"$pathForAssembler\" $offset $length")
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ internal fun optimizeAssembly(lines: MutableList<String>, machine: ICompilationT
|
||||
|
||||
var linesByFourteen = getLinesBy(pretrimmed, lines, 14)
|
||||
linesByFourteen = runPass(optimizeSameAssignments(linesByFourteen, machine, symbolTable), 14, linesByFourteen)
|
||||
linesByFourteen = runPass(optimizeSamePointerIndexingAndUselessBeq(linesByFourteen), 14, linesByFourteen)
|
||||
linesByFourteen = runPass(optimizeSamePointerIndexingAndUselessBeq(linesByFourteen, machine, symbolTable), 14, linesByFourteen)
|
||||
linesByFourteen = runPass(optimizeAddWordToSameVariableOrExtraRegisterLoadInWordStore(linesByFourteen), 14, linesByFourteen)
|
||||
|
||||
if(!modified)
|
||||
@@ -412,7 +412,96 @@ private fun optimizeSameAssignments(
|
||||
return mods
|
||||
}
|
||||
|
||||
private fun optimizeSamePointerIndexingAndUselessBeq(linesByFourteen: Sequence<List<TrimmedLine>>): List<Modification> {
|
||||
private const val NO_MEMORY_WRITE = ""
|
||||
private const val UNKNOWN_WRITE_TARGET = "\u0000"
|
||||
|
||||
/** Mnemonics that can change memory (direct stores plus read-modify-write). */
|
||||
private val memoryWritingMnemonics = setOf(
|
||||
"sta", "stx", "sty", "stz", "inc", "dec", "asl", "lsr", "rol", "ror",
|
||||
"and", "ora", "eor", "adc", "sbc", "bit"
|
||||
)
|
||||
|
||||
/** Control transfers that can reach the code after them with a different Y, or change it. */
|
||||
private val flowMnemonics = setOf(
|
||||
"jsr", "rts", "rti", "jmp", "bra", "brk",
|
||||
"bcc", "bcs", "beq", "bmi", "bne", "bpl", "bvc", "bvs", "bbs", "bbr"
|
||||
)
|
||||
|
||||
private fun TrimmedLine.mnemonic(): String =
|
||||
instruction.trimStart().takeWhile { !it.isWhitespace() }.lowercase()
|
||||
|
||||
/** True if the line carries a label, with or without an instruction following it. */
|
||||
private fun TrimmedLine.hasLabel(): Boolean {
|
||||
if (haslabelPretrimmed(trimmed)) return true
|
||||
return trimmed.isNotEmpty() && (trimmed[0] == '+' || trimmed[0] == '-')
|
||||
}
|
||||
|
||||
/** The memory operand this instruction writes, with any +offset and ,index suffix stripped.
|
||||
* Returns [NO_MEMORY_WRITE] for immediate and implied forms, and [UNKNOWN_WRITE_TARGET] for
|
||||
* indirect forms, where the destination cannot be determined statically. */
|
||||
private fun TrimmedLine.memoryWriteBase(): String {
|
||||
val op = instruction.extractOperandTrimmed()
|
||||
if (op.isEmpty() || op.startsWith("#") || op == "a") return NO_MEMORY_WRITE
|
||||
if (op.startsWith("(")) return UNKNOWN_WRITE_TARGET
|
||||
return op.removeSuffix(",y").removeSuffix(",x").substringBefore('+')
|
||||
}
|
||||
|
||||
/** True if this line may write to [base]. Conservative: an indirect store is assumed to. */
|
||||
private fun TrimmedLine.mayWriteBase(base: String): Boolean {
|
||||
if (mnemonic() !in memoryWritingMnemonics) return false
|
||||
val target = memoryWriteBase()
|
||||
if (target == NO_MEMORY_WRITE) return false
|
||||
if (target == UNKNOWN_WRITE_TARGET) return true
|
||||
return target.equals(base, ignoreCase = true)
|
||||
}
|
||||
|
||||
/** True if [operand] is safe to read more than once: an immediate, or memory that is not an
|
||||
* IO location (an IO read can return a different value without the CPU having written it). */
|
||||
private fun isNonVolatileIndexOperand(indexLine: TrimmedLine, machine: ICompilationTarget, symbolTable: SymbolTable): Boolean {
|
||||
val operand = indexLine.instruction.extractOperandTrimmed()
|
||||
if (operand.isEmpty() || operand.startsWith("#")) return true
|
||||
val addr = getAddressArg(indexLine.instruction, symbolTable)
|
||||
return if (addr == null) !looksLikeIOAddress(operand, machine)
|
||||
else !machine.isIOAddress(addr)
|
||||
}
|
||||
|
||||
/** Decides whether the index reload at [reloadIdx] inside [window] can be deleted.
|
||||
*
|
||||
* The reload is redundant only if Y still holds the value it would have loaded. That requires
|
||||
* the reload to load the very same operand as the original index load, and none of the
|
||||
* following in between:
|
||||
* - modifies Y
|
||||
* - writes the index operand itself (so `ldy i / lda arr,y / inc i / ldy i / sta arr,y` keeps
|
||||
* its reload: without it the store lands one element earlier)
|
||||
* - is a call or control transfer, since a callee can clobber Y and another path can arrive
|
||||
* with a different Y
|
||||
* - carries a label, for the same reason
|
||||
* The index operand must also not be a volatile IO location.
|
||||
*/
|
||||
private fun canDropIndexReload(
|
||||
window: List<TrimmedLine>,
|
||||
reloadIdx: Int,
|
||||
indexLine: TrimmedLine,
|
||||
machine: ICompilationTarget,
|
||||
symbolTable: SymbolTable
|
||||
): Boolean {
|
||||
if (reloadIdx <= 1) return false
|
||||
val operand = indexLine.instruction.extractOperandTrimmed()
|
||||
// the reload has to load the same index, or Y holds a different one and is not redundant
|
||||
if (window[reloadIdx].instruction.extractOperandTrimmed() != operand) return false
|
||||
val base = if (operand.startsWith("#") || operand.isEmpty()) NO_MEMORY_WRITE
|
||||
else operand.removeSuffix(",y").removeSuffix(",x").substringBefore('+')
|
||||
for (idx in 1 until reloadIdx) {
|
||||
val line = window[idx]
|
||||
if (line.instruction.modifiesYRegister()) return false
|
||||
if (line.mnemonic() in flowMnemonics) return false
|
||||
if (line.hasLabel()) return false
|
||||
if (base != NO_MEMORY_WRITE && line.mayWriteBase(base)) return false
|
||||
}
|
||||
return isNonVolatileIndexOperand(indexLine, machine, symbolTable)
|
||||
}
|
||||
|
||||
private fun optimizeSamePointerIndexingAndUselessBeq(linesByFourteen: Sequence<List<TrimmedLine>>, machine: ICompilationTarget, symbolTable: SymbolTable): List<Modification> {
|
||||
|
||||
// Optimize same pointer indexing where for instance we load and store to the same ptr index in Y
|
||||
// if Y isn't modified in between we can omit the second LDY:
|
||||
@@ -436,20 +525,38 @@ private fun optimizeSamePointerIndexingAndUselessBeq(linesByFourteen: Sequence<L
|
||||
val secondvalue = f2.extractOperandTrimmed()
|
||||
val fourthvalue = f4.extractOperandTrimmed()
|
||||
val fifthvalue = f5.extractOperandTrimmed()
|
||||
if(!f3.modifiesYRegister() && firstvalue==fourthvalue && secondvalue==fifthvalue && secondvalue.endsWith(",y") && fifthvalue.endsWith(",y")) {
|
||||
if(secondvalue==fifthvalue && secondvalue.endsWith(",y") && fifthvalue.endsWith(",y") &&
|
||||
canDropIndexReload(lines, 3, lines[0], machine, symbolTable)) {
|
||||
mods.add(Modification(lines[3].index, true, null))
|
||||
}
|
||||
}
|
||||
if(f1.startsWith("ldy ") && f2.startsWith("lda ") && f5.startsWith("ldy ") && f6.startsWith("sta ")) {
|
||||
val firstvalue = f1.extractOperandTrimmed()
|
||||
val secondvalue = f2.extractOperandTrimmed()
|
||||
val fifthvalue = f5.extractOperandTrimmed()
|
||||
val sixthvalue = f6.extractOperandTrimmed()
|
||||
if(!f3.modifiesYRegister() && !f4.modifiesYRegister() && firstvalue==fifthvalue && secondvalue==sixthvalue && secondvalue.endsWith(",y") && sixthvalue.endsWith(",y")) {
|
||||
if(secondvalue==sixthvalue && secondvalue.endsWith(",y") && sixthvalue.endsWith(",y") &&
|
||||
canDropIndexReload(lines, 4, lines[0], machine, symbolTable)) {
|
||||
mods.add(Modification(lines[4].index, true, null))
|
||||
}
|
||||
}
|
||||
|
||||
// Relaxed variant: two consecutive indexed accesses, not necessarily a matched load/store
|
||||
// pair. Only the index reload has to be the same, and nothing in between may disturb it.
|
||||
// ldy V
|
||||
// lda A,y
|
||||
// ... <-- does not modify Y, does not write V, no call or label
|
||||
// ldy V <-- can be removed
|
||||
// lda B,y
|
||||
if(f1.startsWith("ldy ") && f2.startsWith("lda ") && f4.startsWith("ldy ") && f5.startsWith("lda ") &&
|
||||
f2.endsWith(",y") && f5.endsWith(",y") &&
|
||||
canDropIndexReload(lines, 3, lines[0], machine, symbolTable)) {
|
||||
mods.add(Modification(lines[3].index, true, null))
|
||||
}
|
||||
if(f1.startsWith("ldy ") && f2.startsWith("lda ") && f5.startsWith("ldy ") && f6.startsWith("lda ") &&
|
||||
f2.endsWith(",y") && f6.endsWith(",y") &&
|
||||
canDropIndexReload(lines, 4, lines[0], machine, symbolTable)) {
|
||||
mods.add(Modification(lines[4].index, true, null))
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
beq +
|
||||
|
||||
@@ -20,6 +20,25 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
translateFunctioncall(fcall, null, discardResult = true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Several builtins can only produce their result in a CPU register, and which one that is
|
||||
* depends on the width of the result: the byte-returning ones (lsb/msb) take a single register,
|
||||
* the word-returning ones (lsw/msw) and popw take a register pair, and the builtin picks the
|
||||
* specific register. [hardwareReg] is the register to fall back on when the caller has no
|
||||
* preference, or when it asked for something the builtin cannot deliver to directly.
|
||||
*
|
||||
* The caller's requested register is honoured when it is a real CPU register. A CX16 virtual
|
||||
* register (for example an asmsub parameter declared as @R0) is not: those are memory locations,
|
||||
* so the builtin produces its result in [hardwareReg] and the caller's assignment then stores it
|
||||
* into the virtual register. Passing the virtual register through would make the builtin reject
|
||||
* it as an invalid result register.
|
||||
*
|
||||
* (funcPeekW and funcMkword already handle virtual registers themselves.)
|
||||
*/
|
||||
private fun withResultRegister(requested: RegisterOrPair?, hardwareReg: RegisterOrPair, compute: (RegisterOrPair) -> Array<RegisterOrPair>): Array<RegisterOrPair> =
|
||||
if (requested != null && requested !in Cx16VirtualRegisters) compute(requested)
|
||||
else compute(hardwareReg)
|
||||
|
||||
private fun translateFunctioncall(fcall: PtFunctionCall, firstReturnRegister: RegisterOrPair?, discardResult: Boolean): Array<RegisterOrPair> {
|
||||
|
||||
// Note: returns the actual register(s) that the return value is in, so the caller should still make sure to place it into the actual correct destination.
|
||||
@@ -31,12 +50,12 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
val sscope = fcall.definingISub()
|
||||
|
||||
return when (fcall.name) {
|
||||
"msw" -> funcMsw(fcall, firstReturnRegister ?: RegisterOrPair.AY)
|
||||
"lsw" -> funcLsw(fcall, firstReturnRegister ?: RegisterOrPair.AY)
|
||||
"msb" -> funcMsb(fcall, false, firstReturnRegister ?: RegisterOrPair.A)
|
||||
"msb__long" -> funcMsb(fcall, true, firstReturnRegister ?: RegisterOrPair.A)
|
||||
"lsb" -> funcLsb(fcall, false, firstReturnRegister ?: RegisterOrPair.A)
|
||||
"lsb__long" -> funcLsb(fcall,true, firstReturnRegister ?: RegisterOrPair.A)
|
||||
"msw" -> withResultRegister(firstReturnRegister, RegisterOrPair.AY) { funcMsw(fcall, it) }
|
||||
"lsw" -> withResultRegister(firstReturnRegister, RegisterOrPair.AY) { funcLsw(fcall, it) }
|
||||
"msb" -> withResultRegister(firstReturnRegister, RegisterOrPair.A) { funcMsb(fcall, false, it) }
|
||||
"msb__long" -> withResultRegister(firstReturnRegister, RegisterOrPair.A) { funcMsb(fcall, true, it) }
|
||||
"lsb" -> withResultRegister(firstReturnRegister, RegisterOrPair.A) { funcLsb(fcall, false, it) }
|
||||
"lsb__long" -> withResultRegister(firstReturnRegister, RegisterOrPair.A) { funcLsb(fcall, true, it) }
|
||||
"mkword" -> funcMkword(fcall, firstReturnRegister ?: RegisterOrPair.AY)
|
||||
"mklong", "mklong2" -> funcMklong(fcall, firstReturnRegister ?: RegisterOrPair.R14R15)
|
||||
"clamp__byte", "clamp__ubyte" -> funcClamp(fcall)
|
||||
@@ -98,8 +117,8 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
"pushw" -> funcPushW(fcall)
|
||||
"pushl" -> funcPushL(fcall)
|
||||
"pushf" -> funcPushF(fcall)
|
||||
"pop" -> funcPop(firstReturnRegister ?: RegisterOrPair.A)
|
||||
"popw" -> funcPopW(firstReturnRegister ?: RegisterOrPair.AY)
|
||||
"pop" -> withResultRegister(firstReturnRegister, RegisterOrPair.A) { funcPop(it) }
|
||||
"popw" -> withResultRegister(firstReturnRegister, RegisterOrPair.AY) { funcPopW(it) }
|
||||
"popl" -> funcPopL()
|
||||
"popf" -> funcPopF()
|
||||
else -> throw AssemblyError("sizeof must have been replaced with a constant ${fcall.position}")
|
||||
@@ -247,11 +266,15 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
}
|
||||
BaseDataType.UWORD -> {
|
||||
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.AY)
|
||||
// prog8_math.square does its own range check and falls back to the general
|
||||
// multiply_words for values outside -255..255, so the fast path stays valid.
|
||||
asmgen.out(" jsr prog8_math.square") // result is in AY
|
||||
return arrayOf(RegisterOrPair.AY)
|
||||
}
|
||||
BaseDataType.LONG -> {
|
||||
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.R14R15, true)
|
||||
// prog8_math.square_long does its own range check and falls back to the general
|
||||
// multiply_longs when the long doesn't fit in a signed 16-bit word.
|
||||
asmgen.out(" jsr prog8_math.square_long") // result is in R14:R15
|
||||
return arrayOf(RegisterOrPair.R14R15)
|
||||
}
|
||||
@@ -426,7 +449,7 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
.byte $constBank""")
|
||||
} else {
|
||||
if(asmgen.options.romable)
|
||||
TODO("non-const callfar (jsrfar) yet that's usable in ROM ${fcall.position}")
|
||||
asmgen.errors.err("non-const callfar (jsrfar) has no ROMable code generation", fcall.position)
|
||||
// self-modifying code: set jsrfar arguments
|
||||
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.A) // bank
|
||||
asmgen.out(" sta (++)+0")
|
||||
@@ -483,7 +506,7 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
.byte $constBank""")
|
||||
} else {
|
||||
if(asmgen.options.romable)
|
||||
TODO("non-const callfar2 (jsrfar) yet that's usable in ROM ${fcall.position}")
|
||||
asmgen.errors.err("non-const callfar2 (jsrfar) has no ROMable code generation", fcall.position)
|
||||
// self-modifying code: set jsrfar arguments
|
||||
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.A) // bank
|
||||
asmgen.out(" sta (++)+0")
|
||||
@@ -2065,6 +2088,154 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
}
|
||||
}
|
||||
|
||||
private fun funcClampWordConst(minExpr: PtExpression, maxExpr: PtExpression, minOp: Pair<String, String>, maxOp: Pair<String, String>, signed: Boolean) {
|
||||
// value is already in AY on entry, result in AY
|
||||
val minVal = (asmgen.unwrapCasts(minExpr) as? PtNumber)?.number?.toInt()
|
||||
val maxVal = (asmgen.unwrapCasts(maxExpr) as? PtNumber)?.number?.toInt()
|
||||
val typeMin = if (signed) -32768 else 0
|
||||
val typeMax = if (signed) 32767 else 65535
|
||||
val oneSided = minVal != null && minVal == typeMin || maxVal != null && maxVal == typeMax
|
||||
val fullRange = minVal != null && maxVal != null && minVal == typeMin && maxVal == typeMax
|
||||
when {
|
||||
fullRange -> {
|
||||
// no-op: the value in AY is already the clamped result
|
||||
}
|
||||
signed && minVal == typeMin -> {
|
||||
// upper clamp only
|
||||
emitSignedWordClampProlog()
|
||||
emitSignedWordUpperCompare(maxOp)
|
||||
emitWordLoadValueFromW1()
|
||||
}
|
||||
signed && maxVal == typeMax -> {
|
||||
// lower clamp only
|
||||
emitSignedWordClampProlog()
|
||||
emitSignedWordLowerCompare(minOp)
|
||||
}
|
||||
signed -> {
|
||||
emitSignedWordClampProlog()
|
||||
emitSignedWordUpperCompare(maxOp)
|
||||
emitSignedWordLowerCompare(minOp)
|
||||
}
|
||||
!oneSided -> {
|
||||
// uword, two-sided clamp
|
||||
emitWordUpperClamp(maxOp)
|
||||
emitWordLowerClamp(minOp)
|
||||
}
|
||||
minVal == typeMin -> {
|
||||
// uword, upper clamp only
|
||||
emitWordUpperClamp(maxOp)
|
||||
}
|
||||
else -> {
|
||||
// uword, lower clamp only
|
||||
emitWordLowerClamp(minOp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun emitSignedWordClampProlog() {
|
||||
asmgen.out(listOf(
|
||||
" sta P8ZP_SCRATCH_W1",
|
||||
" sty P8ZP_SCRATCH_W1+1"
|
||||
).joinToString("\n"))
|
||||
}
|
||||
|
||||
private fun emitWordLoadValueFromW1() {
|
||||
asmgen.out(listOf(
|
||||
" lda P8ZP_SCRATCH_W1",
|
||||
" ldy P8ZP_SCRATCH_W1+1"
|
||||
).joinToString("\n"))
|
||||
}
|
||||
|
||||
// signed compare of max/min (16-bit immediates) against the running value in P8ZP_SCRATCH_W1.
|
||||
// The result of the two-sided compare is the high byte of (bound - value), with the
|
||||
// overflow-compensated sign bit set, mirroring the func_clamp_word sequence.
|
||||
|
||||
private fun emitSignedWordUpperCompare(maxOp: Pair<String, String>) {
|
||||
// if value > max: running value becomes max
|
||||
val keep = asmgen.makeLabel("clampkeep")
|
||||
val done = asmgen.makeLabel("clampdone")
|
||||
asmgen.out(listOf(
|
||||
" ldy ${maxOp.second}",
|
||||
" lda ${maxOp.first}",
|
||||
" cmp P8ZP_SCRATCH_W1",
|
||||
" tya",
|
||||
" sbc P8ZP_SCRATCH_W1+1",
|
||||
" bvc $keep",
|
||||
" eor #\$80",
|
||||
keep,
|
||||
" bpl $done",
|
||||
" lda ${maxOp.first}",
|
||||
" ldy ${maxOp.second}",
|
||||
" sta P8ZP_SCRATCH_W1",
|
||||
" sty P8ZP_SCRATCH_W1+1",
|
||||
done
|
||||
).joinToString("\n"))
|
||||
}
|
||||
|
||||
private fun emitSignedWordLowerCompare(minOp: Pair<String, String>) {
|
||||
// if value < min: result becomes min, otherwise the running value (in W1)
|
||||
val keep = asmgen.makeLabel("clampkeep")
|
||||
val set = asmgen.makeLabel("clampset")
|
||||
val done = asmgen.makeLabel("clampdone")
|
||||
asmgen.out(listOf(
|
||||
" ldy ${minOp.second}",
|
||||
" lda ${minOp.first}",
|
||||
" cmp P8ZP_SCRATCH_W1",
|
||||
" tya",
|
||||
" sbc P8ZP_SCRATCH_W1+1",
|
||||
" bvc $keep",
|
||||
" eor #\$80",
|
||||
keep,
|
||||
" bpl $set",
|
||||
" lda P8ZP_SCRATCH_W1",
|
||||
" ldy P8ZP_SCRATCH_W1+1",
|
||||
" jmp $done",
|
||||
set,
|
||||
" lda ${minOp.first}",
|
||||
" ldy ${minOp.second}",
|
||||
done
|
||||
).joinToString("\n"))
|
||||
}
|
||||
|
||||
private fun emitWordUpperClamp(maxOp: Pair<String, String>) {
|
||||
// uword: clamp the value in AY down to maxOp (result = min(value, max))
|
||||
val keep = asmgen.makeLabel("clampkeep")
|
||||
val set = asmgen.makeLabel("clampset")
|
||||
asmgen.out(listOf(
|
||||
" cpy ${maxOp.second}",
|
||||
" bcc $keep",
|
||||
" bne $set",
|
||||
" cmp ${maxOp.first}",
|
||||
" bcc $keep",
|
||||
" beq $keep",
|
||||
set,
|
||||
" lda ${maxOp.first}",
|
||||
" ldy ${maxOp.second}",
|
||||
keep
|
||||
).joinToString("\n"))
|
||||
}
|
||||
|
||||
private fun emitWordLowerClamp(minOp: Pair<String, String>) {
|
||||
// uword: clamp the value in AY up to minOp (result = max(value, min))
|
||||
val keep = asmgen.makeLabel("clampkeep")
|
||||
val set = asmgen.makeLabel("clampset")
|
||||
val done = asmgen.makeLabel("clampdone")
|
||||
asmgen.out(listOf(
|
||||
" cpy ${minOp.second}",
|
||||
" bcc $set",
|
||||
" bne $keep",
|
||||
" cmp ${minOp.first}",
|
||||
" bcc $set",
|
||||
" beq $keep",
|
||||
keep,
|
||||
" jmp $done",
|
||||
set,
|
||||
" lda ${minOp.first}",
|
||||
" ldy ${minOp.second}",
|
||||
done
|
||||
).joinToString("\n"))
|
||||
}
|
||||
|
||||
private fun funcClamp(fcall: PtFunctionCall): Array<RegisterOrPair> {
|
||||
val signed = fcall.type.isSigned
|
||||
val zpCheck = { name: String -> asmgen.isZpVar(name) }
|
||||
@@ -2099,6 +2270,17 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
return arrayOf(RegisterOrPair.A)
|
||||
}
|
||||
fcall.type.isWord -> {
|
||||
val minOp = asmgen.getStaticAddressLowHigh(fcall.args[1])
|
||||
val maxOp = asmgen.getStaticAddressLowHigh(fcall.args[2])
|
||||
val minIsConst = asmgen.unwrapCasts(fcall.args[1]) !is PtIdentifier
|
||||
val maxIsConst = asmgen.unwrapCasts(fcall.args[2]) !is PtIdentifier
|
||||
if (minOp != null && maxOp != null && minIsConst && maxIsConst) {
|
||||
// both bounds are compile-time constants (numeric literals or fixed label addresses):
|
||||
// emit a small inline clamp instead of evaluating bounds into scratch + jsr func_clamp_word/uword
|
||||
assignAsmGen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.AY, signed) // value
|
||||
funcClampWordConst(fcall.args[1], fcall.args[2], minOp, maxOp, signed)
|
||||
return arrayOf(RegisterOrPair.AY)
|
||||
}
|
||||
if (fcall.args[0].isSimple(zpCheck) && fcall.args[1].isSimple(zpCheck) && fcall.args[2].isSimple(zpCheck)) {
|
||||
assignAsmGen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_W1", fcall.args[1].type) // minimum
|
||||
assignAsmGen.assignExpressionToVariable(fcall.args[2], "P8ZP_SCRATCH_W2", fcall.args[2].type) // maximum
|
||||
|
||||
@@ -396,24 +396,27 @@ internal class ProgramAndVarsGen(
|
||||
|
||||
asmgen.out("; struct types")
|
||||
symboltable.allStructTypes().distinctBy { it.name }.forEach { structtype ->
|
||||
val paramFields = structtype.fields.filter { !it.isArray }
|
||||
val structargs = paramFields.indices.joinToString(",") { "f$it" }
|
||||
asmgen.out("${structtype.scopedNameString} .struct $structargs\n")
|
||||
var paramIdx = 0
|
||||
for(field in structtype.fields) {
|
||||
val type = asmTypeString(field.type)
|
||||
if(field.isArray) {
|
||||
val anonymous = List(field.arraySize!!) { "?" }.joinToString(",")
|
||||
asmgen.out("p8v_${field.name} $type $anonymous")
|
||||
} else {
|
||||
asmgen.out("p8v_${field.name} $type \\f$paramIdx")
|
||||
paramIdx++
|
||||
}
|
||||
if(structtype.isUnion) {
|
||||
// NB: .union takes no constructor parameters, unlike .struct
|
||||
asmgen.out("${structtype.scopedNameString} .union\n")
|
||||
emitStructDefFields(structtype) { "?" }
|
||||
asmgen.out(" .endunion\n")
|
||||
asmgen.out("${structtype.scopedNameString}_size=${structtype.size}\n")
|
||||
} else {
|
||||
val paramFields = structtype.fields.filter { !it.isArray }
|
||||
val structargs = paramFields.indices.joinToString(",") { "f$it" }
|
||||
asmgen.out("${structtype.scopedNameString} .struct $structargs\n")
|
||||
var paramIdx = 0
|
||||
emitStructDefFields(structtype) { "\\f${paramIdx++}" }
|
||||
asmgen.out(" .endstruct\n")
|
||||
asmgen.out("${structtype.scopedNameString}_size=${structtype.size}\n")
|
||||
}
|
||||
asmgen.out(" .endstruct\n")
|
||||
}
|
||||
|
||||
val (instancesNoInit, instances) = symboltable.allStructInstances().partition { it.initialValues.isEmpty() }
|
||||
val (instancesNoInit, instances) = symboltable.allStructInstances().partition {
|
||||
val structtype = symboltable.lookup(it.structName) as StStruct
|
||||
it.initialValues.isEmpty() || structtype.isUnion
|
||||
}
|
||||
asmgen.out("; struct instances without initialization values, as BSS zeroed at startup\n")
|
||||
asmgen.out(" .section BSS\n")
|
||||
asmgen.out("${StStructInstanceBlockName}_bss .block\n")
|
||||
@@ -454,6 +457,27 @@ internal class ProgramAndVarsGen(
|
||||
asmgen.out(" .send STRUCTINSTANCES\n")
|
||||
}
|
||||
|
||||
private fun emitStructDefFields(structtype: StStruct, scalarValue: () -> String) {
|
||||
for(field in structtype.fields) {
|
||||
val effectiveDt = if(field.isArray) field.type.elementType() else field.type
|
||||
if(effectiveDt.isFloat) {
|
||||
val floatSize = compTarget.FLOAT_MEM_SIZE.toInt()
|
||||
if(field.isArray)
|
||||
asmgen.out("p8v_${field.name} .fill ${floatSize*field.arraySize!!}, ?")
|
||||
else
|
||||
asmgen.out("p8v_${field.name} .fill $floatSize, ${scalarValue()}")
|
||||
} else {
|
||||
val type = asmTypeString(field.type)
|
||||
if(field.isArray) {
|
||||
val anonymous = List(field.arraySize!!) { "?" }.joinToString(",")
|
||||
asmgen.out("p8v_${field.name} $type $anonymous")
|
||||
} else {
|
||||
asmgen.out("p8v_${field.name} $type ${scalarValue()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun translateAsmSubroutine(sub: PtAsmSub) {
|
||||
if(sub.inline) {
|
||||
return // subroutine gets inlined at call site.
|
||||
@@ -799,9 +823,6 @@ internal class ProgramAndVarsGen(
|
||||
asmgen.out("${variable.name}\t.fill $amount * $elementSize ; array of $amount ${variable.dt.elementType()} ")
|
||||
}
|
||||
dt.isPointer -> asmgen.out("${variable.name}\t.word ?") // a pointer is just an uword address
|
||||
dt.isPointerArray -> {
|
||||
TODO("pointers are not supported yet for uninitialized array ${variable.astNode?.position}")
|
||||
}
|
||||
else -> {
|
||||
throw AssemblyError("weird dt")
|
||||
}
|
||||
|
||||
@@ -136,6 +136,12 @@ internal class VariableAllocator(private val symboltable: SymbolTable,
|
||||
|
||||
private fun computeUsageScores(vars: Collection<StStaticVariable>): Map<String, Int> {
|
||||
val scores = mutableMapOf<String, Int>()
|
||||
// Note: by the time codegen runs, the simple-AST identifier names are already fully scoped
|
||||
// AND symbol-prefixed ("p8b_maze.p8s_solve.p8v_cx"), so a plain lookup by PtIdentifier.name
|
||||
// correctly matches the scopedNameString keys used here. This looks like a name/scope
|
||||
// mismatch bug but is not: resolving identifiers to their enclosing scope instead (or
|
||||
// keying this map by the bare variable name) silently makes every score zero, which
|
||||
// degenerates the sort in allocateZeropageVariables() into a stable no-op.
|
||||
val varInfo = vars.associateBy { it.scopedNameString }
|
||||
var loopDepth = 0
|
||||
|
||||
|
||||
@@ -68,10 +68,15 @@ internal class AssignmentAsmGen(
|
||||
|
||||
// Check if we need to save A register before extracting status flags
|
||||
// (flag extraction functions like assignCarryFlagResult overwrite A)
|
||||
val hasByteInA = registersResults.any { (ret, _) ->
|
||||
// A holds the low byte of a byte/bool return in @A, and of a word return in @AX/@AY
|
||||
val byteInA = registersResults.any { (ret, _) ->
|
||||
ret.type.isByteOrBool && ret.register.registerOrPair == RegisterOrPair.A
|
||||
}
|
||||
if(hasByteInA && statusFlagResults.isNotEmpty()) {
|
||||
val wordLowByteInA = registersResults.any { (ret, _) ->
|
||||
ret.type.isWord && ret.register.registerOrPair in setOf(RegisterOrPair.AX, RegisterOrPair.AY)
|
||||
}
|
||||
val hasAInResult = byteInA || wordLowByteInA
|
||||
if(hasAInResult && statusFlagResults.isNotEmpty()) {
|
||||
asmgen.out(" pha") // Save A (contains return value) before flag extraction
|
||||
}
|
||||
|
||||
@@ -101,7 +106,7 @@ internal class AssignmentAsmGen(
|
||||
}
|
||||
|
||||
// Restore A if we saved it for flag extraction
|
||||
if(hasByteInA && statusFlagResults.isNotEmpty()) {
|
||||
if(hasAInResult && statusFlagResults.isNotEmpty()) {
|
||||
asmgen.out(" pla")
|
||||
}
|
||||
|
||||
|
||||
+116
-3
@@ -2,6 +2,8 @@ package prog8.codegen.cpu6502.assignment
|
||||
|
||||
import prog8.code.ast.*
|
||||
import prog8.code.core.*
|
||||
import prog8.code.cpu6502.byteShiftAddExpansion
|
||||
import prog8.code.cpu6502.wordShiftAddExpansion
|
||||
import prog8.codegen.cpu6502.AsmGen6502Internal
|
||||
import prog8.codegen.cpu6502.VariableAllocator
|
||||
import prog8.codegen.cpu6502.toLongHex
|
||||
@@ -119,6 +121,96 @@ internal class AugmentableAssignmentAsmGen(private val program: PtProgram,
|
||||
}
|
||||
}
|
||||
|
||||
private fun inplaceModificationPointerField(target: AsmAssignTarget, operator: String, value: AsmAssignSource) {
|
||||
// the pointer to the target field is materialized in the shared scratch pointer
|
||||
// P8ZP_SCRATCH_PTR because the pointer variable isn't in the zeropage (or the
|
||||
// deref chain is too complex). Evaluating the source value may clobber that
|
||||
// scratch pointer, so we operate on a temporary variable and preserve the
|
||||
// address on the hardware stack, mirroring inplaceModificationIndexedPointerField.
|
||||
val elementDt = target.datatype
|
||||
if(!elementDt.isByteOrBool && !elementDt.isWord && !elementDt.isLong)
|
||||
throw AssemblyError("inplace modification of pointer field for type $elementDt not supported ${target.position}")
|
||||
val ptrTarget = PtrTarget(target)
|
||||
val (zpPtrVar, offset) = ptrgen.deref(ptrTarget.pointer)
|
||||
require(zpPtrVar=="P8ZP_SCRATCH_PTR")
|
||||
|
||||
val elementTemp = asmgen.createTempVarReused(elementDt.base, false, target.origAstTarget ?: target.pointer!!)
|
||||
if(elementDt.isByteOrBool) {
|
||||
asmgen.out("""
|
||||
ldy #$offset
|
||||
lda ($zpPtrVar),y
|
||||
sta $elementTemp""")
|
||||
} else if(elementDt.isWord) {
|
||||
asmgen.out("""
|
||||
ldy #$offset
|
||||
lda ($zpPtrVar),y
|
||||
sta $elementTemp
|
||||
iny
|
||||
lda ($zpPtrVar),y
|
||||
sta $elementTemp+1""")
|
||||
} else {
|
||||
asmgen.out("""
|
||||
ldy #$offset
|
||||
lda ($zpPtrVar),y
|
||||
sta $elementTemp
|
||||
iny
|
||||
lda ($zpPtrVar),y
|
||||
sta $elementTemp+1
|
||||
iny
|
||||
lda ($zpPtrVar),y
|
||||
sta $elementTemp+2
|
||||
iny
|
||||
lda ($zpPtrVar),y
|
||||
sta $elementTemp+3""")
|
||||
}
|
||||
|
||||
// save the computed address on the hardware stack so later code can't clobber it
|
||||
asmgen.out("""
|
||||
lda $zpPtrVar
|
||||
pha
|
||||
lda $zpPtrVar+1
|
||||
pha""")
|
||||
|
||||
// reuse the existing variable-target logic to modify the temp
|
||||
val tempTarget = AsmAssignTarget(TargetStorageKind.VARIABLE, asmgen, elementDt, null, target.position, variableAsmName = elementTemp)
|
||||
inplaceModification(tempTarget, operator, value)
|
||||
|
||||
// restore the address and store the temp back
|
||||
asmgen.out("""
|
||||
pla
|
||||
sta $zpPtrVar+1
|
||||
pla
|
||||
sta $zpPtrVar""")
|
||||
if(elementDt.isByteOrBool) {
|
||||
asmgen.out("""
|
||||
ldy #$offset
|
||||
lda $elementTemp
|
||||
sta ($zpPtrVar),y""")
|
||||
} else if(elementDt.isWord) {
|
||||
asmgen.out("""
|
||||
ldy #$offset
|
||||
lda $elementTemp
|
||||
sta ($zpPtrVar),y
|
||||
iny
|
||||
lda $elementTemp+1
|
||||
sta ($zpPtrVar),y""")
|
||||
} else {
|
||||
asmgen.out("""
|
||||
ldy #$offset
|
||||
lda $elementTemp
|
||||
sta ($zpPtrVar),y
|
||||
iny
|
||||
lda $elementTemp+1
|
||||
sta ($zpPtrVar),y
|
||||
iny
|
||||
lda $elementTemp+2
|
||||
sta ($zpPtrVar),y
|
||||
iny
|
||||
lda $elementTemp+3
|
||||
sta ($zpPtrVar),y""")
|
||||
}
|
||||
}
|
||||
|
||||
private fun inplaceModification(target: AsmAssignTarget, operator: String, value: AsmAssignSource) {
|
||||
|
||||
// the asm-gen code can deal with situations where you want to assign a byte into a word.
|
||||
@@ -611,7 +703,19 @@ internal class AugmentableAssignmentAsmGen(private val program: PtProgram,
|
||||
}
|
||||
}
|
||||
}
|
||||
TargetStorageKind.POINTER -> ptrgen.inplaceModification(PtrTarget(target), operator, value)
|
||||
TargetStorageKind.POINTER -> {
|
||||
// if the effective target address ends up in the shared scratch pointer
|
||||
// P8ZP_SCRATCH_PTR, evaluating the source value could clobber it. In that
|
||||
// case operate on a protected temporary value instead.
|
||||
val usesOffsetPointer = target.datatype.isFloat || operator in setOf("*", "/", "%")
|
||||
val needsProtectedPtr = ptrgen.derefUsesScratchPointer(target.pointer!!, usesOffsetPointer)
|
||||
if (value.kind in setOf(SourceStorageKind.EXPRESSION, SourceStorageKind.REGISTER)
|
||||
&& needsProtectedPtr
|
||||
&& (target.datatype.isByteOrBool || target.datatype.isWord || target.datatype.isLong))
|
||||
inplaceModificationPointerField(target, operator, value)
|
||||
else
|
||||
ptrgen.inplaceModification(PtrTarget(target), operator, value)
|
||||
}
|
||||
TargetStorageKind.REGISTER -> throw AssemblyError("no asm gen for reg in-place modification")
|
||||
TargetStorageKind.VOID -> { /* do nothing */ }
|
||||
}
|
||||
@@ -2143,6 +2247,11 @@ $shortcutLabel:""")
|
||||
"*" -> {
|
||||
if(value in asmgen.optimizedByteMultiplications)
|
||||
asmgen.out(" lda $name | jsr prog8_math.mul_byte_$value | sta $name")
|
||||
else if(byteShiftAddExpansion(value)!=null) {
|
||||
asmgen.out(" lda $name")
|
||||
asmgen.emitByteShiftAdd(value)
|
||||
asmgen.out(" sta $name")
|
||||
}
|
||||
else
|
||||
asmgen.out(" lda $name | ldy #$value | jsr prog8_math.multiply_bytes | sta $name")
|
||||
}
|
||||
@@ -2454,8 +2563,7 @@ $shortcutLabel:""")
|
||||
if(value in asmgen.optimizedWordMultiplications) {
|
||||
asmgen.out(" lda $lsb | ldy $msb | jsr prog8_math.mul_word_$value | sta $lsb | sty $msb")
|
||||
} else {
|
||||
if(block?.options?.veraFxMuls==true) {
|
||||
// cx16 verafx hardware mul
|
||||
if(block?.options?.veraFxMuls==true) { // cx16 verafx hardware mul
|
||||
asmgen.out("""
|
||||
lda $lsb
|
||||
ldy $msb
|
||||
@@ -2469,6 +2577,11 @@ $shortcutLabel:""")
|
||||
sta $lsb
|
||||
sty $msb""")
|
||||
}
|
||||
else if(wordShiftAddExpansion(value)!=null) {
|
||||
asmgen.out(" lda $lsb | ldy $msb")
|
||||
asmgen.emitWordShiftAdd(value)
|
||||
asmgen.out(" sta $lsb | sty $msb")
|
||||
}
|
||||
else
|
||||
asmgen.out("""
|
||||
lda $lsb
|
||||
|
||||
@@ -3,6 +3,8 @@ package prog8.codegen.cpu6502.assignment
|
||||
import prog8.code.StExtSub
|
||||
import prog8.code.ast.*
|
||||
import prog8.code.core.*
|
||||
import prog8.code.cpu6502.byteShiftAddExpansion
|
||||
import prog8.code.cpu6502.wordShiftAddExpansion
|
||||
import prog8.codegen.cpu6502.AsmGen6502Internal
|
||||
import prog8.codegen.cpu6502.toLongHex
|
||||
import kotlin.math.log2
|
||||
@@ -412,6 +414,26 @@ internal class BinaryOpAssignmentsGen(
|
||||
asmgen.out(" jsr verafx.muls16")
|
||||
assignmentAsmGen.assignRegisterpairWord(target, RegisterOrPair.AY)
|
||||
return true
|
||||
} else if (asmgen.unwrapCasts(expr.left).type.isUnsignedByte || asmgen.unwrapCasts(expr.right).type.isUnsignedByte) {
|
||||
// One operand is an unsigned byte (possibly under an implicit widening cast,
|
||||
// hence unwrapCasts). The general path would zero-extend it into a full word
|
||||
// and run 16 shift-add iterations even though only 8 bits are significant, so
|
||||
// use the 16x8 routine instead. isUnsignedByte matters: a signed byte is
|
||||
// sign-extended into A/Y and multiply_ubyte_word deliberately ignores the high
|
||||
// byte, so it would compute the unsigned product instead.
|
||||
//
|
||||
// multiply_ubyte_word wants the 8-bit value in A and the word in the variable.
|
||||
// assignWordOperandsToAYAndVar(left, right, var) puts `left` in AY and `right`
|
||||
// in the variable, so the byte has to be passed as the `left` argument. Only
|
||||
// the low 16 bits of the product are wanted and that is commutative mod 2^16,
|
||||
// so swapping the operands is safe. (Prog8 rejects mixed signedness, so the
|
||||
// other operand here is always an unsigned word.)
|
||||
if (asmgen.unwrapCasts(expr.left).type.isUnsignedByte)
|
||||
asmgen.assignWordOperandsToAYAndVar(expr.left, expr.right, "prog8_math.multiply_ubyte_word.multiplier")
|
||||
else
|
||||
asmgen.assignWordOperandsToAYAndVar(expr.right, expr.left, "prog8_math.multiply_ubyte_word.multiplier")
|
||||
asmgen.out(" jsr prog8_math.multiply_ubyte_word")
|
||||
assignmentAsmGen.assignRegisterpairWord(target, RegisterOrPair.AY)
|
||||
} else {
|
||||
asmgen.assignWordOperandsToAYAndVar(expr.right, expr.left, "prog8_math.multiply_words.multiplier")
|
||||
asmgen.out(" jsr prog8_math.multiply_words")
|
||||
@@ -449,6 +471,8 @@ internal class BinaryOpAssignmentsGen(
|
||||
repeat(shifts) { asmgen.out(" asl a") }
|
||||
}
|
||||
}
|
||||
else if(byteShiftAddExpansion(value)!=null)
|
||||
asmgen.emitByteShiftAdd(value)
|
||||
else
|
||||
asmgen.out(" ldy #$value | jsr prog8_math.multiply_bytes")
|
||||
assignmentAsmGen.assignRegisterByte(target, CpuRegister.A, false, true)
|
||||
@@ -485,6 +509,9 @@ internal class BinaryOpAssignmentsGen(
|
||||
sta cx16.r0
|
||||
sty cx16.r0+1
|
||||
jsr verafx.muls16""")
|
||||
} else if(wordShiftAddExpansion(value)!=null) {
|
||||
assignmentAsmGen.assignExpressionToRegister(expr.left, RegisterOrPair.AY, expr.type.isSigned)
|
||||
asmgen.emitWordShiftAdd(value)
|
||||
} else {
|
||||
asmgen.assignWordOperandsToAYAndVar(expr.right, expr.left, "prog8_math.multiply_words.multiplier")
|
||||
asmgen.out(" jsr prog8_math.multiply_words")
|
||||
@@ -701,13 +728,13 @@ internal class BinaryOpAssignmentsGen(
|
||||
if(target.kind==TargetStorageKind.VARIABLE) {
|
||||
asmgen.assignExpressionTo(expr.left, target)
|
||||
require(expr.right.type.isByte)
|
||||
asmgen.out(" lda #<${target.asmVarname} | ldy #>${target.asmVarname}")
|
||||
asmgen.out(" ldx #$shifts")
|
||||
if (expr.operator == "<<") {
|
||||
asmgen.out(" jsr prog8_lib.long_shiftleftX_inplace")
|
||||
} else {
|
||||
asmgen.out(" jsr prog8_lib.long_shiftrightX_inplace")
|
||||
}
|
||||
// The shift count is a compile-time constant here and the value is already in the
|
||||
// target variable, so shift it in place directly. prog8_lib's long_shift*X_inplace
|
||||
// takes a pointer and shifts through (zp),y inside a loop, which costs ~67 cycles
|
||||
// per bit; inplacemodificationLongWithLiteralval already unrolls the small counts
|
||||
// with plain asl/rol on the 4 bytes, and its '>>' keeps the same arithmetic
|
||||
// (sign-preserving) semantics as the library routine.
|
||||
assignmentAsmGen.augmentableAsmGen.inplacemodificationLongWithLiteralval(target.asmVarname, expr.operator, shifts)
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -290,7 +290,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
|
||||
return "P8ZP_SCRATCH_PTR" to 0u
|
||||
}
|
||||
else
|
||||
return pointer.startpointer.name to field.second
|
||||
return pointer.startpointer.name to field.second.toUByte()
|
||||
}
|
||||
else {
|
||||
// have to copy it to temp zp var
|
||||
@@ -300,7 +300,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
|
||||
return "P8ZP_SCRATCH_PTR" to 0u
|
||||
}
|
||||
else
|
||||
return "P8ZP_SCRATCH_PTR" to field.second
|
||||
return "P8ZP_SCRATCH_PTR" to field.second.toUByte()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -333,10 +333,22 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
|
||||
updateScratchPointer()
|
||||
return "P8ZP_SCRATCH_PTR" to 0u
|
||||
} else
|
||||
return "P8ZP_SCRATCH_PTR" to fieldinfo.second
|
||||
return "P8ZP_SCRATCH_PTR" to fieldinfo.second.toUByte()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun derefUsesScratchPointer(pointer: PtPointerDeref, addOffsetToPointer: Boolean=false): Boolean {
|
||||
// determine if deref() would need to keep the effective address in the shared
|
||||
// scratch pointer P8ZP_SCRATCH_PTR, which a subsequent evaluation of a source
|
||||
// expression could then clobber. Only if it doesn't, may the pointer itself (if
|
||||
// located in the zeropage) keep being used for the read-modify-write.
|
||||
if(pointer.chain.isEmpty())
|
||||
return !allocator.isZpVar(pointer.startpointer.name)
|
||||
if(pointer.chain.size > 1 || addOffsetToPointer)
|
||||
return true
|
||||
return !(allocator.isZpVar(pointer.startpointer.name) && !pointer.derefLast)
|
||||
}
|
||||
|
||||
internal fun indexedEffectiveAddress(target: IndexedPtrTarget): String {
|
||||
val eltSize = asmgen.program.memsizer.memorySize(target.elementDt, null)
|
||||
// generic: evaluate index as word, scale by element size, add field-corrected base pointer
|
||||
@@ -946,7 +958,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
|
||||
val right = binExpr.right as? PtIdentifier
|
||||
require(binExpr.operator=="." && left!=null && right!=null) {"invalid dereference expression ${binExpr.position}"}
|
||||
|
||||
val field: Pair<DataType, UByte>
|
||||
val field: Pair<DataType, Int>
|
||||
var extraFieldOffset = 0
|
||||
|
||||
if(left.type.isStructInstance) {
|
||||
@@ -977,7 +989,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
|
||||
field = struct.getField(right.name, asmgen.program.memsizer)
|
||||
}
|
||||
|
||||
val offset = extraFieldOffset + field.second.toInt()
|
||||
val offset = extraFieldOffset + field.second
|
||||
// TODO avoid always using a zp scratch reg below if the only thing we dereferenced above is a pointer variable that is already in zeropage itself
|
||||
if(offset>=256) {
|
||||
// add field offset to pointer if it doesn't fit in the index register
|
||||
|
||||
@@ -9,7 +9,15 @@ import prog8.code.core.BaseDataType
|
||||
import prog8.code.core.DataType
|
||||
import prog8.code.target.C64Target
|
||||
import prog8.code.target.VMTarget
|
||||
import prog8.codegen.cpu6502.*
|
||||
import prog8.codegen.cpu6502.hasInstr
|
||||
import prog8.codegen.cpu6502.haslabel
|
||||
import prog8.codegen.cpu6502.isBranch
|
||||
import prog8.codegen.cpu6502.isLoadReg
|
||||
import prog8.codegen.cpu6502.isStoreReg
|
||||
import prog8.codegen.cpu6502.isStoreRegOrZero
|
||||
import prog8.codegen.cpu6502.keeplabel
|
||||
import prog8.codegen.cpu6502.modifiesYRegister
|
||||
import prog8.codegen.cpu6502.optimizeAssembly
|
||||
|
||||
|
||||
class TestAsmOptimizer: FunSpec({
|
||||
@@ -262,6 +270,106 @@ class TestAsmOptimizer: FunSpec({
|
||||
" nop", " nop", " nop", " nop", " nop", " nop", " nop", " nop")
|
||||
}
|
||||
|
||||
// --- optimizeSamePointerIndexing: guards on the index reload ---
|
||||
|
||||
fun padded(vararg body: String): MutableList<String> =
|
||||
(body.toList() + List(14 - body.size) { " nop" }).toMutableList()
|
||||
|
||||
/** Same, but one line shorter, for cases where the optimizer is expected to drop a line. */
|
||||
fun paddedAfterDrop(vararg body: String): MutableList<String> =
|
||||
(body.toList() + List(13 - body.size) { " nop" }).toMutableList()
|
||||
|
||||
test("samePointerIndexing: KEEPS reload when the index variable is written in between") {
|
||||
val lines = padded(" ldy i", " lda arr,y", " inc i", " ldy i", " sta arr,y", " rts")
|
||||
optimize(lines)
|
||||
lines shouldBe padded(" ldy i", " lda arr,y", " inc i", " ldy i", " sta arr,y", " rts")
|
||||
}
|
||||
|
||||
test("samePointerIndexing: KEEPS reload when the index variable is decremented in between") {
|
||||
val lines = padded(" ldy i", " lda arr,y", " dec i", " ldy i", " sta arr,y", " rts")
|
||||
optimize(lines)
|
||||
lines shouldBe padded(" ldy i", " lda arr,y", " dec i", " ldy i", " sta arr,y", " rts")
|
||||
}
|
||||
|
||||
test("samePointerIndexing: KEEPS reload across a jsr") {
|
||||
val lines = padded(" ldy #0", " lda (ptr),y", " jsr someSub", " ldy #0", " sta (ptr),y", " rts")
|
||||
optimize(lines)
|
||||
lines shouldBe padded(" ldy #0", " lda (ptr),y", " jsr someSub", " ldy #0", " sta (ptr),y", " rts")
|
||||
}
|
||||
|
||||
test("samePointerIndexing: KEEPS reload across a label") {
|
||||
val lines = padded(" ldy #0", " lda (ptr),y", "someLabel:", " ldy #0", " sta (ptr),y", " rts")
|
||||
optimize(lines)
|
||||
lines shouldBe padded(" ldy #0", " lda (ptr),y", "someLabel:", " ldy #0", " sta (ptr),y", " rts")
|
||||
}
|
||||
|
||||
test("samePointerIndexing: KEEPS reload when an indirect store may alias the index") {
|
||||
val lines = padded(" ldy i", " lda arr,y", " sta (vptr),y", " ldy i", " lda arr2,y")
|
||||
optimize(lines)
|
||||
lines shouldBe padded(" ldy i", " lda arr,y", " sta (vptr),y", " ldy i", " lda arr2,y")
|
||||
}
|
||||
|
||||
test("samePointerIndexing: KEEPS reload when an intervening instruction modifies Y") {
|
||||
val lines = padded(" ldy #0", " lda (ptr),y", " tay", " ldy #0", " sta (ptr),y", " rts")
|
||||
optimize(lines)
|
||||
lines shouldBe padded(" ldy #0", " lda (ptr),y", " tay", " ldy #0", " sta (ptr),y", " rts")
|
||||
}
|
||||
|
||||
test("samePointerIndexing: KEEPS reload for an IO index operand") {
|
||||
val lines = padded($$" ldy c64.$d020", " lda arr,y", " lda arr2,y", $$" ldy c64.$d020", " lda arr3,y")
|
||||
optimize(lines)
|
||||
lines shouldBe padded($$" ldy c64.$d020", " lda arr,y", " lda arr2,y", $$" ldy c64.$d020", " lda arr3,y")
|
||||
}
|
||||
|
||||
test("samePointerIndexing: still drops reload when a store to an unrelated var intervenes") {
|
||||
val lines = padded(" ldy i", " lda arr,y", " sta other", " ldy i", " sta arr,y", " rts")
|
||||
optimize(lines)
|
||||
lines shouldBe paddedAfterDrop(" ldy i", " lda arr,y", " sta other", " sta arr,y", " rts")
|
||||
}
|
||||
|
||||
test("samePointerIndexing: KEEPS reload when the two ldy load DIFFERENT immediates") {
|
||||
// regression: seed[1] then seed[0] - the reload selects a different element
|
||||
val lines = padded(" ldy #1", " lda seed_msb,y", " sta x", " ldy #0", " lda seed_msb,y", " sta y", " rts")
|
||||
optimize(lines)
|
||||
lines shouldBe padded(" ldy #1", " lda seed_msb,y", " sta x", " ldy #0", " lda seed_msb,y", " sta y", " rts")
|
||||
}
|
||||
|
||||
test("samePointerIndexing: KEEPS reload when the two ldy load different variables") {
|
||||
val lines = padded(" ldy i", " lda arr,y", " sta x", " ldy j", " lda arr,y", " sta y", " rts")
|
||||
optimize(lines)
|
||||
lines shouldBe padded(" ldy i", " lda arr,y", " sta x", " ldy j", " lda arr,y", " sta y", " rts")
|
||||
}
|
||||
|
||||
test("samePointerIndexing: KEEPS reload when the two ldy differ by an offset") {
|
||||
val lines = padded(" ldy w+0", " lda arr,y", " sta x", " ldy w+1", " lda arr,y", " sta y", " rts")
|
||||
optimize(lines)
|
||||
lines shouldBe padded(" ldy w+0", " lda arr,y", " sta x", " ldy w+1", " lda arr,y", " sta y", " rts")
|
||||
}
|
||||
|
||||
test("samePointerIndexing: matched load/store arm also requires the same index") {
|
||||
val lines = padded(" ldy i", " lda arr,y", " clc", " ldy j", " sta arr,y", " rts")
|
||||
optimize(lines)
|
||||
lines shouldBe padded(" ldy i", " lda arr,y", " clc", " ldy j", " sta arr,y", " rts")
|
||||
}
|
||||
|
||||
test("samePointerIndexing: relaxed arm drops reload for two indexed loads") {
|
||||
val lines = padded(" ldy sp", " lda cx,y", " sta cxv", " ldy sp", " lda cy,y", " sta cyv", " rts")
|
||||
optimize(lines)
|
||||
lines shouldBe paddedAfterDrop(" ldy sp", " lda cx,y", " sta cxv", " lda cy,y", " sta cyv", " rts")
|
||||
}
|
||||
|
||||
test("samePointerIndexing: relaxed arm does not fire when Y is modified") {
|
||||
val lines = padded(" ldy sp", " lda cx,y", " dey", " ldy sp", " lda cy,y", " rts")
|
||||
optimize(lines)
|
||||
lines shouldBe padded(" ldy sp", " lda cx,y", " dey", " ldy sp", " lda cy,y", " rts")
|
||||
}
|
||||
|
||||
test("samePointerIndexing: relaxed arm keeps reload when index is written in between") {
|
||||
val lines = padded(" ldy i", " lda arr,y", " inc i", " ldy i", " lda arr2,y", " rts")
|
||||
optimize(lines)
|
||||
lines shouldBe padded(" ldy i", " lda arr,y", " inc i", " ldy i", " lda arr2,y", " rts")
|
||||
}
|
||||
|
||||
// --- optimizeAddWordToSameVariableOrExtraRegisterLoadInWordStore ---
|
||||
|
||||
test("optimizeAddWordToSameVariable: optimizes P8ZP_SCRATCH_PTR += AY") {
|
||||
|
||||
@@ -33,9 +33,10 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
|
||||
if (extsub.returns.size == assignmentTargets.size) {
|
||||
// Targets and values match. Assign all the things. Skip 'void' targets.
|
||||
// We need to handle both FP registers (FAC1/FAC2) and regular CPU registers
|
||||
// Status flag returns MUST be processed first, because their branch-based
|
||||
// code needs to read CPU flags immediately after the call, before any
|
||||
// LOADHR/STOREM instructions clobber them.
|
||||
// Status flag returns must be read before anything that clobbers the CPU status
|
||||
// bits. When the call also has non-flag register returns, those are processed
|
||||
// first (they may clobber the scratch register used by the flag extraction),
|
||||
// protected by an outer PUSHST/POPST so the flags stay intact for the extraction.
|
||||
val fpRegs = funcCall.multipleResultFpRegs.toMutableList()
|
||||
val cpuRegs = funcCall.multipleResultRegs.toMutableList()
|
||||
|
||||
@@ -59,8 +60,40 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
|
||||
}
|
||||
}
|
||||
|
||||
flagPairs.forEach(::processPair)
|
||||
otherPairs.forEach(::processPair)
|
||||
fun isVoidTarget(pair: Pair<Pair<StExtSubParameter, PtNode>, Int>) =
|
||||
(pair.first.second as PtAssignTarget).void
|
||||
|
||||
// When multiple status flags are returned in one call, the branch-based
|
||||
// extraction of one flag clobbers the CPU status bits that a subsequent
|
||||
// flag still needs to read. Wrap every flag pair's handling in
|
||||
// PUSHST/POPST so each one sees the original status right after the call.
|
||||
val wrapStatusFlags = flagPairs.count { !isVoidTarget(it) } > 1
|
||||
fun processFlagPairs() {
|
||||
flagPairs.forEach { pair ->
|
||||
if (wrapStatusFlags && !isVoidTarget(pair)) {
|
||||
result += IRCodeChunk(null, null).also { it += IRInstructions.simple(Opcode.PUSHST) }
|
||||
processPair(pair)
|
||||
result += IRCodeChunk(null, null).also { it += IRInstructions.simple(Opcode.POPST) }
|
||||
} else {
|
||||
processPair(pair)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val hasStatusFlags = flagPairs.any { !isVoidTarget(it) }
|
||||
val hasOtherResults = otherPairs.any { !isVoidTarget(it) }
|
||||
if (hasStatusFlags && hasOtherResults) {
|
||||
// The non-flag register returns are read before the flag extraction,
|
||||
// so the extraction's scratch register cannot clobber them. The flag
|
||||
// bits are preserved across the register loads/stores with PUSHST/POPST.
|
||||
result += IRCodeChunk(null, null).also { it += IRInstructions.simple(Opcode.PUSHST) }
|
||||
otherPairs.forEach(::processPair)
|
||||
result += IRCodeChunk(null, null).also { it += IRInstructions.simple(Opcode.POPST) }
|
||||
processFlagPairs()
|
||||
} else {
|
||||
processFlagPairs()
|
||||
otherPairs.forEach(::processPair)
|
||||
}
|
||||
} else {
|
||||
throw AssemblyError("number of values and targets don't match")
|
||||
}
|
||||
@@ -222,7 +255,7 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
|
||||
val (addressReg, fieldOffset) = codeGen.evaluatePointerAddressIntoReg(inplaceInstrs, pointerDeref)
|
||||
val oldvalueReg = codeGen.registers.next(targetDt)
|
||||
|
||||
if((augAssign.operator=="+=" || augAssign.operator=="-=") && value.asConstInteger()==1 || value.asConstInteger()==2) {
|
||||
if((augAssign.operator=="+=" || augAssign.operator=="-=") && (value.asConstInteger()==1 || value.asConstInteger()==2)) {
|
||||
// INC/DEC optimization instead of ADD/SUB
|
||||
|
||||
loadfield(inplaceInstrs, addressReg, fieldOffset, targetDt, oldvalueReg)
|
||||
@@ -346,20 +379,20 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
|
||||
private fun loadfield(
|
||||
inplaceInstrs: MutableList<IRCodeChunkBase>,
|
||||
addressReg: Int,
|
||||
fieldOffset: UByte,
|
||||
fieldOffset: Int,
|
||||
targetDt: IRDataType,
|
||||
oldvalueReg: Int
|
||||
) {
|
||||
if (targetDt == IRDataType.FLOAT) {
|
||||
addInstr(
|
||||
inplaceInstrs,
|
||||
IRInstructions.loadMemory(Opcode.LOADI, targetDt, oldvalueReg, IRMemory.indirect(addressReg, fieldOffset.toInt())),
|
||||
IRInstructions.loadMemory(Opcode.LOADI, targetDt, oldvalueReg, IRMemory.indirect(addressReg, fieldOffset)),
|
||||
null
|
||||
)
|
||||
} else {
|
||||
addInstr(
|
||||
inplaceInstrs,
|
||||
IRInstructions.loadMemory(Opcode.LOADI, targetDt, oldvalueReg, IRMemory.indirect(addressReg, fieldOffset.toInt())),
|
||||
IRInstructions.loadMemory(Opcode.LOADI, targetDt, oldvalueReg, IRMemory.indirect(addressReg, fieldOffset)),
|
||||
null
|
||||
)
|
||||
}
|
||||
@@ -785,26 +818,26 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
|
||||
val offset = eltSize * constIndex
|
||||
addInstr(result, IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerReg, offset), null)
|
||||
} else {
|
||||
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, eltSize, true, targetArray.splitWords)
|
||||
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, true)
|
||||
result += code
|
||||
if(eltSize!=1)
|
||||
result += codeGen.multiplyByConst(DataType.UWORD, indexReg, eltSize)
|
||||
addInstr(result, IRInstructions.binary(Opcode.ADDR, IRDataType.POINTER, pointerReg, indexReg), null)
|
||||
}
|
||||
codeGen.storeValueAtPointersLocation(result, pointerReg, 0u, targetIdent.type.dereference(), true, -1)
|
||||
codeGen.storeValueAtPointersLocation(result, pointerReg, 0, targetIdent.type.dereference(), true, -1)
|
||||
} else {
|
||||
if(constIndex!=null) {
|
||||
val offset = eltSize * constIndex
|
||||
addInstr(result, IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerReg, offset), null)
|
||||
} else {
|
||||
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, eltSize, true, targetArray.splitWords)
|
||||
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, true)
|
||||
result += code
|
||||
if(eltSize!=1)
|
||||
result += codeGen.multiplyByConst(DataType.UWORD, indexReg, eltSize)
|
||||
addInstr(result, IRInstructions.binary(Opcode.ADDR, IRDataType.POINTER, pointerReg, indexReg), null)
|
||||
}
|
||||
val realValueReg = if(targetDt == IRDataType.FLOAT) valueFpRegister else valueRegister
|
||||
codeGen.storeValueAtPointersLocation(result, pointerReg, 0u, targetIdent.type.dereference(), false, realValueReg)
|
||||
codeGen.storeValueAtPointersLocation(result, pointerReg, 0, targetIdent.type.dereference(), false, realValueReg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -819,7 +852,6 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
|
||||
valueFpRegister: Int
|
||||
) {
|
||||
val fixedIndex = targetArray.index.asConstInteger()
|
||||
val arrayLength = codeGen.symbolTable.getLength(variable)
|
||||
if(zero) {
|
||||
if(fixedIndex!=null) {
|
||||
val chunk = IRCodeChunk(null, null).also {
|
||||
@@ -832,7 +864,7 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
|
||||
}
|
||||
result += chunk
|
||||
} else {
|
||||
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, eltSize, codeGen.wordArrayIndex, targetArray.splitWords)
|
||||
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, codeGen.wordArrayIndex)
|
||||
result += code
|
||||
val scaleZX = if(targetArray.splitWords) 1 else eltSize
|
||||
result += IRCodeChunk(null, null).also {
|
||||
@@ -853,7 +885,7 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
|
||||
}
|
||||
result += chunk
|
||||
} else {
|
||||
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, eltSize, codeGen.wordArrayIndex, targetArray.splitWords)
|
||||
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, codeGen.wordArrayIndex)
|
||||
result += code
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstructions.storeMemory(Opcode.STOREX, targetDt, valueFpRegister, IRMemory.indexed(variable, indexReg, codeGen.options.compTarget.indexRegType, scale=eltSize))
|
||||
@@ -874,7 +906,7 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
|
||||
}
|
||||
result += chunk
|
||||
} else {
|
||||
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, eltSize, codeGen.wordArrayIndex, targetArray.splitWords)
|
||||
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, codeGen.wordArrayIndex)
|
||||
result += code
|
||||
result += IRCodeChunk(null, null).also {
|
||||
if(targetArray.splitWords) {
|
||||
@@ -922,7 +954,7 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
|
||||
}
|
||||
} else {
|
||||
// index is an expression
|
||||
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, eltSize, true, targetArray.splitWords)
|
||||
val (code, indexReg) = codeGen.loadIndexReg(targetArray.index, true)
|
||||
result += code
|
||||
if(eltSize!=1)
|
||||
result += codeGen.multiplyByConst(DataType.UWORD, indexReg, eltSize)
|
||||
@@ -3030,7 +3062,7 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
|
||||
|
||||
private fun tryFoldStructArrayDirectWrite(addressExpr: PtExpression, zero: Boolean, valueReg: Int, targetDt: IRDataType, result: MutableList<IRCodeChunkBase>): Boolean {
|
||||
val info = extractStructArrayIndexInfo(addressExpr, codeGen) ?: return false
|
||||
val (idxCode, indexReg) = codeGen.loadIndexReg(info.idxExpr, info.structSize, codeGen.wordArrayIndex, false)
|
||||
val (idxCode, indexReg) = codeGen.loadIndexReg(info.idxExpr, codeGen.wordArrayIndex)
|
||||
result += idxCode
|
||||
if(zero) {
|
||||
result += IRCodeChunk(null, null).also {
|
||||
|
||||
@@ -876,7 +876,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
if(offset > 0)
|
||||
addInstr(result, IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerTr.resultReg, offset), null)
|
||||
} else {
|
||||
val (code, indexWordReg) = codeGen.loadIndexReg(target.index, eltSize, true, false)
|
||||
val (code, indexWordReg) = codeGen.loadIndexReg(target.index, true)
|
||||
result += code
|
||||
if(eltSize!=1)
|
||||
result += codeGen.multiplyByConst(DataType.UWORD, indexWordReg, eltSize)
|
||||
@@ -946,7 +946,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
it += IRInstructions.storeZero(Opcode.STOREZX, IRDataType.BYTE, IRMemory.indexed(targetVariable.name, offsetReg, codeGen.options.compTarget.indexRegType))
|
||||
}
|
||||
} else {
|
||||
val (code, indexReg) = codeGen.loadIndexReg(target.index, eltSize, codeGen.wordArrayIndex, false)
|
||||
val (code, indexReg) = codeGen.loadIndexReg(target.index, codeGen.wordArrayIndex)
|
||||
result += code
|
||||
result += IRCodeChunk(null, null).also {
|
||||
val offset = byteOffset(eltSize)
|
||||
@@ -964,7 +964,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
it += IRInstructions.storeMemory(Opcode.STOREX, IRDataType.BYTE, valueTr.resultReg, IRMemory.indexed(targetVariable.name, offsetReg, codeGen.options.compTarget.indexRegType))
|
||||
}
|
||||
} else {
|
||||
val (code, indexReg) = codeGen.loadIndexReg(target.index, eltSize, codeGen.wordArrayIndex, false)
|
||||
val (code, indexReg) = codeGen.loadIndexReg(target.index, codeGen.wordArrayIndex)
|
||||
result += code
|
||||
result += IRCodeChunk(null, null).also {
|
||||
val offset = byteOffset(eltSize)
|
||||
@@ -985,7 +985,7 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
|
||||
val info = extractStructArrayIndexInfo(call.args[0], codeGen) ?: return null
|
||||
val isZero = codeGen.isZero(call.args[1])
|
||||
val result = mutableListOf<IRCodeChunkBase>()
|
||||
val (idxCode, indexReg) = codeGen.loadIndexReg(info.idxExpr, info.structSize, codeGen.wordArrayIndex, false)
|
||||
val (idxCode, indexReg) = codeGen.loadIndexReg(info.idxExpr, codeGen.wordArrayIndex)
|
||||
result += idxCode
|
||||
return if(isZero) {
|
||||
result += IRCodeChunk(null, null).also {
|
||||
|
||||
@@ -186,7 +186,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
}
|
||||
|
||||
result += IRCodeChunk(null, null).also {
|
||||
it += IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerReg, firstField.second.toInt())
|
||||
it += IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerReg, firstField.second)
|
||||
if (firstField.first.isPointer) {
|
||||
// get the address stored in the pointer and use that for the rest of the chain
|
||||
// LOADI has an exception to allo reg1 and reg2 to be the same, so we can avoid using extra temporary registers and LOADs
|
||||
@@ -214,19 +214,19 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
|
||||
// For inline array fields (not pointer fields), return the address (ptr+offset) instead of loading from it
|
||||
if(deref.type.isArray && !deref.derefLast) {
|
||||
if(offset > 0u)
|
||||
addInstr(result, IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerReg, offset.toInt()), null)
|
||||
if(offset > 0)
|
||||
addInstr(result, IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerReg, offset), null)
|
||||
return ExpressionCodeResult(result, IRDataType.POINTER, pointerReg, -1)
|
||||
}
|
||||
|
||||
return if(deref.type.isFloat) {
|
||||
val resultReg = codeGen.registers.next(IRDataType.FLOAT)
|
||||
addInstr(result, IRInstructions.loadMemory(Opcode.LOADI, IRDataType.FLOAT, resultReg, IRMemory.indirect(pointerReg, offset.toInt())), null)
|
||||
addInstr(result, IRInstructions.loadMemory(Opcode.LOADI, IRDataType.FLOAT, resultReg, IRMemory.indirect(pointerReg, offset)), null)
|
||||
ExpressionCodeResult(result, IRDataType.FLOAT, -1, resultReg)
|
||||
} else {
|
||||
val irdt = codeGen.irType(deref.type)
|
||||
val resultReg = codeGen.registers.next(irdt)
|
||||
addInstr(result, IRInstructions.loadMemory(Opcode.LOADI, irdt, resultReg, IRMemory.indirect(pointerReg, offset.toInt())), null)
|
||||
addInstr(result, IRInstructions.loadMemory(Opcode.LOADI, irdt, resultReg, IRMemory.indirect(pointerReg, offset)), null)
|
||||
ExpressionCodeResult(result, irdt, resultReg, -1)
|
||||
}
|
||||
}
|
||||
@@ -436,7 +436,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
result += pointerTr.chunks
|
||||
val (instructions, offset) = traverseRestOfDerefChainToCalculateFinalAddress(expr.dereference!!, pointerTr.resultReg)
|
||||
result += instructions
|
||||
addInstr(result, IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerTr.resultReg, offset.toInt()), null)
|
||||
addInstr(result, IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerTr.resultReg, offset), null)
|
||||
return ExpressionCodeResult(result, vmDt, pointerTr.resultReg, -1)
|
||||
}
|
||||
}
|
||||
@@ -645,7 +645,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
}
|
||||
|
||||
fun indexByExpression() {
|
||||
val (code, indexReg) = codeGen.loadIndexReg(arrayIx.index, eltSize, codeGen.wordArrayIndex, arrayIx.splitWords)
|
||||
val (code, indexReg) = codeGen.loadIndexReg(arrayIx.index, codeGen.wordArrayIndex)
|
||||
result += code
|
||||
val scale = if(arrayIx.splitWords) 1 else eltSize
|
||||
if(vmDt==IRDataType.FLOAT) {
|
||||
@@ -681,7 +681,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
addInstr(result, IRInstructions.binaryImmediate(Opcode.ADD, IRDataType.POINTER, pointerReg, memOffset), null)
|
||||
}
|
||||
else {
|
||||
val (code, indexWordReg) = codeGen.loadIndexReg(index, eltSize, true, false)
|
||||
val (code, indexWordReg) = codeGen.loadIndexReg(index, true)
|
||||
result += code
|
||||
if(eltSize!=1)
|
||||
result += codeGen.multiplyByConst(DataType.UWORD, indexWordReg, eltSize)
|
||||
@@ -931,9 +931,6 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
}
|
||||
}
|
||||
}
|
||||
BaseDataType.ARRAY_POINTER -> {
|
||||
TODO("typecast to array of pointers $valueDt -> ${cast.type} ${cast.position}")
|
||||
}
|
||||
else -> throw AssemblyError("weird cast value type ${cast.position}")
|
||||
}
|
||||
|
||||
@@ -1876,7 +1873,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
val right = binExpr.right as? PtIdentifier
|
||||
require(binExpr.operator=="." && left!=null && right!=null) {"invalid dereference expression ${binExpr.position}"}
|
||||
val result = mutableListOf<IRCodeChunkBase>()
|
||||
val field: Pair<DataType, UByte>
|
||||
val field: Pair<DataType, Int>
|
||||
val pointerReg: Int
|
||||
var extraFieldOffset = 0
|
||||
|
||||
@@ -1886,7 +1883,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
val struct = left.type.subType!! as StStruct
|
||||
field = struct.getField(right.name, codeGen.program.memsizer)
|
||||
val fieldVmDt = codeGen.irType(field.first)
|
||||
val fieldOffset = field.second.toInt()
|
||||
val fieldOffset = field.second
|
||||
if(left.variable!=null) {
|
||||
val constindex = left.index as? PtNumber
|
||||
if(constindex!=null) {
|
||||
@@ -1898,7 +1895,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
} else if(left.variable!!.type.isArray) {
|
||||
// variable index: fold to single LOADX with scale=structSize, disp=fieldOffset
|
||||
// only for known array symbols (not generic pointer derefs)
|
||||
val (chunks, indexReg) = codeGen.loadIndexReg(left.index, struct.size.toInt(), codeGen.wordArrayIndex, false)
|
||||
val (chunks, indexReg) = codeGen.loadIndexReg(left.index, codeGen.wordArrayIndex)
|
||||
result += chunks
|
||||
if (fieldVmDt == IRDataType.FLOAT) {
|
||||
val fpReg = codeGen.registers.next(IRDataType.FLOAT)
|
||||
@@ -1914,7 +1911,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
val pointerTr = translateExpression(left.variable!!)
|
||||
result += pointerTr.chunks
|
||||
pointerReg = pointerTr.resultReg
|
||||
val (chunks, indexReg) = codeGen.loadIndexReg(left.index, struct.size.toInt(), true, false)
|
||||
val (chunks, indexReg) = codeGen.loadIndexReg(left.index, true)
|
||||
result += chunks
|
||||
if(struct.size.toInt()!=1)
|
||||
result += codeGen.multiplyByConst(DataType.UWORD, indexReg, struct.size.toInt())
|
||||
@@ -1940,7 +1937,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
|
||||
// add field offset to pointer and load the value into the result register
|
||||
val fieldVmDt = codeGen.irType(field.first)
|
||||
val fieldOffset = field.second.toInt() + extraFieldOffset
|
||||
val fieldOffset = field.second + extraFieldOffset
|
||||
var resultFpReg = -1
|
||||
var resultReg = -1
|
||||
if (fieldVmDt == IRDataType.FLOAT)
|
||||
@@ -1955,14 +1952,14 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
return ExpressionCodeResult(result, fieldVmDt, resultReg, resultFpReg)
|
||||
}
|
||||
|
||||
internal fun traverseRestOfDerefChainToCalculateFinalAddress(targetPointerDeref: PtPointerDeref, pointerReg: Int): Pair<IRCodeChunks, UByte> {
|
||||
internal fun traverseRestOfDerefChainToCalculateFinalAddress(targetPointerDeref: PtPointerDeref, pointerReg: Int): Pair<IRCodeChunks, Int> {
|
||||
// returns instructions to calculate the pointer address, and the offset into the struct it points to
|
||||
// so that LOADI and STOREFIELD opcodes can be used instead of having an explicit extra ADD
|
||||
|
||||
val result = mutableListOf<IRCodeChunkBase>()
|
||||
|
||||
if(targetPointerDeref.chain.isEmpty())
|
||||
return result to 0u // nothing to do; there's no deref chain
|
||||
return result to 0 // nothing to do; there's no deref chain
|
||||
|
||||
var struct: StStruct? = null
|
||||
if(targetPointerDeref.startpointer.type.subType!=null)
|
||||
@@ -1978,11 +1975,11 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
|
||||
val field = targetPointerDeref.chain.last()
|
||||
val fieldinfo = struct!!.getField(field, codeGen.program.memsizer)
|
||||
if(fieldinfo.second>0u) {
|
||||
if(fieldinfo.second>0) {
|
||||
if(targetPointerDeref.derefLast) {
|
||||
require(fieldinfo.first.isPointer)
|
||||
result += updatePointerFromField(fieldinfo, pointerReg)
|
||||
return result to 0u
|
||||
return result to 0
|
||||
} else {
|
||||
return result to fieldinfo.second
|
||||
}
|
||||
@@ -1992,13 +1989,13 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
// LOADI has an exception to allow reg1 and reg2 to be the same, so we can avoid using extra temporary registers and LOADs
|
||||
addInstr(result, IRInstructions.loadMemory(Opcode.LOADI, IRDataType.POINTER, pointerReg, IRMemory.indirect(pointerReg, 0)), null)
|
||||
}
|
||||
return result to 0u
|
||||
return result to 0
|
||||
}
|
||||
|
||||
private fun updatePointerFromField(fieldinfo: Pair<DataType, UByte>, pointerReg: Int): IRCodeChunk {
|
||||
private fun updatePointerFromField(fieldinfo: Pair<DataType, Int>, pointerReg: Int): IRCodeChunk {
|
||||
return IRCodeChunk(null, null).also {
|
||||
// LOADI has an exception to allow reg1 and reg2 to be the same, so we can avoid using extra temporary registers and LOADs
|
||||
it += IRInstructions.loadMemory(Opcode.LOADI, IRDataType.POINTER, pointerReg, IRMemory.indirect(pointerReg, fieldinfo.second.toInt()))
|
||||
it += IRInstructions.loadMemory(Opcode.LOADI, IRDataType.POINTER, pointerReg, IRMemory.indirect(pointerReg, fieldinfo.second))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@ class IRCodeGen(
|
||||
replacements += IRStStaticVariable(
|
||||
variable.name, variable.dt, null, variable.length,
|
||||
variable.zpwish, variable.align, variable.dirty,
|
||||
variable.inBss, variable.readonly
|
||||
variable.inBss, variable.readonly, variable.shared
|
||||
)
|
||||
}
|
||||
is IRVariableInitializer.Str, is IRVariableInitializer.Array -> {
|
||||
@@ -274,7 +274,8 @@ class IRCodeGen(
|
||||
variable.align,
|
||||
variable.dirty,
|
||||
variable.inBss,
|
||||
variable.readonly
|
||||
variable.readonly,
|
||||
variable.shared
|
||||
)
|
||||
irProg.st.add(clearedVar)
|
||||
|
||||
@@ -1895,7 +1896,14 @@ class IRCodeGen(
|
||||
}
|
||||
|
||||
val value = ret.children.singleOrNull()
|
||||
if(value==null) {
|
||||
if(ret.numReturnValues()==0) {
|
||||
// a void subroutine may return the result of a void function call (e.g. "return voidfunc(args)"):
|
||||
// translate the call for its side effects, then return void
|
||||
val callValue = value as? PtExpression
|
||||
if(callValue!=null)
|
||||
result += expressionEval.translateExpression(callValue).chunks
|
||||
addInstr(result, IRInstructions.returnVoid(), null)
|
||||
} else if(value==null) {
|
||||
addInstr(result, IRInstructions.returnVoid(), null)
|
||||
} else {
|
||||
value as PtExpression
|
||||
@@ -1942,7 +1950,7 @@ class IRCodeGen(
|
||||
irBlock += chunk
|
||||
}
|
||||
is PtSub -> {
|
||||
val sub = IRSubroutine(child.name, translateParameters(child.signature.children), child.signature.returns, child.position)
|
||||
val sub = IRSubroutine(child.name, translateParameters(child.signature.children), child.signature.returns, child.position, noframe = child.noframe)
|
||||
for (subchild in child.children) {
|
||||
translateNode(subchild).forEach { sub += it }
|
||||
}
|
||||
@@ -2174,7 +2182,7 @@ class IRCodeGen(
|
||||
return chunk
|
||||
}
|
||||
|
||||
internal fun evaluatePointerAddressIntoReg(result: MutableList<IRCodeChunkBase>, deref: PtPointerDeref): Pair<Int, UByte> {
|
||||
internal fun evaluatePointerAddressIntoReg(result: MutableList<IRCodeChunkBase>, deref: PtPointerDeref): Pair<Int, Int> {
|
||||
// calculates the pointer address and returns the register it's in + remaining offset into the struct (so that LOADI/STOREFIELD instructions can be used)
|
||||
val pointerTr = expressionEval.translateExpression(deref.startpointer)
|
||||
result += pointerTr.chunks
|
||||
@@ -2183,8 +2191,8 @@ class IRCodeGen(
|
||||
return pointerTr.resultReg to offset
|
||||
}
|
||||
|
||||
internal fun storeValueAtPointersLocation(result: MutableList<IRCodeChunkBase>, addressReg: Int, offset: UByte, type: DataType, valueIsZero: Boolean, existingValueRegister: Int) {
|
||||
if(offset<=0u) {
|
||||
internal fun storeValueAtPointersLocation(result: MutableList<IRCodeChunkBase>, addressReg: Int, offset: Int, type: DataType, valueIsZero: Boolean, existingValueRegister: Int) {
|
||||
if(offset<=0) {
|
||||
val irdt = irType(type)
|
||||
val instr = if(type.isFloat) {
|
||||
if (valueIsZero) IRInstructions.storeZero(Opcode.STOREZI, IRDataType.FLOAT, IRMemory.indirect(addressReg, 0))
|
||||
@@ -2201,17 +2209,17 @@ class IRCodeGen(
|
||||
val valueRegister = existingValueRegister
|
||||
val irdt = irType(type)
|
||||
if(valueIsZero && valueRegister<0) {
|
||||
addInstr(result, IRInstructions.storeZero(Opcode.STOREZI, irdt, IRMemory.indirect(addressReg, offset.toInt())), null)
|
||||
addInstr(result, IRInstructions.storeZero(Opcode.STOREZI, irdt, IRMemory.indirect(addressReg, offset)), null)
|
||||
} else {
|
||||
val instr = if (type.isFloat)
|
||||
IRInstructions.storeMemory(Opcode.STOREI, IRDataType.FLOAT, valueRegister, IRMemory.indirect(addressReg, offset.toInt()))
|
||||
IRInstructions.storeMemory(Opcode.STOREI, IRDataType.FLOAT, valueRegister, IRMemory.indirect(addressReg, offset))
|
||||
else
|
||||
IRInstructions.storeMemory(Opcode.STOREI, irdt, valueRegister, IRMemory.indirect(addressReg, offset.toInt()))
|
||||
IRInstructions.storeMemory(Opcode.STOREI, irdt, valueRegister, IRMemory.indirect(addressReg, offset))
|
||||
addInstr(result, instr, null)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun loadIndexReg(index: PtExpression, itemsize: Int, wordIndex: Boolean, arrayIsSplitWords: Boolean): Pair<IRCodeChunks, Int> {
|
||||
internal fun loadIndexReg(index: PtExpression, wordIndex: Boolean): Pair<IRCodeChunks, Int> {
|
||||
// returns the code to load the Index into the register, which is also returned.
|
||||
// The returned register is always canonicalized to the expected index width
|
||||
// (WORD on 32-bit targets, BYTE on 8-bit) so that LOADX/STOREX/STOREZX never
|
||||
|
||||
@@ -1158,13 +1158,18 @@ jump p8_label_gen_2
|
||||
mods.add(Mod(i+1, IRInstructions.binaryImmediate(Opcode.AND, IRDataType.LONG, outer.requireDest().registerNumber, 0xffff)))
|
||||
mods.add(Mod(i, IRInstructions.move(IRDataType.LONG, outer.requireDest().registerNumber, inner.requireSrcA().registerNumber)))
|
||||
}
|
||||
// truncate-long/word-to-byte followed by zero-extend byte-to-word == mask with $ff
|
||||
// truncate-long/word-to-byte followed by zero-extend byte-to-word == mask with $ff.
|
||||
// This is only valid when the source and destination have the same type, because
|
||||
// the rewritten loadr would otherwise access the source register with a different
|
||||
// type than its definition (e.g. long source to word dest).
|
||||
inner.opcode==Opcode.LSIGB && inner.type in setOf(IRDataType.WORD, IRDataType.LONG)
|
||||
&& outer.opcode==Opcode.EXT && outer.type==IRDataType.BYTE
|
||||
&& mid!=null && mid==outer.srcA?.register && inner.srcA!=null && outer.dest!=null
|
||||
&& readCounts.getValue(mid)==1 -> {
|
||||
mods.add(Mod(i+1, IRInstructions.binaryImmediate(Opcode.AND, inner.type!!, outer.requireDest().registerNumber, 0xff)))
|
||||
mods.add(Mod(i, IRInstructions.move(inner.type!!, outer.requireDest().registerNumber, inner.requireSrcA().registerNumber)))
|
||||
&& readCounts.getValue(mid)==1
|
||||
&& inner.type == outer.requireDest().type -> {
|
||||
val resultType = outer.requireDest().type
|
||||
mods.add(Mod(i+1, IRInstructions.binaryImmediate(Opcode.AND, resultType, outer.requireDest().registerNumber, 0xff)))
|
||||
mods.add(Mod(i, IRInstructions.move(resultType, outer.requireDest().registerNumber, inner.requireSrcA().registerNumber)))
|
||||
}
|
||||
else -> continue
|
||||
}
|
||||
|
||||
@@ -8,6 +8,18 @@ class IRUnusedCodeRemover(
|
||||
private val irprog: IRProgram,
|
||||
private val errors: IErrorReporter
|
||||
) {
|
||||
/**
|
||||
* The symbols that struct instance initializers take the address of. These are plain data
|
||||
* references: nothing ever calls them, so without this the code they point at is considered
|
||||
* unused and removed, leaving a dangling reference in the instance initializer.
|
||||
* The same reasoning applies to the array initializers of variables, which are handled
|
||||
* separately at each of the use sites below.
|
||||
*/
|
||||
private fun structInstanceInitRefs(): Sequence<IRStSymbolicReference.Symbol> =
|
||||
irprog.st.allStructInstances()
|
||||
.flatMap { instance -> instance.values.asSequence().map { it.value } }
|
||||
.filterIsInstance<IRStSymbolicReference.Symbol>()
|
||||
|
||||
fun optimize(): Int {
|
||||
var numRemoved = removeUnusedSubroutines() + removeUnusedAsmSubroutines()
|
||||
|
||||
@@ -52,6 +64,14 @@ class IRUnusedCodeRemover(
|
||||
return // symbol occurs in an initializer value (address-of this symbol)_
|
||||
}
|
||||
}
|
||||
|
||||
irprog.st.allStructInstances().forEach { instance ->
|
||||
if(instance.values.any { fieldValue ->
|
||||
val ref = fieldValue.value
|
||||
ref is IRStSymbolicReference.Symbol && ref.name.startsWith(blockLabel)
|
||||
})
|
||||
return // symbol occurs in a struct instance initializer value
|
||||
}
|
||||
}
|
||||
|
||||
val blockStructs = irprog.st.allStructDefs().filter { it.name.startsWith(prefix) }
|
||||
@@ -221,6 +241,11 @@ class IRUnusedCodeRemover(
|
||||
}
|
||||
}
|
||||
|
||||
// ... and likewise the ones referenced in struct instance initializers:
|
||||
structInstanceInitRefs()
|
||||
.filter { irprog.st.lookup(it.name) == null }
|
||||
.forEach { ref -> irprog.getChunkWithLabel(ref.name).let { reachable += it } }
|
||||
|
||||
fun grow() {
|
||||
val new = mutableSetOf<IRCodeChunkBase>()
|
||||
reachable.forEach {
|
||||
@@ -277,6 +302,11 @@ class IRUnusedCodeRemover(
|
||||
}
|
||||
}
|
||||
|
||||
// ... and likewise the chunks referenced in struct instance initializers:
|
||||
structInstanceInitRefs()
|
||||
.filter { irprog.st.lookup(it.name) == null }
|
||||
.forEach { ref -> allLabeledChunks[ref.name]?.let { linkedChunks += it } }
|
||||
|
||||
irprog.foreachCodeChunk { chunk ->
|
||||
chunk.next?.let { next -> linkedChunks += next }
|
||||
chunk.instructions.forEach {
|
||||
|
||||
@@ -10,7 +10,7 @@ import prog8.intermediate.*
|
||||
*/
|
||||
private class StToIrConverter(val romable: Boolean) {
|
||||
fun convert(struct: StStruct): IRStStructDef =
|
||||
IRStStructDef(struct.scopedNameString, makeIrStructFieldsFrom(struct.fields), struct.size)
|
||||
IRStStructDef(struct.scopedNameString, makeIrStructFieldsFrom(struct.fields), struct.size, struct.isUnion)
|
||||
|
||||
private fun makeIrStructFieldsFrom(fields: List<PtStructField>): List<IRStStructField> =
|
||||
fields.map { IRStStructField(it.type, it.name, it.arraySize) }
|
||||
@@ -76,9 +76,9 @@ private class StToIrConverter(val romable: Boolean) {
|
||||
|
||||
val name = if('.' in variable.name) variable.name else variable.scopedNameString
|
||||
val hasInit = initValue != null
|
||||
val inBss = !hasInit || (romable && hasInit)
|
||||
val inBss = !hasInit || romable
|
||||
val readonly = romable && hasInit
|
||||
return IRStStaticVariable(name, variable.dt, initValue, variable.length, variable.zpwish, variable.align, variable.dirty, inBss, readonly)
|
||||
return IRStStaticVariable(name, variable.dt, initValue, variable.length, variable.zpwish, variable.align, variable.dirty, inBss, readonly, variable.shared)
|
||||
}
|
||||
|
||||
fun convert(variable: StMemVar): IRStMemVar {
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -196,7 +196,7 @@ fun prefixSymbols(program: PtProgram, options: CompilationOptions, st: SymbolTab
|
||||
}
|
||||
|
||||
|
||||
internal fun prefixScopedName(name: String, type: Char): String {
|
||||
internal fun prefixScopedName(name: String, type: Char, st: SymbolTable? = null): String {
|
||||
if('.' !in name) {
|
||||
if(name.startsWith(GENERATED_LABEL_PREFIX))
|
||||
return name
|
||||
@@ -206,7 +206,11 @@ internal fun prefixScopedName(name: String, type: Char): String {
|
||||
val firstPrefixed = "p8b_${parts[0]}"
|
||||
val lastPart = parts.last()
|
||||
val lastPrefixed = if(lastPart.startsWith(GENERATED_LABEL_PREFIX)) lastPart else "p8${type}_$lastPart"
|
||||
val inbetweenPrefixed = parts.drop(1).dropLast(1).map{ "p8s_$it" }
|
||||
val inbetweenPrefixed = parts.drop(1).dropLast(1).mapIndexed { index, part ->
|
||||
val scopedName = parts.take(index + 2).joinToString(".")
|
||||
val middleType = st?.lookup(scopedName)?.type ?: StNodeType.SUBROUTINE
|
||||
"p8${typePrefixChar(middleType)}_$part"
|
||||
}
|
||||
val prefixed = listOf(firstPrefixed) + inbetweenPrefixed + listOf(lastPrefixed)
|
||||
return prefixed.joinToString(".")
|
||||
}
|
||||
@@ -229,7 +233,7 @@ internal fun typePrefixChar(targetNt: StNodeType): Char {
|
||||
|
||||
|
||||
private fun PtVariable.prefixVariableOrThis(parent: PtNode, st: SymbolTable): PtVariable {
|
||||
name = prefixScopedName(name, 'v')
|
||||
name = prefixScopedName(name, 'v', st)
|
||||
if(value==null)
|
||||
return this
|
||||
|
||||
@@ -256,13 +260,25 @@ private fun PtVariable.prefixVariableOrThis(parent: PtNode, st: SymbolTable): Pt
|
||||
is PtFunctionCall if elt.builtin -> {
|
||||
if (elt.name != "prog8_lib_structalloc")
|
||||
throw AssemblyError("weird array value element $elt")
|
||||
else
|
||||
else {
|
||||
// The struct initializer's arguments are not reachable via children, so
|
||||
// collectRefs never visits them; prefix the address-of ones here.
|
||||
elt.children.forEachIndexed { idx, arg ->
|
||||
if (arg is PtAddressOf && arg.definingBlock()?.options?.noSymbolPrefixing != true) {
|
||||
val newAddr = PtAddressOf(arg.type, false, arg.position)
|
||||
newAddr.add(arg.identifier!!.prefixIdentifierOrThis(newAddr, st))
|
||||
if (arg.arrayIndexExpr != null)
|
||||
newAddr.add(arg.arrayIndexExpr!!)
|
||||
elt.setChild(idx, newAddr)
|
||||
}
|
||||
}
|
||||
newValue.add(elt)
|
||||
}
|
||||
}
|
||||
else -> throw AssemblyError("weird array value element $elt")
|
||||
}
|
||||
}
|
||||
val result = PtVariable(name, type, isSplitWordArray, zeropage, align, dirty, newValue, arraySize, position)
|
||||
val result = PtVariable(name, type, isSplitWordArray, zeropage, align, dirty, newValue, arraySize, position, shared)
|
||||
result.parent = parent
|
||||
result
|
||||
}
|
||||
@@ -290,7 +306,7 @@ private fun PtIdentifier.prefixIdentifierOrThis(parent: PtNode, st: SymbolTable)
|
||||
}
|
||||
|
||||
val prefixType = typePrefixChar(targetNt)
|
||||
val newName = prefixScopedName(name, prefixType)
|
||||
val newName = prefixScopedName(name, prefixType, st)
|
||||
val node = PtIdentifier(newName, type, position)
|
||||
node.parent = parent
|
||||
return node
|
||||
|
||||
@@ -6,12 +6,9 @@ import prog8.code.assembly.IAssemblyProgram
|
||||
import prog8.code.ast.PtProgram
|
||||
import prog8.code.core.CompilationOptions
|
||||
import prog8.code.core.IErrorReporter
|
||||
import prog8.code.core.OutputFiles
|
||||
import prog8.codegen.intermediate.IRCodeGen
|
||||
import prog8.intermediate.IRDataType
|
||||
import prog8.intermediate.IRFileWriter
|
||||
import prog8.intermediate.IRProgram
|
||||
import prog8.intermediate.VirtualRegister
|
||||
import prog8.intermediate.dumpVariables
|
||||
import prog8.intermediate.*
|
||||
|
||||
class VmCodeGen(val retainSSA: Boolean,
|
||||
private val preassignedCallSiteIds: Map<String, UByte> = emptyMap()
|
||||
@@ -54,8 +51,9 @@ internal class VmAssemblyProgram(
|
||||
get() = irProgram.countUsedRegisters()
|
||||
|
||||
override fun assemble(options: CompilationOptions, errors: IErrorReporter): Boolean {
|
||||
// the VM reads the IR file from disk.
|
||||
IRFileWriter(irProgram, null).write()
|
||||
// the VM reads the IR file from disk; for the virtual target the IR file is the program artifact.
|
||||
val programFile = OutputFiles.of(options, irProgram.name).programFile
|
||||
IRFileWriter(irProgram, programFile).write()
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -514,6 +514,51 @@ class TestIRPeepholeOpt: FunSpec({
|
||||
instr[2].requireImmediateInt() shouldBe 42
|
||||
}
|
||||
|
||||
test("collapse lsigb.w + ext.b into loadr.w + and.w") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.WORD, 7, IRMemory.direct("src")),
|
||||
IRInstructions.binary(Opcode.LSIGB, IRDataType.WORD, 8, 7),
|
||||
IRInstructions.binary(Opcode.EXT, IRDataType.BYTE, 9, 8),
|
||||
IRInstructions.binary(Opcode.ADDR, IRDataType.WORD, 2, 9),
|
||||
IRInstructions.returnVoid()
|
||||
))
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.any { it.opcode == Opcode.LSIGB } shouldBe false
|
||||
instr.any { it.opcode == Opcode.EXT && it.requireDest().register == VirtualRegister.int(9) } shouldBe false
|
||||
instr.any {
|
||||
it.opcode == Opcode.LOADR &&
|
||||
it.type == IRDataType.WORD &&
|
||||
it.requireDest().register == VirtualRegister.int(9) &&
|
||||
it.requireSrcA().register == VirtualRegister.int(7)
|
||||
} shouldBe true
|
||||
instr.any {
|
||||
it.opcode == Opcode.AND &&
|
||||
it.type == IRDataType.WORD &&
|
||||
it.requireDest().register == VirtualRegister.int(9) &&
|
||||
it.requireImmediateInt() == 0xff
|
||||
} shouldBe true
|
||||
}
|
||||
|
||||
test("do not collapse lsigb.l + ext.b when source and dest types differ") {
|
||||
// lsigb.l produces a byte from a long; ext.b widens it to word. Rewriting this as
|
||||
// loadr + and would require accessing the long source as word, which conflicts with
|
||||
// the register type system. The optimizer must leave the original sequence intact.
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.LONG, 7, IRMemory.direct("src")),
|
||||
IRInstructions.binary(Opcode.LSIGB, IRDataType.LONG, 8, 7),
|
||||
IRInstructions.binary(Opcode.EXT, IRDataType.BYTE, 9, 8),
|
||||
IRInstructions.binary(Opcode.ADDR, IRDataType.WORD, 2, 9),
|
||||
IRInstructions.returnVoid()
|
||||
))
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.any { it.opcode == Opcode.LSIGB } shouldBe true
|
||||
instr.any { it.opcode == Opcode.EXT && it.requireDest().register == VirtualRegister.int(9) } shouldBe true
|
||||
}
|
||||
|
||||
test("coalesce redundant LOADX/STOREX to same index is removed") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstructions.loadMemory(Opcode.LOADX, IRDataType.BYTE, 1, IRMemory.indexed("myArray", 10, indexRegType)),
|
||||
|
||||
@@ -34,6 +34,17 @@ class TestSymbolPrefixer : FunSpec({
|
||||
prefixScopedName("lib.module.func", 's') shouldBe "p8b_lib.p8s_module.p8s_func"
|
||||
}
|
||||
|
||||
test("prefixScopedName uses the symbol type for middle components") {
|
||||
val program = PtProgram("test", VMTarget())
|
||||
val block = PtBlock("main", false, SourceCode.Generated("test"), PtBlock.Options(), Position.DUMMY)
|
||||
val struct = PtStructDecl("Point", listOf(PtStructField(DataType.UBYTE, "x", null)), Position.DUMMY)
|
||||
block.add(struct)
|
||||
program.add(block)
|
||||
val st = SymbolTableMaker(program, basicTestOptions()).make()
|
||||
|
||||
prefixScopedName("main.Point.field", 'v', st) shouldBe "p8b_main.p8t_Point.p8v_field"
|
||||
}
|
||||
|
||||
test("prefixScopedName generated label is not prefixed") {
|
||||
val genLabel = "p8_label_gen_42"
|
||||
prefixScopedName(genLabel, 's') shouldBe genLabel
|
||||
|
||||
@@ -53,6 +53,16 @@ Passes the linker script (see below).
|
||||
|
||||
## The linker script (`link.ld`)
|
||||
|
||||
The compiler resolves the linker script for a target in this order:
|
||||
|
||||
1. `link.ld` in the target's library directory (the `library` setting). This is how a custom
|
||||
target, defined outside of the compiler, supplies its own memory layout.
|
||||
2. `/prog8lib/<targetname>/link.ld`, for targets built into the compiler.
|
||||
3. `/prog8lib/qemu68k/link.ld` as the fallback, which is the script shown below.
|
||||
|
||||
Only the `ELF` output type links at all. The `AMIGAHUNK` output type (used by the built-in
|
||||
Amiga targets) assembles straight to a hunk executable and ignores the linker script.
|
||||
|
||||
```
|
||||
ENTRY(_start)
|
||||
SECTIONS
|
||||
|
||||
@@ -38,13 +38,13 @@ internal fun IRInstruction.forwardableImmediateFor(register: VirtualRegister, ty
|
||||
* It may contain registers that are NOT call arguments.
|
||||
* For every call argument register, the loaded immediate value is forwarded
|
||||
* directly into the argument's hardware register instead of going through the
|
||||
* register file (move #imm,dX instead of move p8_regfile+N,dX, and
|
||||
* fmovecr or a const-pool load into fpX instead of moving through p8_fregfile).
|
||||
* register file. That is move #imm,dX instead of move p8_regfile+N,dX, and
|
||||
* fmovecr or a const-pool load into fpX instead of moving through p8_fregfile.
|
||||
*
|
||||
* [deadRegisters] are the registers from [loads] that are not read anywhere
|
||||
* else in the subroutine (their only use is the forwarded call argument);
|
||||
* their register-file stores are omitted entirely. Note that this can also
|
||||
* remove stores for registers that are not call arguments at all.
|
||||
* else in the subroutine; their only use is the forwarded call argument, so
|
||||
* their register-file stores are omitted entirely. This can also remove
|
||||
* stores for registers that are not call arguments at all.
|
||||
*
|
||||
* The read check covers the subroutine's whole flattened instruction list
|
||||
* rather than only the part after the call (see `isRegisterReadElsewhere`),
|
||||
@@ -103,7 +103,7 @@ internal fun nativeFloatConst(value: Double): String? = when (value) {
|
||||
* instead. Return values are passed via virtual registers mapped back to the caller's result register.
|
||||
*/
|
||||
|
||||
internal class AsmGen(val program: IRProgram, internal val target: ICompilationTarget) {
|
||||
internal class AsmGen(val program: IRProgram, internal val target: ICompilationTarget, internal val errors: IErrorReporter? = null) {
|
||||
private val output = StringBuilder()
|
||||
private val regsUsed by lazy { program.registersUsed() }
|
||||
internal val cpu get() = target.cpu
|
||||
@@ -111,6 +111,10 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
companion object {
|
||||
const val REGFILE_LABEL = "p8_regfile"
|
||||
const val FLOAT_REGFILE_LABEL = "p8_fregfile"
|
||||
// frames larger than this are reported with an INFO message; deep recursion multiplies the frame per call
|
||||
const val LARGE_FRAME_SIZE = 512
|
||||
// consecutive STOREZM LONG frame stores from which a dbra clear loop is smaller than straight clr.l
|
||||
const val ZERO_CLEAR_LOOP_THRESHOLD = 16
|
||||
}
|
||||
|
||||
init {
|
||||
@@ -281,6 +285,38 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
output.appendLine(code)
|
||||
}
|
||||
|
||||
// === stack frame context ===
|
||||
// true while emitting a subroutine that has a frame (local area and/or incoming argument
|
||||
// area); the return translation emits `unlk a5` before each rts when this is set.
|
||||
internal var frameActive = false
|
||||
|
||||
// the code chunks of the subroutine currently being emitted, to tell a jump that stays
|
||||
// inside it apart from one that leaves it
|
||||
private var currentSubChunks: Set<IRCodeChunkBase> = emptySet()
|
||||
|
||||
// Virtual registers of the subroutine being emitted, by register number -> frame offset.
|
||||
// A re-entrant subroutine (one that can have two live activations) keeps them per-activation in
|
||||
// its stack frame; every other subroutine uses the flat program-static register file.
|
||||
private var frameVregSlots: Map<Int, Int> = emptyMap()
|
||||
|
||||
private fun frameVregAddr(reg: Int): String? = frameVregSlots[reg]?.let { "$it(a5)" }
|
||||
|
||||
/** emits the frame teardown for the subroutine currently being translated, if it has a frame */
|
||||
internal fun emitFrameUnlk() {
|
||||
if (frameActive)
|
||||
emitLine("unlk a5")
|
||||
}
|
||||
|
||||
/**
|
||||
* true when a jump to this label lands outside the subroutine currently being emitted.
|
||||
* An unresolvable label counts as inside: without the chunk map we cannot tell, and guessing
|
||||
* 'outside' would tear down frames of jumps that stay in the subroutine.
|
||||
*/
|
||||
internal fun labelIsOutsideCurrentSub(labelName: String): Boolean {
|
||||
val target = program.resolveCodeTarget(CodeReference.Label(labelName)) ?: return false
|
||||
return target !in currentSubChunks
|
||||
}
|
||||
|
||||
// === virtual register file layout (1, 2 or 4 bytes per slot depending on type, word-aligned) ===
|
||||
private data class RegFileLayout(val offsets: Map<Int, Int>, val totalSize: Int)
|
||||
|
||||
@@ -328,6 +364,7 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
}
|
||||
|
||||
fun regAddr(reg: Int): String {
|
||||
frameVregAddr(reg)?.let { return it }
|
||||
val offset = regFileLayout.offsets[reg] ?: error("register r$reg has no layout info")
|
||||
return "$REGFILE_LABEL+$offset"
|
||||
}
|
||||
@@ -335,9 +372,11 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
private fun regNumForOffset(offset: Int): Int? =
|
||||
regFileLayout.offsets.entries.find { it.value == offset }?.key
|
||||
|
||||
// Resolve a register-file address string ("p8_regfile" or "p8_regfile+N")
|
||||
// back to a virtual register number, or null if it is not a regfile slot.
|
||||
// Resolve a register-file address string ("p8_regfile" or "p8_regfile+N", or a frame slot
|
||||
// like "-8(a5)" for a per-activation virtual register) back to a virtual register number,
|
||||
// or null if it is not a register-file slot.
|
||||
fun regNumForAddress(address: String): Int? {
|
||||
frameVregSlots.entries.firstOrNull { "$it.value(a5)" == address }?.let { return it.key }
|
||||
if (!address.startsWith(REGFILE_LABEL))
|
||||
return null
|
||||
val suffix = address.removePrefix(REGFILE_LABEL)
|
||||
@@ -350,6 +389,7 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
}
|
||||
|
||||
fun regAddrByte(reg: Int, byteOffset: Int): String {
|
||||
frameVregSlots[reg]?.let { return "${it + byteOffset}(a5)" }
|
||||
val offset = regFileLayout.offsets[reg] ?: error("register r$reg has no layout info")
|
||||
return "$REGFILE_LABEL+${offset + byteOffset}"
|
||||
}
|
||||
@@ -380,6 +420,10 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
|
||||
fun loadPointerToA0(reg: Int) {
|
||||
// load a 32-bit pointer from the register file into a0
|
||||
frameVregAddr(reg)?.let {
|
||||
emitLine("movea.l $it, a0")
|
||||
return
|
||||
}
|
||||
val offset = regFileLayout.offsets[reg] ?: error("register r$reg has no layout info")
|
||||
val addr = if (offset == 0) REGFILE_LABEL else "$REGFILE_LABEL+$offset"
|
||||
emitLine("movea.l $addr, a0")
|
||||
@@ -454,6 +498,12 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
|
||||
private fun resolveMemoryBase(base: AddressBase, displacement: Int): String {
|
||||
val resolved = when (base) {
|
||||
// combine slot offset and reference displacement into a single a5-relative displacement
|
||||
is AddressBase.FrameSlot -> {
|
||||
val total = base.offset + displacement
|
||||
if (total !in -32768..32767) error("frame displacement $total out of 16-bit range")
|
||||
return "$total(a5)"
|
||||
}
|
||||
is AddressBase.Symbol -> resolveSymbolRef(base.name)
|
||||
is AddressBase.Absolute -> base.address.value.toHex()
|
||||
}
|
||||
@@ -535,6 +585,7 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
// === FPU helpers (M680x0 with 68881/68882) ===
|
||||
|
||||
fun floatRegFileAddr(reg: RegisterNum): String {
|
||||
frameVregAddr(reg.value)?.let { return it }
|
||||
val offset = floatRegFileLayout.offsets[reg.value] ?: error("float register fr${reg.value} has no layout info")
|
||||
return "$FLOAT_REGFILE_LABEL+$offset"
|
||||
}
|
||||
@@ -577,7 +628,7 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
}
|
||||
|
||||
val options = program.options
|
||||
val asmFile = options.outputDir.resolve("${program.name}.asm")
|
||||
val asmFile = OutputFiles.of(options, program.name).asm()
|
||||
try {
|
||||
if (options.optimize) {
|
||||
val asmLines = output.toString().lines().toMutableList()
|
||||
@@ -638,6 +689,9 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
// emit all Prog8 constants as vasm symbols for inline asm use
|
||||
emitConstants()
|
||||
|
||||
// emit struct/union type sizes and member offsets as vasm symbols for inline asm use
|
||||
emitStructDefs()
|
||||
|
||||
// Set up stack pointer and jump to program start
|
||||
emitLabel("prog8_program_start")
|
||||
|
||||
@@ -725,6 +779,14 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
val entrypointNames = setOf("p8b_main.p8s_start", "main.start")
|
||||
if(sub.label in entrypointNames)
|
||||
emitLine("bsr run_global_inits")
|
||||
frameActive = sub.hasFrame
|
||||
frameVregSlots = sub.frameVregSlots
|
||||
currentSubChunks = mutableSetOf<IRCodeChunkBase>().also { set -> sub.forEachChunk { set.add(it) } }
|
||||
if (sub.hasFrame) {
|
||||
emitRaw("; stack frame: ${sub.frameSize} bytes of locals, ${sub.incomingSize} bytes of arguments")
|
||||
emitLine(if (sub.frameSize > 0) "link a5,#-${sub.frameSize}" else "link a5,#0")
|
||||
reportFrameSize(sub)
|
||||
}
|
||||
val livenessInstructions = sub.chunks.filterIsInstance<IRCodeChunk>().flatMap { it.instructions }
|
||||
val deadStoreSuppressionAllowed = canSuppressDeadStores(sub)
|
||||
var instructionOffset = 0
|
||||
@@ -756,6 +818,9 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
}
|
||||
emitRaw("; End of subroutine: $subLabel")
|
||||
emitRaw("")
|
||||
frameActive = false
|
||||
frameVregSlots = emptyMap()
|
||||
currentSubChunks = emptySet()
|
||||
}
|
||||
|
||||
private fun emitAsmSubroutine(sub: IRAsmSubroutine) {
|
||||
@@ -775,6 +840,16 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
private fun unscopedName(scopedName: String): String =
|
||||
scopedName.substringAfterLast('.')
|
||||
|
||||
/** emits an informational message for unusually large frames; deep recursion multiplies the frame per call */
|
||||
private fun reportFrameSize(sub: IRSubroutine) {
|
||||
if (sub.frameSize > LARGE_FRAME_SIZE)
|
||||
errors?.info("subroutine '${userFacingName(sub.label)}' uses a ${sub.frameSize} byte stack frame; deep recursion multiplies this per call", sub.position)
|
||||
}
|
||||
|
||||
/** the scoped IR label without its internal scope markers, for use in user-facing messages */
|
||||
private fun userFacingName(scopedName: String): String =
|
||||
scopedName.split('.').joinToString(".") { it.removePrefix("p8b_").removePrefix("p8s_") }
|
||||
|
||||
// Dead-store suppression checks whether a register is read anywhere else in the
|
||||
// subroutine (see isRegisterReadElsewhere), which is independent of control
|
||||
// flow and therefore sound regardless of branch topology (loops, indirect
|
||||
@@ -876,8 +951,18 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
}
|
||||
}
|
||||
}
|
||||
for (index in chunk.instructions.indices) {
|
||||
var index = 0
|
||||
while (index < chunk.instructions.size) {
|
||||
val insn = chunk.instructions[index]
|
||||
// Long ascending STOREZM LONG runs lower to a dbra loop; short runs keep clr.l.
|
||||
val zeroRun = frameZeroRunLength(chunk.instructions, index)
|
||||
if (zeroRun >= ZERO_CLEAR_LOOP_THRESHOLD) {
|
||||
val mem = insn.memory as MemoryReference.Direct
|
||||
val slot = mem.base as AddressBase.FrameSlot
|
||||
emitFrameZeroLoop(slot.offset + mem.displacement, zeroRun)
|
||||
index += zeroRun
|
||||
continue
|
||||
}
|
||||
val clobbersLoopCounter = preserveLoopCounter && mayClobberLoopCounter(insn)
|
||||
if (clobbersLoopCounter)
|
||||
emitLine("move.w d7,-(sp)", "save loop counter around call")
|
||||
@@ -888,9 +973,57 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
)
|
||||
if (clobbersLoopCounter)
|
||||
emitLine("move.w (sp)+,d7", "restore loop counter after call")
|
||||
index++
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts consecutive STOREZM LONG instructions on ascending frame-slot displacements
|
||||
* starting at [instructions[from]], stepping by one longword with nothing interleaved.
|
||||
* Returns 0 when [instructions[from]] is not such a store.
|
||||
*/
|
||||
private fun frameZeroRunLength(instructions: List<IRInstruction>, from: Int): Int {
|
||||
val first = instructions[from]
|
||||
if (first.opcode != Opcode.STOREZM || first.type != IRDataType.LONG) return 0
|
||||
val firstMem = first.memory as? MemoryReference.Direct ?: return 0
|
||||
if (firstMem.base !is AddressBase.FrameSlot) return 0
|
||||
var expected = (firstMem.base as AddressBase.FrameSlot).offset + firstMem.displacement
|
||||
var count = 0
|
||||
var index = from
|
||||
while (index < instructions.size) {
|
||||
val insn = instructions[index]
|
||||
if (insn.opcode != Opcode.STOREZM || insn.type != IRDataType.LONG) break
|
||||
val mem = insn.memory as? MemoryReference.Direct ?: break
|
||||
val slot = mem.base as? AddressBase.FrameSlot ?: break
|
||||
if (slot.offset + mem.displacement != expected) break
|
||||
count++
|
||||
expected += 4
|
||||
index++
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
/**
|
||||
* Lowers [longwords] consecutive longword zero-clears at [startDisp](a5)
|
||||
* to a dbra loop using scratch d0/d1/a0 while preserving d7.
|
||||
* Assumes scratch registers are free at chunk boundaries (true at prologues).
|
||||
*/
|
||||
private fun emitFrameZeroLoop(startDisp: Int, longwords: Int) {
|
||||
invalidateD0Cache() // moveq clobbers d0
|
||||
emitLine("moveq #0, d0")
|
||||
emitLine("lea $startDisp(a5), a0")
|
||||
val iterations = longwords - 1
|
||||
if (iterations in 0..127)
|
||||
emitLine("moveq #$iterations, d1")
|
||||
else
|
||||
emitLine("move.w #$iterations, d1")
|
||||
val loopLabel = makeLabel("zeroloop")
|
||||
emitLabel(loopLabel)
|
||||
emitLine("move.l d0,(a0)+")
|
||||
emitLine("dbra d1,$loopLabel")
|
||||
invalidateD0Cache()
|
||||
}
|
||||
|
||||
private fun mayClobberLoopCounter(insn: IRInstruction): Boolean {
|
||||
if (insn.opcode in setOf(Opcode.CALL, Opcode.CALLI, Opcode.CALLFAR, Opcode.CALLFARVB, Opcode.SYSCALL))
|
||||
return true
|
||||
@@ -962,7 +1095,10 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
forwardedImmediateCall: ImmediateCallOptimization? = null,
|
||||
suppressRegfileStore: Boolean = false
|
||||
) {
|
||||
emitRaw(" ; $insn")
|
||||
// No IR echo for zero-clear stores: prologue clear runs would drown in
|
||||
// "; storezm" noise while the lowered clr lines are self-explanatory.
|
||||
if (insn.opcode != Opcode.STOREZM)
|
||||
emitRaw(" ; $insn")
|
||||
when (insn.opcode) {
|
||||
Opcode.NOP -> {}
|
||||
Opcode.BREAKPOINT -> emitLine("illegal")
|
||||
@@ -1036,7 +1172,10 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
} else {
|
||||
emptyList<IRStStaticVariable>() to initdVars
|
||||
}
|
||||
val structInstancesWithInit: List<IRStStructInstance> = program.st.allStructInstances().filter { it.values.isNotEmpty() }.toList()
|
||||
val structInstancesWithInit: List<IRStStructInstance> = program.st.allStructInstances().filter {
|
||||
val def = program.st.lookup(it.structName) as IRStStructDef
|
||||
it.values.isNotEmpty() && !def.isUnion
|
||||
}.toList()
|
||||
val (chipramStructs: List<IRStStructInstance>, normalStructs: List<IRStStructInstance>) =
|
||||
if (chipramBlocks.isNotEmpty()) {
|
||||
structInstancesWithInit.partition { it.name.substringBefore('.') in chipramBlocks }
|
||||
@@ -1332,6 +1471,28 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
emitRaw("")
|
||||
}
|
||||
|
||||
// Emit the size and member offsets of all struct/union types as vasm 'equ' symbols,
|
||||
// so inline asm can reference them by name (parity with the 64tass-based backends).
|
||||
// Offsets use the same packed layout (no alignment padding) that the compiler's own
|
||||
// member-access codegen uses: a plain fold over the target's memory sizes.
|
||||
private fun emitStructDefs() {
|
||||
val structs = program.st.allStructDefs().toList()
|
||||
if (structs.isEmpty()) return
|
||||
emitRaw("; struct type definitions")
|
||||
for (sd in structs) {
|
||||
val base = fixNameSymbols(sd.name)
|
||||
emitRaw("${base}_size equ ${sd.size}")
|
||||
var offset = 0
|
||||
for (field in sd.fields) {
|
||||
emitRaw("${base}.p8v_${field.name} equ $offset")
|
||||
if (!sd.isUnion)
|
||||
offset += target.memorySize(field.type, field.arraySize ?: 1)
|
||||
}
|
||||
emitRaw("")
|
||||
}
|
||||
emitRaw("")
|
||||
}
|
||||
|
||||
|
||||
private fun emitBssSection() {
|
||||
val chipramBlocks = if (target.name in setOf("amiga500", "amiga1200")) chipramBlockLabels() else emptySet()
|
||||
@@ -1344,7 +1505,10 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
|
||||
} else {
|
||||
emptyList<IRStStaticVariable>() to allBssVars
|
||||
}
|
||||
val allStructsNoInit: List<IRStStructInstance> = program.st.allStructInstances().filter { it.values.isEmpty() }.toList()
|
||||
val allStructsNoInit: List<IRStStructInstance> = program.st.allStructInstances().filter {
|
||||
val def = program.st.lookup(it.structName) as IRStStructDef
|
||||
it.values.isEmpty() || def.isUnion
|
||||
}.toList()
|
||||
val (chipramStructsNoInit: List<IRStStructInstance>, normalStructsNoInit: List<IRStStructInstance>) =
|
||||
if (chipramBlocks.isNotEmpty()) {
|
||||
allStructsNoInit.partition { it.name.substringBefore('.') in chipramBlocks }
|
||||
|
||||
@@ -61,8 +61,91 @@ internal fun optimizeAssembly(lines: MutableList<String>) {
|
||||
if (fuseMods.isNotEmpty()) {
|
||||
applyModifications(fuseMods, lines, pretrimmed)
|
||||
}
|
||||
// M5: drop a `lea base,a0` when a0 already holds that same base.
|
||||
// NOT REGFILE-DEPENDENT: a0 is a real CPU register, so this pass is independent of the
|
||||
// p8_regfile layout and of the planned register allocator.
|
||||
val leaMods = optimizeRedundantLea(pretrimmed)
|
||||
if (leaMods.isNotEmpty()) {
|
||||
applyModifications(leaMods, lines, pretrimmed)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* M5: remove `lea base,a0` instructions that recompute a base a0 is already known to hold.
|
||||
*
|
||||
* Indexed array access is emitted as `lea base,a0` followed by `(a0,d0.w*2)`. The lea itself is
|
||||
* required - absolute-indexed addressing (`base(a0,d0.w*2)`) does not exist on the m68k - but in
|
||||
* a sequence of accesses to the same array the base is reloaded several times over.
|
||||
*
|
||||
* A tracked value is dropped (a0 treated as unknown) whenever the scan sees:
|
||||
* - a label: incoming control flow may not be the fallthrough path
|
||||
* - any control transfer: a branch may reach elsewhere, and a call clobbers a0 (it is a
|
||||
* scratch register in the m68k calling convention)
|
||||
* - any instruction that names a0 outside a parenthesised address, i.e. writes it
|
||||
*
|
||||
* Anything else - including pure reads such as `move.w (a0,d0.w),d1` - leaves it untouched.
|
||||
*/
|
||||
private fun optimizeRedundantLea(allPretrimmed: List<String>): List<Modification> {
|
||||
// Unconditional control flow, calls and returns end the tracked value: control leaves via a
|
||||
// path where a0's contents are unknown (and a call clobbers a0, a scratch register in the
|
||||
// m68k calling convention). The whole DBcc family is included as loop control.
|
||||
// Plain condition-code branches (bne, bge, ...) are deliberately NOT in this set: they do not
|
||||
// modify a0, and their target is always a label, which resets the tracked value below. So the
|
||||
// fallthrough keeps the tracked value and the branch target is re-entered conservatively.
|
||||
val controlTransfers = setOf(
|
||||
"bra", "bsr", "jmp", "jsr", "rts", "rtr", "rte", "trap",
|
||||
"dbcc", "dbcs", "dbhi", "dbhs", "dblo", "dbls", "dbne", "dbnz", "dbeq", "dbz",
|
||||
"dbmi", "dbpl", "dbge", "dblt", "dbgt", "dble", "dbvc", "dbvs", "dbtst", "dbset",
|
||||
"dbclr", "dbchg", "dbf", "dbra"
|
||||
)
|
||||
// A lea whose destination is a0, and the value it leaves there.
|
||||
fun leaBase(instr: String): String? {
|
||||
if (!instr.startsWith("lea ") && !instr.startsWith("lea\t"))
|
||||
return null
|
||||
val operands = instr.substringAfter(' ').substringBefore(';')
|
||||
val parts = operands.split(',', limit = 2)
|
||||
if (parts.size != 2 || parts[1].trim() != "a0")
|
||||
return null
|
||||
return parts[0].trim()
|
||||
}
|
||||
|
||||
val filtered = allPretrimmed.withIndex()
|
||||
.filter { it.value.isNotBlank() && !it.value.trimStart().startsWith(';') }
|
||||
.map { TrimmedLine(it.value.trimStart(), it.value.trimStart(), it.index) }
|
||||
|
||||
val mods = mutableListOf<Modification>()
|
||||
var a0Base: String? = null
|
||||
for (line in filtered) {
|
||||
val instr = line.instruction
|
||||
if (hasLabel(line.value)) {
|
||||
// fallthrough into a label is fine, but the label may also be entered from
|
||||
// elsewhere, so the tracked value is no longer known to hold
|
||||
a0Base = null
|
||||
continue
|
||||
}
|
||||
val mnemonic = instr.substringBefore(' ').substringBefore('\t').lowercase()
|
||||
if (mnemonic in controlTransfers) {
|
||||
a0Base = null
|
||||
continue
|
||||
}
|
||||
val base = leaBase(instr.lowercase())
|
||||
if (base != null) {
|
||||
if (base == a0Base)
|
||||
mods.add(Modification(line.index, remove = true, replacement = null))
|
||||
else
|
||||
a0Base = base
|
||||
continue
|
||||
}
|
||||
// Any non-parenthesised mention of a0 means it is written
|
||||
if (instr.replace(parenthesised, "").contains("a0", ignoreCase = true))
|
||||
a0Base = null
|
||||
}
|
||||
return mods
|
||||
}
|
||||
|
||||
/** Matches a parenthesised group, for stripping addressing modes. */
|
||||
private val parenthesised = Regex("""\([^)]*\)""")
|
||||
|
||||
/** A single edit to apply: either remove a line (keeping its label if present) or replace it. */
|
||||
private class Modification(val lineIndex: Int, val remove: Boolean, val replacement: String?)
|
||||
|
||||
|
||||
@@ -1,24 +1,35 @@
|
||||
package prog8.codegen.m68k
|
||||
|
||||
import prog8.code.assembly.IAssemblyProgram
|
||||
import prog8.code.core.CompilationOptions
|
||||
import prog8.code.core.CpuType
|
||||
import prog8.code.core.IErrorReporter
|
||||
import prog8.code.core.OutputType
|
||||
import prog8.code.core.*
|
||||
import prog8.code.target.Amiga1200Target
|
||||
import prog8.code.target.Amiga500Target
|
||||
import prog8.code.target.Qemu68kTarget
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.readText
|
||||
|
||||
class AssemblyProgramM68k(override val name: String, private val outputDir: Path) : IAssemblyProgram {
|
||||
class AssemblyProgramM68k(override val name: String) : IAssemblyProgram {
|
||||
|
||||
override val irInstructionCount: Int = 0
|
||||
override val irChunkCount: Int = 0
|
||||
override val irRegisterCount: Int = 0
|
||||
|
||||
private val assemblyFile = outputDir.resolve("$name.asm")
|
||||
private fun elfFile(outs: OutputFiles): Path = outs.programFile
|
||||
|
||||
fun elfFile(): Path = outputDir.resolve("$name.elf")
|
||||
/**
|
||||
* A target supplies its own memory layout as a "link.ld" file. A target defined by a
|
||||
* config file (i.e. living outside of the compiler) keeps it in its library directory, so
|
||||
* that is looked at first. Targets built into the compiler are looked up as a
|
||||
* /prog8lib/<targetname>/link.ld resource. Anything else falls back to the qemu68k layout.
|
||||
*/
|
||||
private fun resolveLinkerScript(target: ICompilationTarget): String {
|
||||
target.libraryPath?.resolve("link.ld")?.takeIf { Files.isReadable(it) }?.let { return it.readText() }
|
||||
val targetResource = "/prog8lib/${target.name}/link.ld"
|
||||
AssemblyProgramM68k::class.java.getResource(targetResource)?.let { return it.readText() }
|
||||
return AssemblyProgramM68k::class.java.getResource("/prog8lib/${Qemu68kTarget.NAME}/link.ld")?.readText()
|
||||
?: error("cannot find $targetResource resource, and no qemu68k fallback either")
|
||||
}
|
||||
|
||||
private fun runProcess(command: List<String>, quiet: Boolean, tool: String? = null): Boolean {
|
||||
val proc = ProcessBuilder(command).redirectErrorStream(true)
|
||||
@@ -55,12 +66,13 @@ class AssemblyProgramM68k(override val name: String, private val outputDir: Path
|
||||
else -> error("invalid cpu type for m68k codegen ${options.compTarget.cpu}")
|
||||
}
|
||||
val assemblerCpu = if(options.compTarget.name == "amiga500" && options.floats) "68020" else cpu
|
||||
val outs = OutputFiles.of(options, name)
|
||||
|
||||
when(options.output) {
|
||||
OutputType.ELF -> {
|
||||
// Step 1: assemble to ELF object file
|
||||
val objFile = outputDir.resolve("$name.o")
|
||||
val listFile = outputDir.resolve("$name.list")
|
||||
val objFile = outs.obj()
|
||||
val listFile = outs.list()
|
||||
val assembleCmd = mutableListOf(
|
||||
"vasmm68k_mot",
|
||||
"-m$assemblerCpu",
|
||||
@@ -71,7 +83,7 @@ class AssemblyProgramM68k(override val name: String, private val outputDir: Path
|
||||
"-ldots",
|
||||
"-spaces",
|
||||
"-o", objFile.toString(),
|
||||
assemblyFile.toString()
|
||||
outs.asm().toString()
|
||||
)
|
||||
if (options.asmListfile)
|
||||
assembleCmd.addAll(listOf("-L", listFile.toString()))
|
||||
@@ -80,14 +92,12 @@ class AssemblyProgramM68k(override val name: String, private val outputDir: Path
|
||||
if (!runProcess(assembleCmd, options.quiet, "vasm"))
|
||||
return false
|
||||
// clean up any leftover ELF/obj files from previous builds
|
||||
Files.deleteIfExists(outputDir.resolve("$name.bin"))
|
||||
Files.deleteIfExists(outs.binFile())
|
||||
|
||||
// Step 2: write linker script and link to ELF executable
|
||||
val linkScript = outputDir.resolve("$name.link.ld")
|
||||
val elfFile = elfFile()
|
||||
val resourceUrl = AssemblyProgramM68k::class.java.getResource("/prog8lib/qemu68k/link.ld")
|
||||
?: error("cannot find /prog8lib/qemu68k/link.ld resource")
|
||||
Files.writeString(linkScript, resourceUrl.readText())
|
||||
val linkScript = outs.linkScript()
|
||||
val elfFile = elfFile(outs)
|
||||
Files.writeString(linkScript, resolveLinkerScript(options.compTarget))
|
||||
|
||||
val linkCmd = listOf(
|
||||
"vlink",
|
||||
@@ -105,8 +115,8 @@ class AssemblyProgramM68k(override val name: String, private val outputDir: Path
|
||||
}
|
||||
OutputType.AMIGAHUNK -> {
|
||||
// Step 1: assemble directly to AmigaHunk executable file
|
||||
val exefile = outputDir.resolve(name)
|
||||
val listfile = outputDir.resolve("$name.list")
|
||||
val exefile = outs.programFile
|
||||
val listfile = outs.list()
|
||||
val assembleCmd = when(options.compTarget) {
|
||||
is Amiga500Target -> {
|
||||
// amiga 500 with kickstart 1.3
|
||||
@@ -122,7 +132,7 @@ class AssemblyProgramM68k(override val name: String, private val outputDir: Path
|
||||
"-spaces",
|
||||
"-nosym", // no debug symbols
|
||||
"-o", exefile.toString(),
|
||||
assemblyFile.toString()
|
||||
outs.asm().toString()
|
||||
).also { cmd ->
|
||||
if (options.asmListfile) {
|
||||
cmd.add("-L")
|
||||
@@ -146,7 +156,7 @@ class AssemblyProgramM68k(override val name: String, private val outputDir: Path
|
||||
"-spaces",
|
||||
"-nosym", // no debug symbols
|
||||
"-o", exefile.toString(),
|
||||
assemblyFile.toString()
|
||||
outs.asm().toString()
|
||||
).also { cmd ->
|
||||
if (options.asmListfile) {
|
||||
cmd.add("-L")
|
||||
|
||||
@@ -28,40 +28,46 @@ import prog8.intermediate.requireSrcB
|
||||
import prog8.intermediate.requireTarget
|
||||
|
||||
internal fun AsmGen.translateBranch(insn: IRInstruction) {
|
||||
val label: String = when (val target = insn.requireTarget()) {
|
||||
val target = insn.requireTarget()
|
||||
val label: String = when (target) {
|
||||
is CodeReference.Label -> fixNameSymbols(target.name)
|
||||
is CodeReference.Absolute -> target.address.toHex()
|
||||
is CodeReference.Indirect -> error("branch needs a static target")
|
||||
}
|
||||
val leavesSubroutine = when (target) {
|
||||
is CodeReference.Label -> labelIsOutsideCurrentSub(target.name)
|
||||
is CodeReference.Absolute -> true
|
||||
is CodeReference.Indirect -> false
|
||||
}
|
||||
|
||||
when (insn.opcode) {
|
||||
Opcode.BSTCC -> emitBranch("bcc", label)
|
||||
Opcode.BSTCS -> emitBranch("bcs", label)
|
||||
Opcode.BSTEQ -> emitBranch("beq", label)
|
||||
Opcode.BSTNE -> emitBranch("bne", label)
|
||||
Opcode.BSTNEG -> emitBranch("bmi", label)
|
||||
Opcode.BSTPOS -> emitBranch("bpl", label)
|
||||
Opcode.BSTVC -> emitBranch("bvc", label)
|
||||
Opcode.BSTVS -> emitBranch("bvs", label)
|
||||
Opcode.BSTCC -> emitBranch("bcc", label, leavesSubroutine)
|
||||
Opcode.BSTCS -> emitBranch("bcs", label, leavesSubroutine)
|
||||
Opcode.BSTEQ -> emitBranch("beq", label, leavesSubroutine)
|
||||
Opcode.BSTNE -> emitBranch("bne", label, leavesSubroutine)
|
||||
Opcode.BSTNEG -> emitBranch("bmi", label, leavesSubroutine)
|
||||
Opcode.BSTPOS -> emitBranch("bpl", label, leavesSubroutine)
|
||||
Opcode.BSTVC -> emitBranch("bvc", label, leavesSubroutine)
|
||||
Opcode.BSTVS -> emitBranch("bvs", label, leavesSubroutine)
|
||||
|
||||
// Unsigned integer comparison branches
|
||||
Opcode.BGT -> cmpBranchUnsignedImm(insn, label, "bhi")
|
||||
Opcode.BGE -> cmpBranchUnsignedImm(insn, label, "bhs")
|
||||
Opcode.BLT -> cmpBranchUnsignedImm(insn, label, "blo")
|
||||
Opcode.BLE -> cmpBranchUnsignedImm(insn, label, "bls")
|
||||
Opcode.BGT -> cmpBranchUnsignedImm(insn, label, "bhi", leavesSubroutine)
|
||||
Opcode.BGE -> cmpBranchUnsignedImm(insn, label, "bhs", leavesSubroutine)
|
||||
Opcode.BLT -> cmpBranchUnsignedImm(insn, label, "blo", leavesSubroutine)
|
||||
Opcode.BLE -> cmpBranchUnsignedImm(insn, label, "bls", leavesSubroutine)
|
||||
|
||||
Opcode.BGTR -> cmpBranchUnsignedReg(insn, label, "bhi")
|
||||
Opcode.BGER -> cmpBranchUnsignedReg(insn, label, "bhs")
|
||||
Opcode.BGTR -> cmpBranchUnsignedReg(insn, label, "bhi", leavesSubroutine)
|
||||
Opcode.BGER -> cmpBranchUnsignedReg(insn, label, "bhs", leavesSubroutine)
|
||||
// BLTR doesn't exist in IR — uses BGTR with swapped operands
|
||||
|
||||
// Signed integer comparison branches
|
||||
Opcode.BGTS -> cmpBranchSignedImm(insn, label, "bgt")
|
||||
Opcode.BGES -> cmpBranchSignedImm(insn, label, "bge")
|
||||
Opcode.BLTS -> cmpBranchSignedImm(insn, label, "blt")
|
||||
Opcode.BLES -> cmpBranchSignedImm(insn, label, "ble")
|
||||
Opcode.BGTS -> cmpBranchSignedImm(insn, label, "bgt", leavesSubroutine)
|
||||
Opcode.BGES -> cmpBranchSignedImm(insn, label, "bge", leavesSubroutine)
|
||||
Opcode.BLTS -> cmpBranchSignedImm(insn, label, "blt", leavesSubroutine)
|
||||
Opcode.BLES -> cmpBranchSignedImm(insn, label, "ble", leavesSubroutine)
|
||||
|
||||
Opcode.BGTSR -> cmpBranchSignedReg(insn, label, "bgt")
|
||||
Opcode.BGESR -> cmpBranchSignedReg(insn, label, "bge")
|
||||
Opcode.BGTSR -> cmpBranchSignedReg(insn, label, "bgt", leavesSubroutine)
|
||||
Opcode.BGESR -> cmpBranchSignedReg(insn, label, "bge", leavesSubroutine)
|
||||
// BLTSR doesn't exist in IR — uses BGTSR with swapped operands
|
||||
|
||||
else -> error("Unknown branch opcode: ${insn.opcode}")
|
||||
@@ -70,7 +76,7 @@ internal fun AsmGen.translateBranch(insn: IRInstruction) {
|
||||
|
||||
// === Unsigned comparisons: register vs immediate ===
|
||||
|
||||
private fun AsmGen.cmpBranchUnsignedImm(insn: IRInstruction, label: String, branchOp: String) {
|
||||
private fun AsmGen.cmpBranchUnsignedImm(insn: IRInstruction, label: String, branchOp: String, leavesSubroutine: Boolean) {
|
||||
val type = insn.type ?: IRDataType.BYTE
|
||||
val reg = insn.requireSrcA().intNumber
|
||||
val imm = insn.requireImmediateInt()
|
||||
@@ -81,24 +87,24 @@ private fun AsmGen.cmpBranchUnsignedImm(insn: IRInstruction, label: String, bran
|
||||
emitLine("tst$s ${regAddr(reg)}")
|
||||
else
|
||||
emitLine("cmpi$s #$imm, ${regAddr(reg)}")
|
||||
emitBranch(branchOp, label)
|
||||
emitBranch(branchOp, label, leavesSubroutine)
|
||||
}
|
||||
|
||||
// === Unsigned comparisons: register vs register ===
|
||||
|
||||
private fun AsmGen.cmpBranchUnsignedReg(insn: IRInstruction, label: String, branchOp: String) {
|
||||
private fun AsmGen.cmpBranchUnsignedReg(insn: IRInstruction, label: String, branchOp: String, leavesSubroutine: Boolean) {
|
||||
val type = insn.type ?: IRDataType.BYTE
|
||||
val left = insn.requireSrcA().intNumber
|
||||
val right = insn.requireSrcB().intNumber
|
||||
val s = dtSuffix(type)
|
||||
emitLoadD0(left, type)
|
||||
emitLine("cmp$s ${regAddr(right)}, d0")
|
||||
emitBranch(branchOp, label)
|
||||
emitBranch(branchOp, label, leavesSubroutine)
|
||||
}
|
||||
|
||||
// === Signed comparisons: register vs immediate ===
|
||||
|
||||
private fun AsmGen.cmpBranchSignedImm(insn: IRInstruction, label: String, branchOp: String) {
|
||||
private fun AsmGen.cmpBranchSignedImm(insn: IRInstruction, label: String, branchOp: String, leavesSubroutine: Boolean) {
|
||||
val type = insn.type ?: IRDataType.BYTE
|
||||
val reg = insn.requireSrcA().intNumber
|
||||
val imm = insn.requireImmediateInt()
|
||||
@@ -109,22 +115,41 @@ private fun AsmGen.cmpBranchSignedImm(insn: IRInstruction, label: String, branch
|
||||
emitLine("tst$s ${regAddr(reg)}")
|
||||
else
|
||||
emitLine("cmpi$s #$imm, ${regAddr(reg)}")
|
||||
emitBranch(branchOp, label)
|
||||
emitBranch(branchOp, label, leavesSubroutine)
|
||||
}
|
||||
|
||||
// === Signed comparisons: register vs register ===
|
||||
|
||||
private fun AsmGen.cmpBranchSignedReg(insn: IRInstruction, label: String, branchOp: String) {
|
||||
private fun AsmGen.cmpBranchSignedReg(insn: IRInstruction, label: String, branchOp: String, leavesSubroutine: Boolean) {
|
||||
val type = insn.type ?: IRDataType.BYTE
|
||||
val left = insn.requireSrcA().intNumber
|
||||
val right = insn.requireSrcB().intNumber
|
||||
val s = dtSuffix(type)
|
||||
emitLoadD0(left, type)
|
||||
emitLine("cmp$s ${regAddr(right)}, d0")
|
||||
emitBranch(branchOp, label)
|
||||
emitBranch(branchOp, label, leavesSubroutine)
|
||||
}
|
||||
|
||||
private fun AsmGen.emitBranch(branchOp: String, label: String) {
|
||||
private val inverseBranchOps = mapOf(
|
||||
"bcc" to "bcs", "bcs" to "bcc",
|
||||
"beq" to "bne", "bne" to "beq",
|
||||
"bmi" to "bpl", "bpl" to "bmi",
|
||||
"bvc" to "bvs", "bvs" to "bvc",
|
||||
"bhi" to "bls", "bls" to "bhi",
|
||||
"bhs" to "blo", "blo" to "bhs",
|
||||
"bgt" to "ble", "ble" to "bgt",
|
||||
"bge" to "blt", "blt" to "bge")
|
||||
|
||||
private fun AsmGen.emitBranch(branchOp: String, label: String, leavesSubroutine: Boolean) {
|
||||
invalidateD0Cache()
|
||||
emitLine("$branchOp $label")
|
||||
if (!leavesSubroutine || !frameActive) {
|
||||
emitLine("$branchOp $label")
|
||||
return
|
||||
}
|
||||
|
||||
val skipLabel = makeLabel("branch_keep_frame")
|
||||
emitLine("${inverseBranchOps.getValue(branchOp)} $skipLabel")
|
||||
emitFrameUnlk()
|
||||
emitLine("jmp $label")
|
||||
emitLabel(skipLabel)
|
||||
}
|
||||
@@ -26,8 +26,19 @@ internal fun AsmGen.translateControl(insn: IRInstruction, forwardedImmediateCall
|
||||
Opcode.JUMP -> {
|
||||
invalidateD0Cache()
|
||||
when (val t = insn.requireTarget()) {
|
||||
is CodeReference.Label -> emitLine("bra ${fixNameSymbols(t.name)}") // PC-relative branch; vasm picks the optimal size and falls back to jmp if out of range
|
||||
is CodeReference.Absolute -> emitLine("jmp ${t.address.value.toHex()}")
|
||||
is CodeReference.Label -> {
|
||||
// a jump to a label in another subroutine ends this activation, so our frame has to be gone
|
||||
// before we leave: whatever runs next returns to our caller, not to us
|
||||
if (frameActive && labelIsOutsideCurrentSub(t.name))
|
||||
emitFrameUnlk()
|
||||
emitLine("bra ${fixNameSymbols(t.name)}") // PC-relative branch; vasm picks the optimal size and falls back to jmp if out of range
|
||||
}
|
||||
is CodeReference.Absolute -> {
|
||||
// a fixed address can't be resolved to a chunk of this subroutine, so assume it
|
||||
// leaves for good: whatever runs there returns to our caller, not to us
|
||||
emitFrameUnlk()
|
||||
emitLine("jmp ${t.address.value.toHex()}")
|
||||
}
|
||||
is CodeReference.Indirect -> error("JUMP needs target")
|
||||
}
|
||||
}
|
||||
@@ -36,6 +47,8 @@ internal fun AsmGen.translateControl(insn: IRInstruction, forwardedImmediateCall
|
||||
val ref = insn.requireTarget() as? CodeReference.Indirect ?: error("JUMPI needs an indirect target register")
|
||||
val reg = ref.pointer.intNumber
|
||||
invalidateD0Cache()
|
||||
// a dispatch jump leaves this subroutine for good: the target's rts must find our caller's return address, so our frame has to be gone first
|
||||
emitFrameUnlk()
|
||||
if(program.options.compTarget.cpu >= CpuType.M68020) {
|
||||
// 68020+ supports memory-indirect addressing: fetch the target address from memory directly.
|
||||
emitLine("jmp ([${regAddr(reg)}])")
|
||||
@@ -99,8 +112,10 @@ internal fun AsmGen.translateControl(insn: IRInstruction, forwardedImmediateCall
|
||||
// In multi-assign context (returns.size > 1), also skip slot-based returns:
|
||||
// the IR generates LOADHR for them.
|
||||
// In single-return context, process slot returns normally.
|
||||
// LIMITATION: multiple status flag returns in one multi-assign are not supported
|
||||
// (codegen limitation: first flag extraction clobbers flags before second can be read).
|
||||
// Multiple status flag returns in one multi-assign are protected by the IR:
|
||||
// it wraps each flag's extraction in PUSHST/POPST so they don't clobber each other.
|
||||
// On the 68000 cpu this fails, because it cannot save/restore the CCR with a
|
||||
// nonprivileged instruction (compile for 68010 or higher to use this).
|
||||
val isMultiReturn = callSite.results.size > 1
|
||||
for(ret in callSite.results) {
|
||||
if (ret.location is CallLocation.StatusFlag)
|
||||
@@ -129,6 +144,7 @@ internal fun AsmGen.translateControl(insn: IRInstruction, forwardedImmediateCall
|
||||
|
||||
Opcode.RETURN -> {
|
||||
invalidateD0Cache()
|
||||
emitFrameUnlk()
|
||||
emitLine("rts")
|
||||
}
|
||||
|
||||
@@ -142,6 +158,7 @@ internal fun AsmGen.translateControl(insn: IRInstruction, forwardedImmediateCall
|
||||
val reg = insn.requireIntSourceA().intNumber
|
||||
emitLoadD0(reg, type)
|
||||
}
|
||||
emitFrameUnlk()
|
||||
emitLine("rts")
|
||||
}
|
||||
|
||||
@@ -154,6 +171,7 @@ internal fun AsmGen.translateControl(insn: IRInstruction, forwardedImmediateCall
|
||||
emitLine("moveq #$value, d0")
|
||||
else
|
||||
emitLine("move$s #$value, d0")
|
||||
emitFrameUnlk()
|
||||
emitLine("rts")
|
||||
}
|
||||
|
||||
@@ -178,21 +196,18 @@ internal fun AsmGen.translateControl(insn: IRInstruction, forwardedImmediateCall
|
||||
// === Status flag stack ops via Scc ===
|
||||
|
||||
Opcode.PUSHST -> {
|
||||
// Move CCR to D0 (low byte), then push as byte.
|
||||
// Save CCR to the stack. The 68010+ memory form keeps the CCR save
|
||||
// register-neutral (no data register is clobbered) and word-aligned.
|
||||
if(program.options.compTarget.cpu == CpuType.M68000)
|
||||
error("the 68000 cpu cannot save/restore the status bits using a nonprivileged instruction. This is required to implement the 'PUSHST/POPST' IR opcodes. Compile for 68010 or higher cpu or change the code such that PUSHST/POPST are no longer used (sometimes complicated rol/ror operations need it)")
|
||||
invalidateD0Cache()
|
||||
emitLine("move ccr, d0")
|
||||
emitLine("move.b d0, -(sp)")
|
||||
emitLine("move ccr, -(sp)")
|
||||
}
|
||||
|
||||
Opcode.POPST -> {
|
||||
// Pop byte into D0, then restore CCR
|
||||
// Restore CCR from the stack (register-neutral on 68010+).
|
||||
if(program.options.compTarget.cpu == CpuType.M68000)
|
||||
error("the 68000 cpu cannot save/restore the status bits using a nonprivileged instruction. This is required to implement the 'PUSHST/POPST' IR opcodes. Compile for 68010 or higher cpu or change the code such that PUSHST/POPST are no longer used (sometimes complicated rol/ror operations need it)")
|
||||
invalidateD0Cache()
|
||||
emitLine("move.b (sp)+, d0")
|
||||
emitLine("move d0, ccr")
|
||||
emitLine("move (sp)+, ccr")
|
||||
}
|
||||
|
||||
// === Flag manipulation ===
|
||||
@@ -634,16 +649,19 @@ private fun AsmGen.translateCall(fnLabel: String, callSite: CallSite, forwardedI
|
||||
// Emit non-constant arguments first (from regfile), then constant immediates
|
||||
// just before the BSR/JSR to avoid clobbering the constant registers while
|
||||
// evaluating non-const arguments (e.g. address calculations that may use D0/A0 as scratch).
|
||||
val (forwarded, nonForwarded) = if (forwardedImmediateCall != null) {
|
||||
callSite.arguments.partition { it.source.register in forwardedImmediateCall.loads }
|
||||
} else {
|
||||
emptyList<CallArgument>() to callSite.arguments
|
||||
// Parameter names are resolved by argument index so each stack push can be
|
||||
// annotated with the parameter it fills; unknown callees get no annotation.
|
||||
val paramNames = program.allSubs().find { it.label == fnLabel }?.parameters?.map { it.name } ?: emptyList()
|
||||
fun paramNameFor(index: Int): String? =
|
||||
paramNames.getOrNull(index)?.substringAfterLast('.')?.removePrefix("p8v_")
|
||||
val forwardedRegs = forwardedImmediateCall?.loads?.keys ?: emptySet()
|
||||
for ((index, arg) in callSite.arguments.withIndex()) {
|
||||
if (arg.source.register in forwardedRegs) continue
|
||||
translateArgument(arg, fnLabel, forwardedImmediateCall, paramNameFor(index))
|
||||
}
|
||||
for (arg in nonForwarded) {
|
||||
translateArgument(arg, fnLabel, forwardedImmediateCall)
|
||||
}
|
||||
for (arg in forwarded) {
|
||||
translateArgument(arg, fnLabel, forwardedImmediateCall)
|
||||
for ((index, arg) in callSite.arguments.withIndex()) {
|
||||
if (arg.source.register !in forwardedRegs) continue
|
||||
translateArgument(arg, fnLabel, forwardedImmediateCall, paramNameFor(index))
|
||||
}
|
||||
|
||||
// Check if this call targets an inline asmsub — emit its body directly instead of jsr
|
||||
@@ -655,6 +673,20 @@ private fun AsmGen.translateCall(fnLabel: String, callSite: CallSite, forwardedI
|
||||
emitLine("bsr $fnLabel")
|
||||
}
|
||||
|
||||
// caller cleanup of the all-stack argument area:
|
||||
// each argument was pushed into its own padded longword slot
|
||||
val pushedSlots = callSite.arguments.count { it.location is CallLocation.FrameSlot }
|
||||
if (pushedSlots > 0) {
|
||||
val totalSize = pushedSlots * 4
|
||||
val needsCcr = callSite.results.any { it.location is CallLocation.StatusFlag }
|
||||
when {
|
||||
// addq is compact but updates CCR; lea preserves CCR so status-flag returns
|
||||
// remain visible to the IR's branch pattern.
|
||||
totalSize in 1..8 && !needsCcr -> emitLine("addq.l #$totalSize,sp")
|
||||
else -> emitLine("lea $totalSize(sp),sp")
|
||||
}
|
||||
}
|
||||
|
||||
// Move return values back to virtual registers.
|
||||
// Skip status flag returns: always handled by IR's bsteq/bstneg/bstvs branch pattern.
|
||||
// In multi-assign context (results.size > 1), also skip slot-based returns:
|
||||
@@ -662,8 +694,10 @@ private fun AsmGen.translateCall(fnLabel: String, callSite: CallSite, forwardedI
|
||||
// before the branch pattern can read them.
|
||||
// In single-return expression context, process all slot returns normally
|
||||
// (the IR doesn't generate LOADHR for single-return calls).
|
||||
// LIMITATION: multiple status flag returns in one multi-assign (e.g. -> bool @Pz, bool @Pc)
|
||||
// are not supported - codegen limitation: the first flag's extraction clobbers the state for subsequent flags.
|
||||
// Multiple status flag returns in one multi-assign are protected by the IR:
|
||||
// it wraps each flag's extraction in PUSHST/POPST so they don't clobber each other.
|
||||
// On the 68000 cpu this fails, because it cannot save/restore the CCR with a
|
||||
// nonprivileged instruction (compile for 68010 or higher to use this).
|
||||
val isMultiReturn = callSite.results.size > 1
|
||||
for (ret in callSite.results) {
|
||||
if (ret.location is CallLocation.StatusFlag)
|
||||
@@ -757,7 +791,8 @@ private fun AsmGen.emitInlineCopyLoop(iterations: Int, size: Int) {
|
||||
private fun AsmGen.translateArgument(
|
||||
arg: CallArgument,
|
||||
fnLabel: String? = null,
|
||||
forwardedImmediateCall: ImmediateCallOptimization? = null
|
||||
forwardedImmediateCall: ImmediateCallOptimization? = null,
|
||||
paramName: String? = null
|
||||
) {
|
||||
val argReg = arg.source
|
||||
val forwarded = forwardedImmediateCall?.loads?.get(argReg.register)
|
||||
@@ -807,6 +842,32 @@ private fun AsmGen.translateArgument(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Stack argument: push into a padded longword slot, right-justified.
|
||||
// The caller pops the whole argument area after the return, so the callee never needs to know the argument count.
|
||||
// Optimizing this to use word-padding instead of long padding could be done in a future version
|
||||
if (arg.location is CallLocation.FrameSlot) {
|
||||
if (argReg.isFloat) {
|
||||
// the FPU has no absolute-long addressing mode, so the value has to travel
|
||||
// through an FPU register first (fp0 is translator scratch, never holds a live vreg).
|
||||
// The store itself reserves the slot via predecrement, so no separate subq is needed.
|
||||
emitLine("fmove.s ${floatRegFileAddr(argReg.floatNumber)},$FP_ACC")
|
||||
emitLine("fmove.s $FP_ACC,-(sp)", paramName.orEmpty())
|
||||
} else {
|
||||
when (argReg.type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("subq.l #4,sp")
|
||||
emitLine("move.b ${regAddr(argReg.intNumber)},3(sp)", paramName.orEmpty())
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLine("subq.l #4,sp")
|
||||
emitLine("move.w ${regAddr(argReg.intNumber)},2(sp)", paramName.orEmpty())
|
||||
}
|
||||
// longs and pointers fill the whole slot, so reserve and store in one push
|
||||
else -> emitLine("move.l ${regAddr(argReg.intNumber)},-(sp)", paramName.orEmpty())
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
// Store to the callee's parameter variable (if this is a named param)
|
||||
val paramName = (arg.location as? CallLocation.ParameterMemory)?.name.orEmpty()
|
||||
if (paramName.isNotEmpty() && fnLabel != null) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import prog8.code.ast.PtProgram
|
||||
import prog8.code.core.CompilationOptions
|
||||
import prog8.code.core.IErrorReporter
|
||||
import prog8.codegen.intermediate.IRCodeGen
|
||||
import prog8.codegen.intermediate.StackFrameLayout
|
||||
import prog8.intermediate.IRDataType
|
||||
import prog8.intermediate.IRFileWriter
|
||||
import prog8.intermediate.VirtualRegister
|
||||
@@ -28,15 +29,18 @@ class M68kCodeGenerator(val retainSSA: Boolean): ICodeGeneratorBackend {
|
||||
}
|
||||
irProgram.verifyRegisterTypes(virtualRegisterTypes)
|
||||
|
||||
if (options.compTarget.supportsStackFrames)
|
||||
StackFrameLayout(irProgram, errors).apply()
|
||||
|
||||
if (options.dumpVariables)
|
||||
dumpVariables(irProgram)
|
||||
|
||||
IRFileWriter(irProgram, null).write()
|
||||
|
||||
val gen = AsmGen(irProgram, irProgram.options.compTarget)
|
||||
val gen = AsmGen(irProgram, irProgram.options.compTarget, errors)
|
||||
if (!gen.generate())
|
||||
throw RuntimeException("M68k assembly generation failed")
|
||||
|
||||
return AssemblyProgramM68k(irProgram.name, irProgram.options.outputDir)
|
||||
return AssemblyProgramM68k(irProgram.name)
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,11 @@ import com.github.ajalt.clikt.core.CliktCommand
|
||||
import com.github.ajalt.clikt.core.main
|
||||
import com.github.ajalt.clikt.parameters.arguments.argument
|
||||
import prog8.code.core.CpuType
|
||||
import prog8.code.core.OutputFiles
|
||||
import prog8.intermediate.IRFileReader
|
||||
import kotlin.io.path.Path
|
||||
import kotlin.io.path.readText
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val cli = M68kCli()
|
||||
@@ -23,8 +25,12 @@ fun main(args: Array<String>) {
|
||||
when(target.cpu) {
|
||||
CpuType.M68000, CpuType.M68020 -> {
|
||||
val gen = AsmGen(program, target)
|
||||
gen.generate()
|
||||
println("Generated assembly: ${program.name}.asm")
|
||||
val ok = gen.generate()
|
||||
if(!ok) {
|
||||
System.err.println("Assembly failed.")
|
||||
exitProcess(1)
|
||||
}
|
||||
println("Generated assembly: ${OutputFiles.of(program.options, program.name).asm()}")
|
||||
}
|
||||
else -> {
|
||||
error("This code generator only works for M68000 or M68020 CPU.")
|
||||
|
||||
@@ -1255,7 +1255,7 @@ class TestInstructionSelectionOptimizations : FunSpec({
|
||||
lines.any { it == "move.w #1234,var" } shouldBe true
|
||||
}
|
||||
|
||||
// === extb.l gate robustification ===
|
||||
// === extb.l gate hardening ===
|
||||
|
||||
test("M68020 sign-extends byte to long with extb.l") {
|
||||
val lines = generateAsm(
|
||||
@@ -1279,4 +1279,80 @@ class TestInstructionSelectionOptimizations : FunSpec({
|
||||
lines.count { it == "ext.w d0" } shouldBe 1
|
||||
lines.count { it == "ext.l d0" } shouldBe 1
|
||||
}
|
||||
|
||||
test("M5 drops a lea when a0 already holds the same base") {
|
||||
val lines = mutableListOf(
|
||||
" lea arr,a0",
|
||||
" move.w d1,(a0,d0.w)",
|
||||
" lea arr,a0",
|
||||
" clr.w (a0,d0.w)"
|
||||
)
|
||||
optimizeAssembly(lines)
|
||||
lines.count { it.trimStart().startsWith("lea arr,a0") } shouldBe 1
|
||||
lines.any { it.trimStart().startsWith("clr.w") } shouldBe true
|
||||
}
|
||||
|
||||
test("M5 keeps a lea when a different base was loaded into a0") {
|
||||
val lines = mutableListOf(
|
||||
" lea arr,a0",
|
||||
" move.w d1,(a0,d0.w)",
|
||||
" lea other,a0",
|
||||
" lea arr,a0",
|
||||
" clr.w (a0,d0.w)"
|
||||
)
|
||||
optimizeAssembly(lines)
|
||||
lines.count { it.trimStart().startsWith("lea arr,a0") } shouldBe 2
|
||||
lines.count { it.trimStart().startsWith("lea other,a0") } shouldBe 1
|
||||
}
|
||||
|
||||
test("M5 keeps a lea when a0 is written in between") {
|
||||
val lines = mutableListOf(
|
||||
" lea arr,a0",
|
||||
" movea.l other,a0",
|
||||
" lea arr,a0",
|
||||
" clr.w (a0,d0.w)"
|
||||
)
|
||||
optimizeAssembly(lines)
|
||||
lines.count { it.trimStart().startsWith("lea arr,a0") } shouldBe 2
|
||||
}
|
||||
|
||||
test("M5 keeps a lea when a call intervenes") {
|
||||
val lines = mutableListOf(
|
||||
" lea arr,a0",
|
||||
" bsr some.sub",
|
||||
" lea arr,a0",
|
||||
" clr.w (a0,d0.w)"
|
||||
)
|
||||
optimizeAssembly(lines)
|
||||
lines.count { it.trimStart().startsWith("lea arr,a0") } shouldBe 2
|
||||
lines.any { it.trimStart().startsWith("bsr") } shouldBe true
|
||||
}
|
||||
|
||||
test("M5 forgets a0 across a label") {
|
||||
val lines = mutableListOf(
|
||||
" lea arr,a0",
|
||||
"somewhere:",
|
||||
" lea arr,a0",
|
||||
" clr.w (a0,d0.w)"
|
||||
)
|
||||
optimizeAssembly(lines)
|
||||
lines.count { it.trimStart().startsWith("lea arr,a0") } shouldBe 2
|
||||
lines.any { it.trimStart().startsWith("somewhere:") } shouldBe true
|
||||
}
|
||||
|
||||
test("M5 drops a lea across a conditional branch to a label") {
|
||||
// A conditional branch does not modify a0, and its target is a label where the tracked
|
||||
// value is dropped, so the fallthrough still knows a0 holds the base.
|
||||
val lines = mutableListOf(
|
||||
" lea arr,a0",
|
||||
" move.w (a0,d0.w),d0",
|
||||
" bge clamp.done",
|
||||
" lea arr,a0",
|
||||
" clr.w (a0,d0.w)",
|
||||
"clamp.done:"
|
||||
)
|
||||
optimizeAssembly(lines)
|
||||
lines.count { it.trimStart().startsWith("lea arr,a0") } shouldBe 1
|
||||
lines.any { it.trimStart().startsWith("clr.w") } shouldBe true
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,467 @@
|
||||
package prog8tests.codegen.m68k
|
||||
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.engine.spec.tempdir
|
||||
import io.kotest.matchers.shouldBe
|
||||
import prog8.code.core.*
|
||||
import prog8.code.target.Qemu68kTarget
|
||||
import prog8.codegen.m68k.AsmGen
|
||||
import prog8.intermediate.*
|
||||
import prog8tests.helpers.DummyStringEncoder
|
||||
import prog8tests.helpers.ErrorReporterForTests
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.readText
|
||||
|
||||
// Vertical Slice Prototype: m68k backend lowering of FrameSlot memory references
|
||||
// and the link/unlk frame prologue/epilogue.
|
||||
class TestStackFrameEmission : FunSpec({
|
||||
|
||||
val tempRoot = tempdir().toPath()
|
||||
|
||||
fun generateAsmWithIncoming(
|
||||
frameSize: Int,
|
||||
instructions: List<IRInstruction>,
|
||||
incomingSize: Int = 0,
|
||||
subLabel: String = "test.start",
|
||||
frameVregSlots: Map<Int, Int> = emptyMap(),
|
||||
errors: ErrorReporterForTests? = null
|
||||
): List<String> {
|
||||
val options = CompilationOptions.builder(Qemu68kTarget())
|
||||
.output(OutputType.RAW)
|
||||
.zeropage(ZeropageType.DONTUSE)
|
||||
.floats(false)
|
||||
.compilerVersion("test")
|
||||
.memtopAddress(0xffffu)
|
||||
.optimize(true)
|
||||
.build()
|
||||
val program = IRProgram("test", IRSymbolTable(), options, DummyStringEncoder)
|
||||
program.options.outputDir = tempRoot
|
||||
val chunk = IRCodeChunk(subLabel, null)
|
||||
chunk.instructions.addAll(instructions)
|
||||
val params = if (incomingSize > 0)
|
||||
List(incomingSize / 4) { IRSubroutine.IRParam("p8v_p$it", DataType.UBYTE) }
|
||||
else emptyList()
|
||||
val sub = IRSubroutine(subLabel, params, emptyList(), Position.DUMMY, frameSize, incomingSize, frameVregSlots)
|
||||
sub.chunks.add(chunk)
|
||||
val block = IRBlock("test", false, IRBlock.Options(), Position.DUMMY)
|
||||
block.children.add(sub)
|
||||
program.blocks.add(block)
|
||||
|
||||
tempRoot.toFile().deleteRecursively()
|
||||
tempRoot.toFile().mkdirs()
|
||||
AsmGen(program, Qemu68kTarget(), errors).generate()
|
||||
val asmFile = tempRoot.resolve("test.asm")
|
||||
check(asmFile.exists()) { "Assembly file not written: $asmFile" }
|
||||
return asmFile.readText().lines().map { it.trim() }
|
||||
}
|
||||
|
||||
fun generateAsm(frameSize: Int, instructions: List<IRInstruction>): List<String> =
|
||||
generateAsmWithIncoming(frameSize, instructions, 0, "test.start")
|
||||
|
||||
// builds a subroutine with a chunk per extra chunk label, for jump-target scope tests
|
||||
fun sub(label: String, frameSize: Int, instructions: List<IRInstruction>, extraChunks: List<IRCodeChunkBase> = emptyList()): IRSubroutine {
|
||||
val first = IRCodeChunk(label, null)
|
||||
first.instructions.addAll(instructions)
|
||||
val sub = IRSubroutine(label, emptyList(), emptyList(), Position.DUMMY, frameSize, 0, emptyMap())
|
||||
sub.chunks.add(first)
|
||||
sub.chunks.addAll(extraChunks)
|
||||
return sub
|
||||
}
|
||||
|
||||
fun generateAsmForSubs(vararg subs: IRSubroutine): List<String> {
|
||||
val options = CompilationOptions.builder(Qemu68kTarget())
|
||||
.output(OutputType.RAW)
|
||||
.zeropage(ZeropageType.DONTUSE)
|
||||
.floats(false)
|
||||
.compilerVersion("test")
|
||||
.memtopAddress(0xffffu)
|
||||
.optimize(true)
|
||||
.build()
|
||||
val program = IRProgram("test", IRSymbolTable(), options, DummyStringEncoder)
|
||||
program.options.outputDir = tempRoot
|
||||
val block = IRBlock("test", false, IRBlock.Options(), Position.DUMMY)
|
||||
block.children.addAll(subs)
|
||||
program.blocks.add(block)
|
||||
program.linkChunks()
|
||||
tempRoot.toFile().deleteRecursively()
|
||||
tempRoot.toFile().mkdirs()
|
||||
AsmGen(program, Qemu68kTarget(), null).generate()
|
||||
return tempRoot.resolve("test.asm").readText().lines().map { it.trim() }
|
||||
}
|
||||
|
||||
test("frameless subroutine emits no frame setup") {
|
||||
val lines = generateAsm(0, listOf(
|
||||
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.BYTE, 1, IRMemory.direct("some.var")),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
lines.any { it.startsWith("link") } shouldBe false
|
||||
lines.any { it.startsWith("unlk") } shouldBe false
|
||||
}
|
||||
|
||||
test("framed subroutine emits link prologue and unlk epilogue") {
|
||||
val lines = generateAsm(16, listOf(
|
||||
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.BYTE, IRMemory.frameDirect(-1)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
lines.any { it == "link a5,#-16" } shouldBe true
|
||||
lines.count { it == "unlk a5" } shouldBe 1
|
||||
lines.any { "-1(a5)" in it } shouldBe true
|
||||
// epilogue must directly precede the subroutine's return
|
||||
lines[lines.indexOf("unlk a5") + 1] shouldBe "rts"
|
||||
}
|
||||
|
||||
test("frame slot direct references lower to displacements relative to a5") {
|
||||
val lines = generateAsm(16, listOf(
|
||||
IRInstructions.storeImmediate(IRDataType.WORD, 5, IRMemory.frameDirect(-4)),
|
||||
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.BYTE, 1, IRMemory.frameDirect(-1)),
|
||||
IRInstructions.memoryOpImmediate(Opcode.ADDIM, IRDataType.BYTE, IRMemory.frameDirect(-1), 3),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
lines.any { it.contains("-4(a5)") && it.startsWith("move.w") } shouldBe true
|
||||
lines.any { it.contains("-1(a5)") } shouldBe true
|
||||
lines.any { it.startsWith("addq.b") || it.startsWith("add.b") } shouldBe true
|
||||
}
|
||||
|
||||
test("indexed frame slot access uses lea of the slot base plus index") {
|
||||
val lines = generateAsm(16, listOf(
|
||||
IRInstructions.loadMemory(Opcode.LOADX, IRDataType.BYTE, 1, IRMemory.frameIndexed(-8, 3, IRDataType.WORD, scale = 1)),
|
||||
IRInstructions.storeZero(Opcode.STOREZX, IRDataType.BYTE, IRMemory.frameIndexed(-8, 3, IRDataType.WORD, scale = 1)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
lines.any { it.startsWith("lea") && "-8(a5)" in it } shouldBe true
|
||||
lines.count { it.contains("(a0,d0.w)") } shouldBe 2
|
||||
}
|
||||
|
||||
test("frame displacement combines slot offset and reference displacement") {
|
||||
val lines = generateAsm(16, listOf(
|
||||
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.BYTE, IRMemory.frameDirect(-8, 4)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
// -8 slot + 4 displacement = -4(a5), a single combined displacement
|
||||
lines.any { it.contains("-4(a5)") } shouldBe true
|
||||
lines.any { it.contains("-8(a5)+4") } shouldBe false
|
||||
}
|
||||
|
||||
test("incoming-only frame still emits link and unlk") {
|
||||
val lines = generateAsmWithIncoming(0, listOf(IRInstructions.simple(Opcode.RETURN)), incomingSize = 12)
|
||||
lines.any { it == "link a5,#0" } shouldBe true
|
||||
lines.any { it == "unlk a5" } shouldBe true
|
||||
}
|
||||
|
||||
test("stack arguments are pushed right-justified and popped by the caller") {
|
||||
val calls = listOf(
|
||||
IRInstructions.call(CallSite(
|
||||
target = CallTarget.Direct(CodeReference.Label("test.sub")),
|
||||
arguments = listOf(
|
||||
Calls.argument(1, IRDataType.BYTE, CallLocation.FrameSlot(16)),
|
||||
Calls.argument(2, IRDataType.WORD, CallLocation.FrameSlot(12)),
|
||||
Calls.argument(3, IRDataType.LONG, CallLocation.FrameSlot(8))
|
||||
)
|
||||
)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
)
|
||||
val lines = generateAsmWithIncoming(0, calls, incomingSize = 12, subLabel = "test.caller")
|
||||
lines.count { it == "subq.l #4,sp" } shouldBe 2
|
||||
lines.count { it.startsWith("move.b ") && it.contains(",3(sp)") } shouldBe 1
|
||||
lines.count { it.startsWith("move.w ") && it.contains(",2(sp)") } shouldBe 1
|
||||
lines.count { it.startsWith("move.l ") && it.contains(",-(sp)") } shouldBe 1
|
||||
lines.any { it.startsWith("bsr") } shouldBe true
|
||||
lines.any { it == "lea 12(sp),sp" } shouldBe true
|
||||
// small argument areas use the short addq form
|
||||
val smallCalls = listOf(
|
||||
IRInstructions.call(CallSite(
|
||||
target = CallTarget.Direct(CodeReference.Label("test.sub")),
|
||||
arguments = listOf(Calls.argument(1, IRDataType.BYTE, CallLocation.FrameSlot(8)))
|
||||
)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
)
|
||||
val smallLines = generateAsmWithIncoming(0, smallCalls, incomingSize = 4, subLabel = "test.caller")
|
||||
smallLines.any { it == "addq.l #4,sp" } shouldBe true
|
||||
}
|
||||
|
||||
test("status-flag results use CCR-preserving cleanup") {
|
||||
val calls = listOf(
|
||||
IRInstructions.call(CallSite(
|
||||
target = CallTarget.Direct(CodeReference.Label("test.sub")),
|
||||
arguments = listOf(Calls.argument(1, IRDataType.BYTE, CallLocation.FrameSlot(8))),
|
||||
results = listOf(CallResult(null, CallLocation.StatusFlag(Statusflag.Pz)))
|
||||
)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
)
|
||||
val lines = generateAsmWithIncoming(0, calls, incomingSize = 4, subLabel = "test.caller")
|
||||
// a single byte argument would normally use addq.l #4,sp, but that clobbers CCR,
|
||||
// so status-flag returns must use lea instead.
|
||||
lines.any { it == "lea 4(sp),sp" } shouldBe true
|
||||
lines.none { it == "addq.l #4,sp" } shouldBe true
|
||||
}
|
||||
|
||||
test("float stack arguments travel through the FPU accumulator") {
|
||||
val calls = listOf(
|
||||
IRInstructions.call(CallSite(
|
||||
target = CallTarget.Direct(CodeReference.Label("test.sub")),
|
||||
arguments = listOf(Calls.argument(1, IRDataType.FLOAT, CallLocation.FrameSlot(8)))
|
||||
)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
)
|
||||
val lines = generateAsmWithIncoming(0, calls, incomingSize = 4, subLabel = "test.caller")
|
||||
// the 68881 has no absolute-long addressing mode: the value must pass through fp0,
|
||||
// and the store itself reserves the slot via predecrement
|
||||
lines.any { it.startsWith("fmove.s p8_fregfile+0,fp0") } shouldBe true
|
||||
lines.any { it == "fmove.s fp0,-(sp)" } shouldBe true
|
||||
lines.none { it == "subq.l #4,sp" } shouldBe true
|
||||
}
|
||||
test("per-activation virtual registers are addressed through the frame") {
|
||||
// a re-entrant subroutine keeps its virtual registers in its own frame (slice 3), so they
|
||||
// are addressed relative to a5 instead of the flat program-static register file
|
||||
val lines = generateAsmWithIncoming(8, listOf(
|
||||
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.WORD, 5, IRMemory.direct("main.var")),
|
||||
IRInstructions.returnRegister(IRDataType.WORD, 5)
|
||||
), frameVregSlots = mapOf(5 to -2, 6 to -4))
|
||||
lines.any { it.startsWith("link ") } shouldBe true
|
||||
lines.any { it.startsWith("move") && it.contains("-2(a5)") } shouldBe true
|
||||
lines.none { it.startsWith("move") && it.contains("p8_regfile") } shouldBe true
|
||||
}
|
||||
|
||||
test("virtual registers without a frame slot keep using the static register file") {
|
||||
val lines = generateAsmWithIncoming(8, listOf(
|
||||
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.WORD, 5, IRMemory.direct("main.var")),
|
||||
IRInstructions.returnRegister(IRDataType.WORD, 5)
|
||||
), frameVregSlots = mapOf(6 to -4))
|
||||
// register 5 has no frame slot, so it stays in the static register file
|
||||
lines.any { it.startsWith("move") && it.contains("p8_regfile") } shouldBe true
|
||||
lines.any { it.startsWith("move") && it.contains("(a5)") && it.contains("p8_regfile") } shouldBe false
|
||||
}
|
||||
|
||||
test("a byte-sized frame-resident register is addressed with its byte offset") {
|
||||
val lines = generateAsmWithIncoming(4, listOf(
|
||||
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.BYTE, 5, IRMemory.direct("main.var")),
|
||||
IRInstructions.returnRegister(IRDataType.BYTE, 5)
|
||||
), frameVregSlots = mapOf(5 to -4))
|
||||
lines.any { it.startsWith("move.b") && it.contains("-4(a5)") } shouldBe true
|
||||
}
|
||||
|
||||
test("sub-byte extraction of a frame-resident register uses a valid a5 displacement") {
|
||||
// MSIGB on a long goes through regAddrByte, which must not emit "$(-4)(a5)"
|
||||
val lines = generateAsmWithIncoming(4, listOf(
|
||||
IRInstructions.binary(Opcode.MSIGB, IRDataType.LONG, 6, 5),
|
||||
IRInstructions.returnRegister(IRDataType.BYTE, 6)
|
||||
), frameVregSlots = mapOf(5 to -4))
|
||||
lines.any { it.startsWith("move.b") && it.contains("-4(a5)") } shouldBe true
|
||||
lines.none { it.contains("\$(-4)(a5)") } shouldBe true
|
||||
}
|
||||
|
||||
test("framed subroutine emits a stack frame listing comment") {
|
||||
val lines = generateAsmWithIncoming(24, listOf(
|
||||
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.BYTE, IRMemory.frameDirect(-1)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
), incomingSize = 8)
|
||||
lines.any { it == "; stack frame: 24 bytes of locals, 8 bytes of arguments" } shouldBe true
|
||||
}
|
||||
|
||||
test("incoming-only frame emits a stack frame listing comment with zero locals") {
|
||||
val lines = generateAsmWithIncoming(0, listOf(IRInstructions.simple(Opcode.RETURN)), incomingSize = 8)
|
||||
lines.any { it == "; stack frame: 0 bytes of locals, 8 bytes of arguments" } shouldBe true
|
||||
}
|
||||
|
||||
test("frameless subroutine emits no stack frame listing comment") {
|
||||
val lines = generateAsm(0, listOf(
|
||||
IRInstructions.loadMemory(Opcode.LOADM, IRDataType.BYTE, 1, IRMemory.direct("some.var")),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
lines.none { it.startsWith("; stack frame:") } shouldBe true
|
||||
}
|
||||
|
||||
test("subroutine boundary markers are emitted exactly once per subroutine") {
|
||||
val lines = generateAsm(16, listOf(
|
||||
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.BYTE, IRMemory.frameDirect(-1)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
lines.count { it == "; ---- Subroutine: test.start ----" } shouldBe 1
|
||||
lines.count { it == "; End of subroutine: test.start" } shouldBe 1
|
||||
}
|
||||
|
||||
test("large frames report an informational message") {
|
||||
val errors = ErrorReporterForTests()
|
||||
generateAsmWithIncoming(900, listOf(
|
||||
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.BYTE, IRMemory.frameDirect(-1)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
), subLabel = "p8b_main.p8s_worker", errors = errors)
|
||||
errors.infos.any { it.contains("subroutine 'main.worker' uses a 900 byte stack frame; deep recursion multiplies this per call") } shouldBe true
|
||||
}
|
||||
|
||||
test("small frames produce no informational message") {
|
||||
val errors = ErrorReporterForTests()
|
||||
generateAsmWithIncoming(24, listOf(
|
||||
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.BYTE, IRMemory.frameDirect(-1)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
), incomingSize = 8, errors = errors)
|
||||
errors.infos shouldBe emptyList()
|
||||
}
|
||||
|
||||
test("a long run of longword zero-clears lowers to a dbra loop") {
|
||||
val stores = (0 until 17).map { i ->
|
||||
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.LONG, IRMemory.frameDirect(-68 + i * 4))
|
||||
}
|
||||
val lines = generateAsm(68, stores + IRInstructions.simple(Opcode.RETURN))
|
||||
lines.any { it == "moveq #0,d0" } shouldBe true
|
||||
lines.any { it == "lea -68(a5),a0" } shouldBe true
|
||||
lines.any { it == "moveq #16,d1" } shouldBe true
|
||||
lines.any { it == "move.l d0,(a0)+" } shouldBe true
|
||||
lines.count { it.startsWith("dbra d1,zeroloop_") } shouldBe 1
|
||||
lines.none { it.startsWith("clr.l") } shouldBe true
|
||||
}
|
||||
|
||||
test("a short run of longword zero-clears keeps straight stores") {
|
||||
val stores = (0 until 15).map { i ->
|
||||
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.LONG, IRMemory.frameDirect(-60 + i * 4))
|
||||
}
|
||||
val lines = generateAsm(60, stores + IRInstructions.simple(Opcode.RETURN))
|
||||
lines.count { it.startsWith("clr.l") } shouldBe 15
|
||||
lines.none { it.startsWith("dbra") } shouldBe true
|
||||
}
|
||||
|
||||
test("an interleaved store splits the zero-clear run") {
|
||||
val first = (0 until 8).map { i ->
|
||||
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.LONG, IRMemory.frameDirect(-68 + i * 4))
|
||||
}
|
||||
val second = (0 until 8).map { i ->
|
||||
IRInstructions.storeZero(Opcode.STOREZM, IRDataType.LONG, IRMemory.frameDirect(-32 + i * 4))
|
||||
}
|
||||
val instructions = first +
|
||||
IRInstructions.storeImmediate(IRDataType.BYTE, 0, IRMemory.frameDirect(-36)) +
|
||||
second +
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
val lines = generateAsm(68, instructions)
|
||||
// two runs of 8 stay below the loop threshold
|
||||
lines.none { it.startsWith("dbra") } shouldBe true
|
||||
lines.count { it.startsWith("clr.l") } shouldBe 16
|
||||
lines.any { it == "clr.b -36(a5)" } shouldBe true
|
||||
}
|
||||
|
||||
test("dispatch jump in a framed subroutine pops the frame before jumping") {
|
||||
// an on..goto leaves the subroutine for good, so the target's rts must find our caller's
|
||||
// return address: leaving our frame behind would make it pop garbage
|
||||
val lines = generateAsm(16, listOf(
|
||||
IRInstructions.jumpIndirect(5),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
val unlkIdx = lines.indexOf("unlk a5")
|
||||
unlkIdx shouldBe lines.indexOfFirst { it.startsWith("jmp") } - 1
|
||||
}
|
||||
|
||||
test("dispatch jump in a frameless subroutine emits no frame teardown") {
|
||||
val lines = generateAsm(0, listOf(
|
||||
IRInstructions.jumpIndirect(5),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
lines.none { it.startsWith("unlk") } shouldBe true
|
||||
}
|
||||
|
||||
test("jump to an absolute address pops the frame before jumping") {
|
||||
// a fixed address can't be resolved to a chunk of this subroutine, so it must be assumed
|
||||
// to leave for good and the frame must be gone before the target's eventual rts
|
||||
val lines = generateAsm(16, listOf(
|
||||
IRInstructions.jump(codeAddress(0x1234u)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
val jmpIdx = lines.indexOfFirst { it.startsWith("jmp") }
|
||||
(jmpIdx > 0) shouldBe true
|
||||
lines[jmpIdx - 1] shouldBe "unlk a5"
|
||||
}
|
||||
|
||||
test("jump to an absolute address in a frameless subroutine emits no frame teardown") {
|
||||
val lines = generateAsm(0, listOf(
|
||||
IRInstructions.jump(codeAddress(0x1234u)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
lines.none { it.startsWith("unlk") } shouldBe true
|
||||
}
|
||||
|
||||
test("conditional branch to an absolute address tears down only the taken path") {
|
||||
val lines = generateAsm(16, listOf(
|
||||
IRInstructions.branch(Opcode.BSTEQ, codeAddress(0x1234u)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
val branchIdx = lines.indexOfFirst { it.startsWith("bne") }
|
||||
val jmpIdx = lines.indexOfFirst { it.startsWith("jmp") && "$1234" in it }
|
||||
(branchIdx >= 0) shouldBe true
|
||||
(jmpIdx > branchIdx) shouldBe true
|
||||
lines[jmpIdx - 1] shouldBe "unlk a5"
|
||||
lines.count { it == "unlk a5" } shouldBe 2
|
||||
}
|
||||
|
||||
test("conditional branch to an external non-entry label tears down only the taken path") {
|
||||
val outsideInner = IRCodeChunk("test.outside.inner", null).also {
|
||||
it.instructions.add(IRInstructions.simple(Opcode.RETURN))
|
||||
}
|
||||
val lines = generateAsmForSubs(
|
||||
sub("test.start", frameSize = 16, instructions = listOf(
|
||||
IRInstructions.branch(Opcode.BSTEQ, CodeReference.Label("test.outside.inner")),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
)),
|
||||
sub("test.outside", frameSize = 0, instructions = listOf(IRInstructions.simple(Opcode.RETURN)),
|
||||
extraChunks = listOf(outsideInner))
|
||||
)
|
||||
val jmpIdx = lines.indexOfFirst { it.startsWith("jmp") && "outside.inner" in it }
|
||||
(jmpIdx > 0) shouldBe true
|
||||
lines[jmpIdx - 1] shouldBe "unlk a5"
|
||||
lines.any { it.startsWith("bne") } shouldBe true
|
||||
}
|
||||
|
||||
test("signed comparison branch to an absolute address tears down only the taken path") {
|
||||
val lines = generateAsm(16, listOf(
|
||||
IRInstructions.branchImmediate(Opcode.BGTS, IRDataType.WORD, 1, 1, codeAddress(0x1234u)),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
))
|
||||
lines.any { it.startsWith("cmpi.w") } shouldBe true
|
||||
val branchIdx = lines.indexOfFirst { it.startsWith("ble") }
|
||||
val jmpIdx = lines.indexOfFirst { it.startsWith("jmp") && "$1234" in it }
|
||||
(branchIdx >= 0) shouldBe true
|
||||
(jmpIdx > branchIdx) shouldBe true
|
||||
lines[jmpIdx - 1] shouldBe "unlk a5"
|
||||
lines.count { it == "unlk a5" } shouldBe 2
|
||||
}
|
||||
|
||||
test("jump to a label in another subroutine pops the frame before jumping") {
|
||||
// jumping out of the subroutine is a documented way to end it, so our frame must not be
|
||||
// left behind for whatever runs next (and its eventual rts) to trip over
|
||||
val lines = generateAsmForSubs(
|
||||
sub("test.start", frameSize = 16, instructions = listOf(
|
||||
IRInstructions.jump(CodeReference.Label("test.outside")),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
)),
|
||||
sub("test.outside", frameSize = 0, instructions = listOf(IRInstructions.simple(Opcode.RETURN)))
|
||||
)
|
||||
val braIdx = lines.indexOfFirst { it.startsWith("bra") && "test.outside" in it }
|
||||
(braIdx > 0) shouldBe true
|
||||
lines[braIdx - 1] shouldBe "unlk a5"
|
||||
}
|
||||
|
||||
test("jump to a label in a chunk of the same subroutine keeps the frame") {
|
||||
val lines = generateAsmForSubs(
|
||||
sub("test.start", frameSize = 16, instructions = listOf(
|
||||
IRInstructions.jump(CodeReference.Label("test.start.second")),
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
), extraChunks = listOf(
|
||||
IRCodeChunk("test.start.second", null).also {
|
||||
it.instructions.add(IRInstructions.storeImmediate(IRDataType.BYTE, 0, IRMemory.direct("test.var")))
|
||||
}
|
||||
))
|
||||
)
|
||||
lines.count { it == "unlk a5" } shouldBe 1 // only the return epilogue
|
||||
lines[lines.indexOf("unlk a5") + 1] shouldBe "rts"
|
||||
lines.any { it.startsWith("bra") && "test.start.second" in it } shouldBe true
|
||||
}
|
||||
|
||||
test("jump to a label inside a loop body of the same subroutine keeps the frame") {
|
||||
val loopBodyChunk = IRCodeChunk("test.start.loopbody", null)
|
||||
loopBodyChunk.instructions.add(IRInstructions.jump(CodeReference.Label("test.start.loopbody")))
|
||||
val lines = generateAsmForSubs(
|
||||
sub("test.start", frameSize = 16, instructions = listOf(
|
||||
IRInstructions.simple(Opcode.RETURN)
|
||||
), extraChunks = listOf(IRLoopChunk("test.start.loop", 3, mutableListOf(loopBodyChunk))))
|
||||
)
|
||||
lines.count { it == "unlk a5" } shouldBe 1 // the jump to the loop body label must not tear the frame down
|
||||
}
|
||||
|
||||
})
|
||||
@@ -31,6 +31,7 @@
|
||||
|
||||
package prog8.codegen.new6502
|
||||
|
||||
import prog8.code.StStructInstanceBlockName
|
||||
import prog8.code.core.*
|
||||
import prog8.codegen.new6502.optimization.PeepholeOptimizer
|
||||
import prog8.intermediate.*
|
||||
@@ -116,7 +117,7 @@ internal class AsmGen(val program: IRProgram, private val target: ICompilationTa
|
||||
emitBssSection()
|
||||
|
||||
val options = program.options
|
||||
val asmFile = options.outputDir.resolve("${program.name}.asm")
|
||||
val asmFile = OutputFiles.of(options, program.name).asm()
|
||||
try {
|
||||
asmFile.toFile().writeText(output.toString())
|
||||
} catch (e: Exception) {
|
||||
@@ -227,7 +228,25 @@ internal class AsmGen(val program: IRProgram, private val target: ICompilationTa
|
||||
name.startsWith('>') -> ">" to name.drop(1)
|
||||
else -> "" to name
|
||||
}
|
||||
return "$prefix${fixNameSymbols(rest)}"
|
||||
val node = program.st.lookup(rest)
|
||||
return when (node) {
|
||||
is IRStStructInstance -> "$prefix${structInstanceRef(node, rest)}"
|
||||
else -> "$prefix${fixNameSymbols(rest)}"
|
||||
}
|
||||
}
|
||||
|
||||
private fun structInstanceRef(node: IRStStructInstance, name: String): String {
|
||||
// Uninitialized instances (empty initializer, or unions) are emitted into
|
||||
// the prog8_struct_instances_bss block, but the IR bakes in the plain
|
||||
// prog8_struct_instances prefix into every reference. Rewrite those to
|
||||
// match the emitted block label.
|
||||
val def = program.st.lookup(node.structName) as IRStStructDef
|
||||
val fixed = fixNameSymbols(name)
|
||||
return if (node.values.isEmpty() || def.isUnion) {
|
||||
"${StStructInstanceBlockName}_bss.${fixed.substringAfter('.')}"
|
||||
} else {
|
||||
fixed
|
||||
}
|
||||
}
|
||||
|
||||
fun resolveSymbolRef(name: String): String {
|
||||
@@ -235,8 +254,8 @@ internal class AsmGen(val program: IRProgram, private val target: ICompilationTa
|
||||
return when (node) {
|
||||
is IRStConstant -> constLabel(name)
|
||||
is IRStStaticVariable, is IRStMemVar,
|
||||
is IRStMemorySlab,
|
||||
is IRStStructInstance -> fixNameSymbols(name)
|
||||
is IRStMemorySlab -> fixNameSymbols(name)
|
||||
is IRStStructInstance -> structInstanceRef(node, name)
|
||||
else -> name // external/library symbol, keep as-is
|
||||
}
|
||||
}
|
||||
@@ -936,34 +955,58 @@ internal class AsmGen(val program: IRProgram, private val target: ICompilationTa
|
||||
if (structs.isEmpty()) return
|
||||
emitRaw("; struct type definitions")
|
||||
for (sd in structs) {
|
||||
val paramFields = sd.fields.filter { it.arraySize == null }
|
||||
val structargs = paramFields.indices.joinToString(",") { "f$it" }
|
||||
emitRaw("${fixNameSymbols(sd.name)} .struct $structargs")
|
||||
var paramIdx = 0
|
||||
for (field in sd.fields) {
|
||||
val type = when {
|
||||
field.type.isBool || field.type.isUnsignedByte -> ".byte"
|
||||
field.type.isSignedByte -> ".char"
|
||||
field.type.isUnsignedWord || field.type.isPointer -> ".word"
|
||||
field.type.isSignedWord -> ".sint"
|
||||
field.type.isLong -> ".dint"
|
||||
field.type.isFloat -> ".byte"
|
||||
else -> ".byte"
|
||||
if(sd.isUnion) {
|
||||
// NB: .union takes no constructor parameters, unlike .struct
|
||||
emitRaw("${fixNameSymbols(sd.name)} .union")
|
||||
emitStructDefFields(sd) { "?" }
|
||||
emitRaw(" .endunion")
|
||||
emitRaw("${fixNameSymbols(sd.name)}_size=${sd.size}")
|
||||
} else {
|
||||
val paramFields = sd.fields.filter { it.arraySize == null }
|
||||
val structargs = paramFields.indices.joinToString(",") { "f$it" }
|
||||
emitRaw("${fixNameSymbols(sd.name)} .struct $structargs")
|
||||
var paramIdx = 0
|
||||
emitStructDefFields(sd) { "\\f${paramIdx++}" }
|
||||
emitRaw(" .endstruct")
|
||||
emitRaw("${fixNameSymbols(sd.name)}_size=${sd.size}")
|
||||
}
|
||||
emitRaw("")
|
||||
}
|
||||
emitRaw("")
|
||||
}
|
||||
|
||||
private fun emitStructDefFields(sd: IRStStructDef, scalarValue: () -> String) {
|
||||
for (field in sd.fields) {
|
||||
val fieldName = "p8v_${field.name}"
|
||||
val arrSize = field.arraySize
|
||||
val effectiveDt = if(arrSize != null) field.type.elementType() else field.type
|
||||
if (effectiveDt.isFloat) {
|
||||
val floatSize = target.FLOAT_MEM_SIZE.toInt()
|
||||
if (arrSize != null) {
|
||||
emitRaw(" $fieldName .fill ${floatSize*arrSize}, ?")
|
||||
} else {
|
||||
emitRaw(" $fieldName .fill $floatSize, ${scalarValue()}")
|
||||
}
|
||||
val fieldName = "p8v_${field.name}"
|
||||
val arrSize = field.arraySize
|
||||
} else {
|
||||
val type = structDefFieldAsmType(effectiveDt)
|
||||
if (arrSize != null) {
|
||||
val anon = List(arrSize) { "?" }.joinToString(",")
|
||||
emitRaw(" $fieldName $type $anon")
|
||||
} else {
|
||||
emitRaw(" $fieldName $type \\f$paramIdx")
|
||||
paramIdx++
|
||||
emitRaw(" $fieldName $type ${scalarValue()}")
|
||||
}
|
||||
}
|
||||
emitRaw(" .endstruct")
|
||||
emitRaw("")
|
||||
}
|
||||
emitRaw("")
|
||||
}
|
||||
|
||||
private fun structDefFieldAsmType(type: DataType): String = when {
|
||||
type.isBool || type.isUnsignedByte -> ".byte"
|
||||
type.isSignedByte -> ".char"
|
||||
type.isUnsignedWord || type.isPointer -> ".word"
|
||||
type.isSignedWord -> ".sint"
|
||||
type.isLong -> ".dint"
|
||||
type.isFloat -> ".byte"
|
||||
else -> ".byte"
|
||||
}
|
||||
|
||||
private fun emitFloatConstants() {
|
||||
@@ -1028,7 +1071,10 @@ internal class AsmGen(val program: IRProgram, private val target: ICompilationTa
|
||||
|
||||
// struct instances without init values -> BSS (zeroed at startup)
|
||||
val allInstances = program.st.allStructInstances().toList()
|
||||
val (instancesNoInit, instancesWithInit) = allInstances.partition { it.values.isEmpty() }
|
||||
val (instancesNoInit, instancesWithInit) = allInstances.partition {
|
||||
val def = program.st.lookup(it.structName) as IRStStructDef
|
||||
it.values.isEmpty() || def.isUnion
|
||||
}
|
||||
if (instancesNoInit.isNotEmpty()) {
|
||||
emitRaw(" .section BSS")
|
||||
emitRaw("prog8_struct_instances_bss .block")
|
||||
@@ -1053,13 +1099,17 @@ internal class AsmGen(val program: IRProgram, private val target: ICompilationTa
|
||||
for (fieldValue in si.values) {
|
||||
when (val fv = fieldValue.value) {
|
||||
is IRStSymbolicReference.Numeric -> {
|
||||
val v = fv.value.toInt()
|
||||
val size = when (fieldValue.dt) {
|
||||
BaseDataType.UBYTE, BaseDataType.BYTE, BaseDataType.BOOL -> ".byte"
|
||||
BaseDataType.UWORD, BaseDataType.WORD -> ".word"
|
||||
else -> ".word"
|
||||
// FLOAT is 5 MFLPT bytes (6502), LONG is 4 bytes; both would
|
||||
// be truncated if emitted as a 2-byte .word, corrupting the
|
||||
// instance layout.
|
||||
when (fieldValue.dt) {
|
||||
BaseDataType.FLOAT -> emitLine(" .byte [${target.getFloatAsmBytes(fv.value)}] ; float ${fv.value}")
|
||||
BaseDataType.LONG -> emitLine(" .dint ${fv.value.toLong()}")
|
||||
BaseDataType.BYTE -> emitLine(" .char ${fv.value.toInt()}")
|
||||
BaseDataType.UBYTE, BaseDataType.BOOL -> emitLine(" .byte ${fv.value.toInt()}")
|
||||
BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.STR, BaseDataType.POINTER -> emitLine(" .word ${fv.value.toInt()}")
|
||||
else -> error("unsupported datatype for struct instance field value: ${fieldValue.dt}")
|
||||
}
|
||||
emitLine(" $size $v")
|
||||
}
|
||||
is IRStSymbolicReference.Symbol -> {
|
||||
emitLine(" .word ${fixNameSymbols(fv.name)}")
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
package prog8.codegen.new6502
|
||||
|
||||
import prog8.code.core.AssemblyError
|
||||
import prog8.code.cpu6502.byteShiftAddExpansion
|
||||
import prog8.code.cpu6502.wordShiftAddExpansion
|
||||
import prog8.intermediate.*
|
||||
|
||||
internal fun AsmGen.translateArithmetic(insn: IRInstruction) {
|
||||
@@ -722,20 +724,42 @@ internal fun AsmGen.mulImmediate(dstReg: Int, value: Int, type: IRDataType) {
|
||||
when (type) {
|
||||
IRDataType.BYTE -> {
|
||||
emitLine("lda ${regAddrLo(dstReg)}")
|
||||
emitLine("ldy #${value and 0xff}")
|
||||
emitLine("jsr prog8_math.multiply_bytes")
|
||||
// small constant: expand inline as a Horner shift-add instead of calling the ~130
|
||||
// cycle multiply_bytes. Shared sequence with the classic 6502 backend.
|
||||
val expansion = byteShiftAddExpansion(value)
|
||||
if (expansion != null) {
|
||||
expansion.forEach { emitLine(it) }
|
||||
} else {
|
||||
emitLine("ldy #${value and 0xff}")
|
||||
emitLine("jsr prog8_math.multiply_bytes")
|
||||
}
|
||||
emitLine("sta ${regAddrLo(dstReg)}")
|
||||
}
|
||||
IRDataType.WORD -> {
|
||||
emitLine("lda #<${value and 0xffff}")
|
||||
emitLine("sta prog8_math.multiply_words.multiplier")
|
||||
emitLine("lda #>${value and 0xffff}")
|
||||
emitLine("sta prog8_math.multiply_words.multiplier+1")
|
||||
emitLine("lda ${regAddrLo(dstReg)}")
|
||||
emitLine("ldy ${regAddrHi(dstReg)}")
|
||||
emitLine("jsr prog8_math.multiply_words")
|
||||
emitLine("sta ${regAddrLo(dstReg)}")
|
||||
emitLine("sty ${regAddrHi(dstReg)}")
|
||||
val expansion = wordShiftAddExpansion(value)
|
||||
if (expansion != null) {
|
||||
// set up the shared sequence's contract: A = r.lo, W1 = r, W2 = x
|
||||
emitLine("lda ${regAddrLo(dstReg)}")
|
||||
emitLine("sta P8ZP_SCRATCH_W2")
|
||||
emitLine("sta P8ZP_SCRATCH_W1")
|
||||
emitLine("lda ${regAddrHi(dstReg)}")
|
||||
emitLine("sta P8ZP_SCRATCH_W2+1")
|
||||
emitLine("sta P8ZP_SCRATCH_W1+1")
|
||||
emitLine("lda P8ZP_SCRATCH_W1")
|
||||
expansion.forEach { emitLine(it) }
|
||||
emitLine("sta ${regAddrLo(dstReg)}")
|
||||
emitLine("sty ${regAddrHi(dstReg)}")
|
||||
} else {
|
||||
emitLine("lda #<${value and 0xffff}")
|
||||
emitLine("sta prog8_math.multiply_words.multiplier")
|
||||
emitLine("lda #>${value and 0xffff}")
|
||||
emitLine("sta prog8_math.multiply_words.multiplier+1")
|
||||
emitLine("lda ${regAddrLo(dstReg)}")
|
||||
emitLine("ldy ${regAddrHi(dstReg)}")
|
||||
emitLine("jsr prog8_math.multiply_words")
|
||||
emitLine("sta ${regAddrLo(dstReg)}")
|
||||
emitLine("sty ${regAddrHi(dstReg)}")
|
||||
}
|
||||
}
|
||||
IRDataType.LONG -> {
|
||||
// multiplicand (dstReg) -> cx16.r12/r13, immediate multiplier -> cx16.r14/r15
|
||||
@@ -1535,6 +1559,41 @@ internal fun AsmGen.translateFloatArithmetic(insn: IRInstruction) {
|
||||
emitLine("jsr floats.MOVMF")
|
||||
}
|
||||
|
||||
Opcode.ADDIM -> {
|
||||
val value = insn.requireImmediateFloat()
|
||||
val constLabel = getFloatConstLabel(value)
|
||||
val target = resolveAddress(insn.requireMemory())
|
||||
emitLine("lda #<$target")
|
||||
emitLine("ldy #>$target")
|
||||
emitLine("jsr floats.MOVFM")
|
||||
emitLine("lda #<$constLabel")
|
||||
emitLine("ldy #>$constLabel")
|
||||
emitLine("jsr floats.FADD")
|
||||
emitLine("ldx #<$target")
|
||||
emitLine("ldy #>$target")
|
||||
emitLine("jsr floats.MOVMF")
|
||||
}
|
||||
|
||||
Opcode.SUBIM -> {
|
||||
// NOTE: FSUB does FAC1 = memory - FAC1, so we must use push/pop + FSUBT instead
|
||||
val value = insn.requireImmediateFloat()
|
||||
val constLabel = getFloatConstLabel(value)
|
||||
val target = resolveAddress(insn.requireMemory())
|
||||
emitLine("lda #<$target")
|
||||
emitLine("ldy #>$target")
|
||||
emitLine("jsr floats.MOVFM")
|
||||
emitLine("jsr floats.pushFAC1")
|
||||
emitLine("lda #<$constLabel")
|
||||
emitLine("ldy #>$constLabel")
|
||||
emitLine("jsr floats.MOVFM")
|
||||
emitLine("sec")
|
||||
emitLine("jsr floats.popFAC")
|
||||
emitLine("jsr floats.FSUBT")
|
||||
emitLine("ldx #<$target")
|
||||
emitLine("ldy #>$target")
|
||||
emitLine("jsr floats.MOVMF")
|
||||
}
|
||||
|
||||
Opcode.INCM -> {
|
||||
val target = resolveAddress(insn.requireMemory())
|
||||
emitLine("lda #<$target")
|
||||
|
||||
@@ -494,7 +494,7 @@ internal val CallResult.hardwareSlot: CallingConventionSlot?
|
||||
/**
|
||||
* The slot-based arguments, in the order they must be loaded to avoid register clobbering.
|
||||
* Order matches the old 6502 codegen: paired regs first (AX/AY/XY), then single regs (Y, X, A),
|
||||
* then float regs, then status flags. This ensures that loading a paired register
|
||||
* then float regs, then status flags. That way, loading a paired register
|
||||
* (which clobbers two hardware regs) doesn't overwrite a value needed for a later argument.
|
||||
* Within each group the original argument order is kept.
|
||||
*/
|
||||
@@ -554,8 +554,8 @@ private fun AsmGen.translateCall(fnLabel: String, site: CallSite) {
|
||||
// the IR generates LOADHR for them.
|
||||
// In single-return expression context, process slot returns normally
|
||||
// (the IR doesn't generate LOADHR for single-return calls).
|
||||
// LIMITATION: multiple status flag returns in one multi-assign (e.g. -> bool @Pz, bool @Pc)
|
||||
// are not supported - codegen limitation: the first flag's extraction clobbers the state for subsequent flags.
|
||||
// Multiple status flag returns in one multi-assign are protected by the IR:
|
||||
// it wraps each flag's extraction in PUSHST/POPST (php/plp) so they don't clobber each other.
|
||||
val isMultiReturn = site.results.size > 1
|
||||
for (ret in site.results) {
|
||||
if (ret.location is CallLocation.StatusFlag)
|
||||
@@ -670,6 +670,8 @@ private fun AsmGen.translateArgument(arg: CallArgument, argIndex: Int = -1, fnLa
|
||||
// Syscall argument - value is already in the register file,
|
||||
// the syscall handler reads it from there.
|
||||
}
|
||||
is CallLocation.FrameSlot ->
|
||||
error("stack frame argument slots are only supported on m68k targets: $location")
|
||||
is CallLocation.HardwareRegister -> throw IllegalStateException("slot already handled")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1104,8 +1104,9 @@ internal fun AsmGen.copyRegisterLong(dstReg: Int, srcReg: Int) {
|
||||
emitLine("bpl -")
|
||||
}
|
||||
|
||||
/** the address of a [base] (+ [displacement], for a Symbol base only - matches the previous behavior
|
||||
* of ignoring any offset that came together with a fixed numeric address) */
|
||||
/** the address of a [base], plus [displacement]. The displacement is only added
|
||||
* for a Symbol base; this matches the previous behavior of ignoring any offset
|
||||
* that came together with a fixed numeric address. */
|
||||
internal fun AsmGen.resolveAddress(base: AddressBase, displacement: Int = 0): String {
|
||||
return when (base) {
|
||||
is AddressBase.Symbol -> {
|
||||
@@ -1113,6 +1114,7 @@ internal fun AsmGen.resolveAddress(base: AddressBase, displacement: Int = 0): St
|
||||
if (displacement != 0) "$resolved+$displacement" else resolved
|
||||
}
|
||||
is AddressBase.Absolute -> base.address.value.toHex()
|
||||
is AddressBase.FrameSlot -> error("stack frame slots are only supported on m68k targets, got $base")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,6 @@ class New6502CodeGenerator(val retainSSA: Boolean,
|
||||
if(!success)
|
||||
return null
|
||||
|
||||
return AssemblyProgram6502(irProgram.name, irProgram.options.outputDir, irProgram.options.compTarget)
|
||||
return AssemblyProgram6502(irProgram.name, irProgram.options, irProgram.options.compTarget)
|
||||
}
|
||||
}
|
||||
@@ -1013,11 +1013,12 @@ class ExpressionSimplifier(private val program: Program, private val errors: IEr
|
||||
}
|
||||
in powersOfTwoFloat -> {
|
||||
val numshifts = powersOfTwoFloat.indexOf(cv)
|
||||
if (leftDt.isInteger) {
|
||||
// division by a power of two => logical shift right
|
||||
// NOTE: technically this optimization is only 100% accurate on unsigned integers
|
||||
// whereas shifthing signed integers can result in an off-by-1 rounding difference
|
||||
// (If you change the guard to only trigger this for unsigned integers the codegen will fall back to using division, which is slow but 100% accurate)
|
||||
if (leftDt.isUnsignedInteger) {
|
||||
// unsigned division by a power of two => logical shift right (exact)
|
||||
// Signed integers must NOT be folded here: the shift floors toward -inf, while
|
||||
// Prog8's signed '/' truncates toward zero, so x / 2^n would be off by one for
|
||||
// every negative dividend that is not an exact multiple. Leaving the division in
|
||||
// place lets the codegen emit a correct (slower) signed DIVS instead.
|
||||
return BinaryExpression(expr.left, ">>", NumericLiteral.optimalInteger(numshifts, expr.position), expr.position)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,11 +57,11 @@ fun Program.optimizeStatements(errors: IErrorReporter,
|
||||
return optimizationCount
|
||||
}
|
||||
|
||||
fun Program.inlineSubroutines(options: CompilationOptions): Int {
|
||||
fun Program.inlineSubroutines(errors: IErrorReporter, options: CompilationOptions): Int {
|
||||
// Skip inlining when optimizations are disabled (-noopt flag)
|
||||
if (!options.optimize) return 0
|
||||
|
||||
val inliner = Inliner(this, options)
|
||||
val inliner = Inliner(this, options, errors)
|
||||
inliner.visit(this)
|
||||
val mods = inliner.applyModifications()
|
||||
inliner.linkAffectedParents(namespace)
|
||||
|
||||
@@ -5,6 +5,7 @@ import prog8.ast.expressions.*
|
||||
import prog8.ast.statements.*
|
||||
import prog8.ast.walk.*
|
||||
import prog8.code.core.CompilationOptions
|
||||
import prog8.code.core.IErrorReporter
|
||||
import prog8.code.core.InternalCompilerException
|
||||
|
||||
|
||||
@@ -27,11 +28,13 @@ private class CannotInlineException : Exception()
|
||||
* - Subroutines must have a single functional statement in their body to be auto-inlined.
|
||||
* - Multi-return value subroutines are not yet supported for auto-inlining.
|
||||
*/
|
||||
class Inliner(private val program: Program, private val options: CompilationOptions): AstWalker() {
|
||||
class Inliner(private val program: Program, private val options: CompilationOptions, private val errors: IErrorReporter): AstWalker() {
|
||||
|
||||
private val suggestInlineReported = mutableSetOf<Subroutine>()
|
||||
|
||||
inner class DetermineInlineSubs(val program: Program): IAstVisitor {
|
||||
private val modifications = mutableListOf<AstModification>()
|
||||
private val visitedSubroutines = java.util.Collections.newSetFromMap(java.util.IdentityHashMap<Subroutine, Boolean>())
|
||||
private val visitedSubroutines = mutableSetOf<Subroutine>()
|
||||
|
||||
init {
|
||||
visit(program)
|
||||
@@ -238,8 +241,11 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
override fun after(functionCallStatement: FunctionCallStatement, parent: Node): Iterable<AstModification> {
|
||||
val sub = functionCallStatement.target.targetStatement(program.builtinFunctions) as? Subroutine
|
||||
if (sub == null) return noModifications
|
||||
|
||||
if (!sub.inline) return noModifications
|
||||
|
||||
if (!sub.inline) {
|
||||
maybeSuggestInlineKeyword(sub)
|
||||
return noModifications
|
||||
}
|
||||
|
||||
val (canInline, _) = canInlineAtCallSiteWithReason(sub, functionCallStatement)
|
||||
if (!canInline) {
|
||||
@@ -258,15 +264,43 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
return false to "asmsub with parameters cannot be inlined"
|
||||
}
|
||||
|
||||
if (sub.parameters.size > 1) {
|
||||
return false to "subroutines with more than 1 parameter cannot be inlined in this compiler version"
|
||||
}
|
||||
|
||||
if (sub.parameters.any { it.registerOrPair != null }) {
|
||||
return false to "subroutines with register-mapped parameters cannot be inlined"
|
||||
}
|
||||
|
||||
if (sub.parameters.size == 1) {
|
||||
if (sub.parameters.size > 1) {
|
||||
// Multi-parameter inlining is only supported on m68k targets (stack-based
|
||||
// calling convention makes the call overhead worthwhile), and only for the
|
||||
// single-return-value expression case with a tiny body and simple args.
|
||||
if (!options.compTarget.cpu.is68k) {
|
||||
return false to "subroutines with more than 1 parameter cannot be inlined in this compiler version"
|
||||
}
|
||||
if (fcall.args.size != sub.parameters.size) {
|
||||
return false to "argument count does not match parameter count"
|
||||
}
|
||||
if (!fcall.args.all { it.isSimple }) {
|
||||
return false to "argument is too complex to inline (might have side effects)"
|
||||
}
|
||||
// Refuse to inline a sub that assigns directly to any of its parameters.
|
||||
// Substituting the argument into the target would crash for a literal
|
||||
// argument, or wrongly modify the caller's variable for a variable argument
|
||||
// (parameters are passed by value, so the sub only modifies its local copy).
|
||||
val paramNames = sub.parameters.map { it.name }.toSet()
|
||||
val bodyStmt = sub.statements.firstOrNull { it !is VarDecl || it.origin != VarDeclOrigin.SUBROUTINEPARAM }
|
||||
if ((bodyStmt as? Assignment)?.target?.identifier?.nameInSource?.lastOrNull() in paramNames)
|
||||
return false to "cannot inline subroutine that assigns to its parameter"
|
||||
// v1 only supports the single-return-value expression case on this path.
|
||||
// Void and multi-value statement call sites keep the previous restriction.
|
||||
val returnStmt = bodyStmt as? Return
|
||||
?: return false to "multi-parameter inlining requires a single return statement"
|
||||
if (returnStmt.values.size != 1) {
|
||||
return false to "multi-parameter inlining requires a single return value"
|
||||
}
|
||||
if (!isFairlySimpleReturnExpression(returnStmt.values[0], paramNames)) {
|
||||
return false to "multi-parameter inline body is too complex"
|
||||
}
|
||||
// fall through to the 6502-independent checks below (none apply to m68k)
|
||||
} else if (sub.parameters.size == 1) {
|
||||
val arg = fcall.args.first()
|
||||
if (!arg.isSimple) {
|
||||
return false to "argument is too complex to inline (might have side effects)"
|
||||
@@ -298,10 +332,52 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
return canInlineAtCallSiteWithReason(sub, fcall).first
|
||||
}
|
||||
|
||||
private fun fitsM68kMultiParamInline(sub: Subroutine): Boolean {
|
||||
if (!options.compTarget.cpu.is68k)
|
||||
return false
|
||||
if (sub.inline)
|
||||
return false
|
||||
if (sub.isAsmSubroutine && sub.parameters.isNotEmpty())
|
||||
return false
|
||||
if (sub.parameters.size <= 1)
|
||||
return false
|
||||
if (sub.parameters.any { it.registerOrPair != null })
|
||||
return false
|
||||
if (sub === program.entrypoint)
|
||||
return false
|
||||
if (sub.statements.any { it is Subroutine || (it is VarDecl && it.origin != VarDeclOrigin.SUBROUTINEPARAM) })
|
||||
return false
|
||||
val functionalCount = sub.statements.count { it !is VarDecl || it.origin != VarDeclOrigin.SUBROUTINEPARAM }
|
||||
if (functionalCount != 1 && functionalCount != 2)
|
||||
return false
|
||||
val bodyStmt = sub.statements.firstOrNull { it !is VarDecl || it.origin != VarDeclOrigin.SUBROUTINEPARAM }
|
||||
val returnStmt = bodyStmt as? Return ?: return false
|
||||
if (returnStmt.values.size != 1)
|
||||
return false
|
||||
if (functionalCount == 2 && sub.statements.lastOrNull()?.let(::isEmptyReturn) != true)
|
||||
return false
|
||||
val paramNames = sub.parameters.map { it.name }.toSet()
|
||||
if (!isFairlySimpleReturnExpression(returnStmt.values[0], paramNames))
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
private fun maybeSuggestInlineKeyword(sub: Subroutine) {
|
||||
if (sub in suggestInlineReported)
|
||||
return
|
||||
if (!fitsM68kMultiParamInline(sub))
|
||||
return
|
||||
suggestInlineReported.add(sub)
|
||||
errors.info("subroutine '${sub.name}' would likely benefit from 'inline'", sub.position)
|
||||
}
|
||||
|
||||
override fun before(functionCallExpr: FunctionCallExpression, parent: Node): Iterable<AstModification> {
|
||||
val sub = functionCallExpr.target.targetStatement(program.builtinFunctions) as? Subroutine
|
||||
if (sub == null) return noModifications
|
||||
if (!sub.inline) return noModifications
|
||||
if (!sub.inline) {
|
||||
maybeSuggestInlineKeyword(sub)
|
||||
return noModifications
|
||||
}
|
||||
|
||||
val (canInline, _) = canInlineAtCallSiteWithReason(sub, functionCallExpr)
|
||||
if (!canInline) {
|
||||
@@ -422,6 +498,78 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* m68k-only predicate for multi-parameter inlining (v1: expression case only).
|
||||
* Allows any number of parameters but bounds the duplicated work instead:
|
||||
* single return expression, expression-tree depth <= MAX_DEPTH, total nodes
|
||||
* <= MAX_NODES, and total parameter occurrences <= MAX_PARAM_USES.
|
||||
* Numeric literals and constant identifiers count as free (cost 0).
|
||||
*/
|
||||
private fun isFairlySimpleReturnExpression(expr: Expression, paramNames: Set<String>): Boolean {
|
||||
val maxDepth = 3
|
||||
val maxNodes = 12
|
||||
val maxParamUses = 6
|
||||
|
||||
var nodes = 0
|
||||
var paramUses = 0
|
||||
var ok = true
|
||||
|
||||
fun isFreeLeaf(ident: IdentifierReference): Boolean {
|
||||
// Constants count as free (usually already constant-folded to literals).
|
||||
if (ident.constValue(program) != null)
|
||||
return true
|
||||
val target = try {
|
||||
ident.targetStatement()
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
return target is VarDecl && target.type == VarDeclType.CONST
|
||||
}
|
||||
|
||||
fun visit(e: Expression, depth: Int) {
|
||||
if (!ok)
|
||||
return
|
||||
if (depth > maxDepth) {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
when (e) {
|
||||
is NumericLiteral -> {
|
||||
// free, costs no node budget
|
||||
}
|
||||
is IdentifierReference -> {
|
||||
if (!isFreeLeaf(e)) {
|
||||
nodes++
|
||||
if (nodes > maxNodes) {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
if (e.nameInSource.lastOrNull() in paramNames) {
|
||||
paramUses++
|
||||
if (paramUses > maxParamUses)
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
}
|
||||
is PrefixExpression -> visit(e.expression, depth + 1)
|
||||
is BinaryExpression -> {
|
||||
nodes++
|
||||
if (nodes > maxNodes) {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
visit(e.left, depth + 1)
|
||||
visit(e.right, depth + 1)
|
||||
}
|
||||
is TypecastExpression -> visit(e.expression, depth + 1)
|
||||
else -> ok = false
|
||||
}
|
||||
}
|
||||
|
||||
visit(expr, 0)
|
||||
return ok
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitutes parameter references in a statement with argument values.
|
||||
*/
|
||||
@@ -434,7 +582,11 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
?: return noModifications
|
||||
|
||||
// Only handle inlining if sub is marked as such and call site is okay
|
||||
if (!sub.inline || !canInlineAtCallSite(sub, fcall))
|
||||
if (!sub.inline) {
|
||||
maybeSuggestInlineKeyword(sub)
|
||||
return noModifications
|
||||
}
|
||||
if (!canInlineAtCallSite(sub, fcall))
|
||||
return noModifications
|
||||
|
||||
val toInline = sub.statements.firstOrNull { it !is VarDecl || it.origin != VarDeclOrigin.SUBROUTINEPARAM } as? Return ?: return noModifications
|
||||
@@ -477,6 +629,8 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
}
|
||||
|
||||
private fun substituteParameters(sub: Subroutine, fcall: IFunctionCall, node: Node): Node? {
|
||||
if (fcall.args.size != sub.parameters.size)
|
||||
return null
|
||||
val paramVarDecls = sub.parameters.map { param ->
|
||||
sub.statements.filterIsInstance<VarDecl>().find { it.origin == VarDeclOrigin.SUBROUTINEPARAM && it.name == param.name }
|
||||
?: throw FatalAstException("parameter ${param.name} not found in subroutine ${sub.name}")
|
||||
|
||||
@@ -486,15 +486,19 @@ class StatementOptimizer(private val program: Program,
|
||||
return listOf(AstReplaceNode(assignment, ifstmt, parent))
|
||||
}
|
||||
|
||||
if(v2 is NumericLiteral || v2 is IdentifierReference) {
|
||||
if(v2 is NumericLiteral) {
|
||||
// x = min(expression, 100) -> x=expression, if x>100 x=100
|
||||
// Only safe with a real constant: the assignment to x happens before the
|
||||
// comparison, so comparing against anything that x could alias (such as a
|
||||
// variable that happens to be the assignment target) would read the already
|
||||
// overwritten value.
|
||||
val assign = Assignment(assignment.target.copy(), v1, AssignmentOrigin.OPTIMIZER, assignment.position)
|
||||
val ifstmt = makeMinMaxCheckAndAssignRight(assign.target.toExpression(), ">", v2, assignment.target, assignment.position)
|
||||
return listOf(
|
||||
AstInsert.after(assignment, ifstmt, parent as IStatementContainer),
|
||||
AstReplaceNode(assignment, assign, parent)
|
||||
)
|
||||
} else if(v1 is NumericLiteral || v1 is IdentifierReference) {
|
||||
} else if(v1 is NumericLiteral) {
|
||||
// x = min(100, expression) -> x=expression, if x>100 x=100
|
||||
val assign = Assignment(assignment.target.copy(), v2, AssignmentOrigin.OPTIMIZER, assignment.position)
|
||||
val ifstmt = makeMinMaxCheckAndAssignRight(assign.target.toExpression(), ">", v1, assignment.target, assignment.position)
|
||||
@@ -519,15 +523,17 @@ class StatementOptimizer(private val program: Program,
|
||||
return listOf(AstReplaceNode(assignment, ifstmt, parent))
|
||||
}
|
||||
|
||||
if(v2 is NumericLiteral || v2 is IdentifierReference) {
|
||||
if(v2 is NumericLiteral) {
|
||||
// x = max(expression, 100) -> x=expression, if x<100 x=100
|
||||
// See the note in the min__ case above: the compared operand must be a
|
||||
// real constant, never a variable that the assignment could have aliased.
|
||||
val assign = Assignment(assignment.target.copy(), v1, AssignmentOrigin.OPTIMIZER, assignment.position)
|
||||
val ifstmt = makeMinMaxCheckAndAssignRight(assign.target.toExpression(), "<", v2, assignment.target, assignment.position)
|
||||
return listOf(
|
||||
AstInsert.after(assignment, ifstmt, parent as IStatementContainer),
|
||||
AstReplaceNode(assignment, assign, parent)
|
||||
)
|
||||
} else if(v1 is NumericLiteral || v1 is IdentifierReference) {
|
||||
} else if(v1 is NumericLiteral) {
|
||||
// x = max(100, expression) -> x=expression, if x<100 x=100
|
||||
val assign = Assignment(assignment.target.copy(), v2, AssignmentOrigin.OPTIMIZER, assignment.position)
|
||||
val ifstmt = makeMinMaxCheckAndAssignRight(assign.target.toExpression(), "<", v1, assignment.target, assignment.position)
|
||||
|
||||
@@ -32,20 +32,66 @@ configurations.all {
|
||||
exclude(group = "org.antlr", module = "antlr4")
|
||||
}
|
||||
|
||||
// Post-process the generated launcher scripts: pass --enable-native-access only to
|
||||
// JVMs that understand it (JDK 18+), to avoid JNA restricted-method warnings on newer JDKs.
|
||||
fun patchLaunchScripts(scriptDir: File) {
|
||||
val unixScript = scriptDir.resolve("prog8c")
|
||||
if (unixScript.canRead()) {
|
||||
val marker = "eval \"set -- $("
|
||||
val text = unixScript.readText()
|
||||
check(marker in text) { "unexpected POSIX launch script layout for prog8c" }
|
||||
val probe = $$"""if "$JAVACMD" --enable-native-access=ALL-UNNAMED -version >/dev/null 2>&1; then
|
||||
JAVA_OPTS="$JAVA_OPTS --enable-native-access=ALL-UNNAMED"
|
||||
fi
|
||||
|
||||
"""
|
||||
unixScript.writeText(text.replace(marker, probe + marker))
|
||||
}
|
||||
val windowsScript = scriptDir.resolve("prog8c.bat")
|
||||
if (windowsScript.canRead()) {
|
||||
val marker = "@rem Execute prog8c"
|
||||
val text = windowsScript.readText()
|
||||
check(marker in text) { "unexpected Windows launch script layout for prog8c" }
|
||||
val probe = $$"""@rem Prog8: add --enable-native-access on JVMs that support it (JDK 18+) to avoid JNA restricted-method warnings
|
||||
"%JAVA_EXE%" --enable-native-access=ALL-UNNAMED -version >NUL 2>&1
|
||||
if not errorlevel 1 set JAVA_OPTS=%JAVA_OPTS% --enable-native-access=ALL-UNNAMED
|
||||
|
||||
"""
|
||||
windowsScript.writeText(text.replace(marker, probe + marker))
|
||||
}
|
||||
}
|
||||
|
||||
tasks.startScripts {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
tasks.withType<org.gradle.jvm.application.tasks.CreateStartScripts>().configureEach {
|
||||
doLast {
|
||||
patchLaunchScripts(outputDir!!)
|
||||
}
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass.set("prog8.CompilerMainKt")
|
||||
applicationName = "prog8c"
|
||||
}
|
||||
|
||||
tasks.jar {
|
||||
manifest {
|
||||
attributes("Main-Class" to "prog8.CompilerMainKt")
|
||||
attributes("Enable-Native-Access" to "ALL-UNNAMED")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.shadowJar {
|
||||
archiveBaseName.set("prog8c")
|
||||
archiveVersion.set(version.toString())
|
||||
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||
mergeServiceFiles()
|
||||
manifest {
|
||||
attributes("Main-Class" to "prog8.CompilerMainKt")
|
||||
attributes("Enable-Native-Access" to "ALL-UNNAMED")
|
||||
}
|
||||
filesMatching("META-INF/LICENSE") {
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
%import shared_m68k_adpcm
|
||||
|
||||
adpcm {
|
||||
%option ignore_unused, private_symbols
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
%import exec
|
||||
|
||||
ahi {
|
||||
%option no_symbol_prefixing, ignore_unused
|
||||
|
||||
; Playback-only binding for ahi.device, high-level (CMD_WRITE) API.
|
||||
; AHI v4 compatible: ahir_Version is 4 and only 8/16-bit sample types
|
||||
; are exposed (32-bit types need version 6+).
|
||||
; The unit to play on is selectable via ahi.open() (pass 0 = AHI_DEFAULT_UNIT
|
||||
; for the user's default output, or 1-3 for another configured unit; AHI_NO_UNIT
|
||||
; only makes sense for low-level-only access and is not supported here).
|
||||
; The low-level control and audio-mode query LVOs (audio channel handles)
|
||||
; are exposed as extsubs below; their hooks and requester are deliberately
|
||||
; not covered.
|
||||
; Requires a system with ahi.device installed (real hardware or full
|
||||
; emulation); not testable under vamos. Amiga1200 target only (68020+,
|
||||
; Kickstart 3+): uses CreateMsgPort/CreateIORequest for port handling.
|
||||
; Struct offsets verified against NDK3.2 exec/io.h (IOStdReq) and the
|
||||
; AHI v6 dev kit (Developer/Include/Asm/devices/ahi.i, AHIRequest).
|
||||
|
||||
; ---- constants ----
|
||||
|
||||
const uword AHI_VERSION = 4 ; ahir_Version preset before OpenDevice
|
||||
|
||||
const ubyte AHI_DEFAULT_UNIT = 0
|
||||
const ubyte AHI_NO_UNIT = 255 ; low-level API only, no hardware allocated
|
||||
|
||||
const long AHI_INVALID_ID = $ffffffff ; audio id sentinel, not a valid mode
|
||||
const long AHI_DEFAULT_ID = 0 ; AllocAudioA: use the default mode
|
||||
|
||||
; sample formats for ahir_Type (all V4-compatible)
|
||||
const long AHIST_M8S = 0 ; mono, 8-bit signed
|
||||
const long AHIST_M16S = 1 ; mono, 16-bit signed (big-endian WORDs)
|
||||
const long AHIST_S8S = 2 ; stereo, 8-bit signed
|
||||
const long AHIST_S16S = 3 ; stereo, 16-bit signed (big-endian WORDs)
|
||||
|
||||
; volume and stereo position are 16.16 Fixed
|
||||
const long FULL_VOLUME = $10000
|
||||
const long PAN_LEFT = 0
|
||||
const long PAN_CENTER = $8000
|
||||
const long PAN_RIGHT = $10000
|
||||
|
||||
; error codes (AHIE_*, seen in io_Error alongside exec IO errors)
|
||||
const byte AHIE_OK = 0
|
||||
const byte AHIE_NOMEM = 1
|
||||
const byte AHIE_BADSOUNDTYPE = 2
|
||||
const byte AHIE_BADSAMPLETYPE = 3
|
||||
const byte AHIE_ABORTED = 4
|
||||
const byte AHIE_UNKNOWN = 5
|
||||
const byte AHIE_HALFDUPLEX = 6
|
||||
|
||||
; ---- low-level LVO API ----
|
||||
; These call the device's library-style routines through its base
|
||||
; (sys.AHIBase, @bank 101), which ahi.open() sets from the opened
|
||||
; device's io_Device. Register assignments and offsets verified against
|
||||
; the AHI dev kit ahi_lib.fd/sfd (bias 42). The synth routines need an
|
||||
; AudioCtrl handle from ahi.AllocAudioA(); the query trio reads the
|
||||
; audio-mode database, useful for diagnostics. Taglists are plain
|
||||
; 'pointer' parameters (like the intuition bindings): build them as
|
||||
; ^^utility.TagItem arrays of {tag, data} pairs, terminated by
|
||||
; utility.TAG_END. Data is a pointer-or-value per the tag's semantics.
|
||||
|
||||
extsub @bank 101 -42 = AllocAudioA(pointer tagList @A1) -> pointer @D0 ; returns AudioCtrl, or 0 (AF_NULL) if failed
|
||||
extsub @bank 101 -48 = FreeAudio(pointer audioCtrl @A2)
|
||||
extsub @bank 101 -60 = ControlAudioA(pointer audioCtrl @A2, pointer tagList @A1) -> long @D0
|
||||
extsub @bank 101 -66 = SetVol(uword channel @D0, long volume @D1, long pan @D2, pointer audioCtrl @A2, long flags @D3)
|
||||
extsub @bank 101 -72 = SetFreq(uword channel @D0, long freq @D1, pointer audioCtrl @A2, long flags @D2)
|
||||
extsub @bank 101 -78 = SetSound(uword channel @D0, uword sound @D1, long offset @D2, long length @D3, pointer audioCtrl @A2, long flags @D4)
|
||||
extsub @bank 101 -90 = LoadSound(uword sound @D0, long sampletype @D1, pointer info @A0, pointer audioCtrl @A2) -> long @D0
|
||||
extsub @bank 101 -96 = UnloadSound(uword sound @D0, pointer audioCtrl @A2)
|
||||
extsub @bank 101 -102 = NextAudioID(long lastID @D0) -> long @D0 ; next id, or AHI_INVALID_ID at the end
|
||||
extsub @bank 101 -108 = GetAudioAttrsA(long id @D0, pointer audioCtrl @A2, pointer tagList @A1) -> long @D0 ; 1=ok, 0=invalid id
|
||||
extsub @bank 101 -114 = BestAudioIDA(pointer tagList @A1) -> long @D0 ; best matching audio id, AHI_INVALID_ID if none
|
||||
|
||||
; For ahi.BestAudioIDA(): hard requirements go in the main taglist, and
|
||||
; AHIB_Dizzy points at a second taglist of soft preferences - after the
|
||||
; hard ones are applied, the mode with the most of the wanted (and fewest
|
||||
; of the unwanted) features wins. Without it the call hardly does what its
|
||||
; name suggests, so it is worth passing.
|
||||
const long AHIB_Dizzy = $800000be
|
||||
|
||||
; ---- low-level tag constants ----
|
||||
; AHIC_* go to ahi.ControlAudioA(), AHIDB_* to ahi.GetAudioAttrsA(),
|
||||
; AHIB_* to ahi.BestAudioIDA().
|
||||
; Boolean AHIDB_* tags return 0/1; the R-bit string tags (driver, name,
|
||||
; author, copyright, version, annotation) are OUT parameters: the device
|
||||
; writes a string into the buffer you pass (size it via AHIDB_BufferLen).
|
||||
; Tag base is utility.TAG_USER.
|
||||
const long AHIC_Play = $80000050
|
||||
const long AHIC_Record = $80000051
|
||||
const long AHIC_MonitorVolume = $80000052
|
||||
const long AHIC_MonitorVolume_Query = $80000053
|
||||
const long AHIC_MixFreq_Query = $80000054
|
||||
const long AHIC_InputGain = $80000055
|
||||
const long AHIC_InputGain_Query = $80000056
|
||||
const long AHIC_OutputVolume = $80000057
|
||||
const long AHIC_OutputVolume_Query = $80000058
|
||||
const long AHIC_Input = $80000059
|
||||
const long AHIC_Input_Query = $8000005a
|
||||
const long AHIC_Output = $8000005b
|
||||
const long AHIC_Output_Query = $8000005c
|
||||
|
||||
const long AHIDB_AudioID = $80000064
|
||||
const long AHIDB_Driver = $80008065 ; R
|
||||
const long AHIDB_Volume = $80000067
|
||||
const long AHIDB_Panning = $80000068
|
||||
const long AHIDB_Stereo = $80000069
|
||||
const long AHIDB_HiFi = $8000006a
|
||||
const long AHIDB_PingPong = $8000006b
|
||||
const long AHIDB_Name = $8000806d ; R
|
||||
const long AHIDB_Bits = $8000006e
|
||||
const long AHIDB_MaxChannels = $8000006f
|
||||
const long AHIDB_MinMixFreq = $80000070
|
||||
const long AHIDB_MaxMixFreq = $80000071
|
||||
const long AHIDB_Record = $80000072
|
||||
const long AHIDB_Frequencies = $80000073
|
||||
const long AHIDB_FrequencyArg = $80000074
|
||||
const long AHIDB_Frequency = $80000075
|
||||
const long AHIDB_Author = $80008076 ; R
|
||||
const long AHIDB_Copyright = $80008077 ; R
|
||||
const long AHIDB_Version = $80008078 ; R
|
||||
const long AHIDB_Annotation = $80008079 ; R
|
||||
const long AHIDB_BufferLen = $8000007a
|
||||
const long AHIDB_IndexArg = $8000007b
|
||||
const long AHIDB_Index = $8000007c
|
||||
const long AHIDB_Realtime = $8000007d
|
||||
const long AHIDB_MaxPlaySamples = $8000007e
|
||||
const long AHIDB_MaxRecordSamples = $8000007f
|
||||
const long AHIDB_FullDuplex = $80000081
|
||||
const long AHIDB_MinMonitorVolume = $80000082
|
||||
const long AHIDB_MaxMonitorVolume = $80000083
|
||||
const long AHIDB_MinInputGain = $80000084
|
||||
const long AHIDB_MaxInputGain = $80000085
|
||||
const long AHIDB_MinOutputVolume = $80000086
|
||||
const long AHIDB_MaxOutputVolume = $80000087
|
||||
const long AHIDB_Inputs = $80000088
|
||||
const long AHIDB_InputArg = $80000089
|
||||
const long AHIDB_Input = $8000008a
|
||||
const long AHIDB_Outputs = $8000008b
|
||||
const long AHIDB_OutputArg = $8000008c
|
||||
const long AHIDB_Output = $8000008d
|
||||
|
||||
; ---- struct ----
|
||||
|
||||
struct AHIRequest { ; total size: 80
|
||||
; struct IOStdReq base (48 bytes, same layout as exec.IOStdReq)
|
||||
^^exec.Node Succ ; 0
|
||||
^^exec.Node Pred ; 4
|
||||
ubyte Type ; 8
|
||||
byte Pri ; 9
|
||||
str Name ; 10
|
||||
^^exec.MsgPort ReplyPort ; 14
|
||||
uword Length ; 18
|
||||
pointer Device ; 20
|
||||
long Unit ; 24
|
||||
uword Command ; 28
|
||||
ubyte Flags ; 30
|
||||
byte Error ; 31
|
||||
long Actual ; 32
|
||||
long IOStdReq_Length ; 36
|
||||
pointer Data ; 40
|
||||
long Offset ; 44
|
||||
; AHI extension
|
||||
uword Version ; 48
|
||||
uword Pad ; 50
|
||||
long Private1 ; 52
|
||||
long Private2 ; 56
|
||||
long SampleType ; 60
|
||||
long Frequency ; 64
|
||||
long Volume ; 68
|
||||
long Position ; 72
|
||||
pointer Link ; 76
|
||||
}
|
||||
|
||||
; Two request blocks for gapless double-buffered playback, used
|
||||
; ping-pong fashion and chained via ahir_Link. Allocated with
|
||||
; CreateIORequest (Kickstart 2.04+), sharing a single reply port.
|
||||
^^AHIRequest Req0
|
||||
^^AHIRequest Req1
|
||||
private ^^exec.MsgPort port
|
||||
private ubyte queued ; number of outstanding writes (0..2)
|
||||
private bool play_alt ; next play uses Req1
|
||||
private bool wait_alt ; next wait reaps Req1
|
||||
private bool opened
|
||||
|
||||
; ---- high level playback interface ----
|
||||
|
||||
sub open(ubyte unit) -> bool {
|
||||
; -- Open ahi.device for playback on the given unit (0 = default,
|
||||
; 1-3 other configured units; see the header note above).
|
||||
; Returns false if unavailable.
|
||||
if opened
|
||||
return true
|
||||
port = exec.CreateMsgPort()
|
||||
if port == 0
|
||||
return false
|
||||
Req0 = exec.CreateIORequest(port, sizeof(ahi.AHIRequest))
|
||||
if Req0 == 0 {
|
||||
exec.DeleteMsgPort(port)
|
||||
return false
|
||||
}
|
||||
Req0.Version = AHI_VERSION
|
||||
if exec.OpenDevice("ahi.device", unit, Req0, 0)!=0 {
|
||||
exec.DeleteIORequest(Req0)
|
||||
exec.DeleteMsgPort(port)
|
||||
return false
|
||||
}
|
||||
Req1 = exec.CreateIORequest(port, sizeof(ahi.AHIRequest))
|
||||
if Req1 == 0 {
|
||||
exec.CloseDevice(Req0)
|
||||
exec.DeleteIORequest(Req0)
|
||||
exec.DeleteMsgPort(port)
|
||||
return false
|
||||
}
|
||||
Req1.Device = Req0.Device
|
||||
Req1.Unit = Req0.Unit
|
||||
Req1.Version = AHI_VERSION
|
||||
sys.AHIBase = Req0.Device ; base for the low-level LVO calls above
|
||||
queued = 0
|
||||
play_alt = false
|
||||
wait_alt = false
|
||||
opened = true
|
||||
return true
|
||||
}
|
||||
|
||||
sub close() {
|
||||
; -- Abort pending audio, close the device, free the port and requests.
|
||||
if not opened
|
||||
return
|
||||
stop()
|
||||
exec.CloseDevice(Req0)
|
||||
exec.DeleteIORequest(Req0)
|
||||
exec.DeleteIORequest(Req1)
|
||||
exec.DeleteMsgPort(port)
|
||||
sys.AHIBase = 0
|
||||
opened = false
|
||||
}
|
||||
|
||||
sub write_sync(pointer samples, long num_bytes, long sampletype, uword frequency, long volume, long position) -> byte {
|
||||
; -- Play a sample synchronously, blocks until finished. Returns io_Error.
|
||||
while queued>0
|
||||
wait_oldest()
|
||||
num_bytes = setup(Req0, 0, samples, num_bytes, sampletype, frequency, volume, position)
|
||||
if num_bytes==0
|
||||
return AHIE_OK
|
||||
void exec.DoIO(Req0)
|
||||
return Req0.Error
|
||||
}
|
||||
|
||||
sub play(pointer samples, long num_bytes, long sampletype, uword frequency, long volume, long position) {
|
||||
; -- Play a sample asynchronously on the (mixed) output.
|
||||
; If chunks were queued earlier with play_queued, those are waited for first,
|
||||
; so this never reuses a request block that still has a write in flight.
|
||||
while queued>0
|
||||
wait_oldest()
|
||||
num_bytes = setup(Req0, 0, samples, num_bytes, sampletype, frequency, volume, position)
|
||||
if num_bytes==0
|
||||
return
|
||||
BeginIO(Req0)
|
||||
queued = 1
|
||||
play_alt = true
|
||||
wait_alt = false
|
||||
; sound now plays asynchronously.
|
||||
}
|
||||
|
||||
sub play_queued(pointer samples, long num_bytes, long sampletype, uword frequency, long volume, long position) {
|
||||
; -- Queue a sample chunk for gapless playback (ahir_Link chaining).
|
||||
; Unlike play(), this may be called while the previous chunk is still playing:
|
||||
; AHI starts it the moment the previous chunk ends.
|
||||
; If both slots are full this waits for the oldest chunk to finish first.
|
||||
if queued==2
|
||||
wait_oldest()
|
||||
^^AHIRequest io = Req0
|
||||
pointer link = 0
|
||||
if play_alt {
|
||||
io = Req1
|
||||
link = Req0
|
||||
} else {
|
||||
if queued>0
|
||||
link = Req1
|
||||
}
|
||||
num_bytes = setup(io, link, samples, num_bytes, sampletype, frequency, volume, position)
|
||||
if num_bytes==0
|
||||
return
|
||||
play_alt = not play_alt
|
||||
BeginIO(io)
|
||||
queued = queued+1
|
||||
; chunk plays (or stays queued) asynchronously.
|
||||
}
|
||||
|
||||
sub wait() {
|
||||
; -- Wait for all queued audio to finish playing.
|
||||
while queued>0
|
||||
wait_oldest()
|
||||
}
|
||||
|
||||
sub stop() {
|
||||
; -- Abort pending playback. Only our own requests are aborted;
|
||||
; CMD_FLUSH would kill other programs' audio too and must not be used here.
|
||||
while queued>0 {
|
||||
^^AHIRequest io = get_slot(wait_alt)
|
||||
exec.AbortIO(io)
|
||||
void exec.WaitIO(io)
|
||||
wait_alt = not wait_alt
|
||||
queued = queued-1
|
||||
}
|
||||
play_alt = false
|
||||
wait_alt = false
|
||||
}
|
||||
|
||||
inline sub is_playing() -> bool {
|
||||
; -- True while chunks are queued but not yet reaped by wait().
|
||||
; Note: a finished chunk still counts until wait() reaps it.
|
||||
return queued>0
|
||||
}
|
||||
|
||||
sub frame_size(long sampletype) -> ubyte {
|
||||
; -- Bytes per sample frame for the given AHIST_ type.
|
||||
when sampletype {
|
||||
AHIST_M8S -> return 1
|
||||
AHIST_M16S, AHIST_S8S -> return 2
|
||||
else -> return 4
|
||||
}
|
||||
}
|
||||
|
||||
private sub get_slot(bool alt) -> ^^AHIRequest {
|
||||
; request block taking the next play/wait (primary or alternate).
|
||||
if alt
|
||||
return Req1
|
||||
return Req0
|
||||
}
|
||||
|
||||
private sub wait_oldest() {
|
||||
; reap the oldest outstanding write.
|
||||
if queued==0
|
||||
return
|
||||
^^AHIRequest io = get_slot(wait_alt)
|
||||
void exec.WaitIO(io)
|
||||
wait_alt = not wait_alt
|
||||
queued = queued-1
|
||||
if queued==0 {
|
||||
play_alt = false
|
||||
wait_alt = false
|
||||
}
|
||||
}
|
||||
|
||||
private sub setup(^^AHIRequest io, pointer link, pointer samples, long num_bytes, long sampletype, uword frequency, long volume, long position) -> long {
|
||||
; -- Fill a request block. Rounds the length down to whole sample frames
|
||||
; (io_Length must be a multiple of the frame size); io_Offset must be 0.
|
||||
; Returns the rounded length (0 = nothing to play).
|
||||
ubyte frame = frame_size(sampletype)
|
||||
num_bytes = num_bytes / frame * frame
|
||||
if num_bytes==0
|
||||
return 0
|
||||
io.Command = exec.CMD_WRITE
|
||||
io.Flags = 0
|
||||
io.Data = samples
|
||||
io.IOStdReq_Length = num_bytes
|
||||
io.Offset = 0
|
||||
io.SampleType = sampletype
|
||||
io.Frequency = frequency
|
||||
io.Volume = volume
|
||||
io.Position = position
|
||||
io.Link = link
|
||||
return num_bytes
|
||||
}
|
||||
|
||||
private asmsub BeginIO(^^AHIRequest io @A1) clobbers (D0, D1, A0, A1, A6) {
|
||||
; Calls the device's BeginIO vector directly, instead of exec DoIO/SendIO,
|
||||
; because those clear io_Flags.
|
||||
%asm {{
|
||||
move.l 20(a1), a6 ; a6 = io_Device (offset 20 in IORequest)
|
||||
jsr -30(a6) ; device BeginIO vector
|
||||
rts
|
||||
}}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
%import shared_amiga_asyncio
|
||||
|
||||
asyncio {
|
||||
%option no_symbol_prefixing, ignore_unused
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
%import shared_amiga_lowlevel
|
||||
|
||||
lowlevel {
|
||||
%option no_symbol_prefixing, ignore_unused
|
||||
}
|
||||
@@ -29,6 +29,7 @@ p8_sys_startup {
|
||||
; }
|
||||
|
||||
sub init_system() {
|
||||
%option noframe
|
||||
%asm {{
|
||||
; save CLI arguments
|
||||
move.l d0,-(sp)
|
||||
@@ -89,6 +90,7 @@ proc_CLI = 172
|
||||
}
|
||||
|
||||
sub init_system_phase2() {
|
||||
%option noframe
|
||||
%asm {{
|
||||
moveq #0,d0
|
||||
moveq #0,d1
|
||||
@@ -110,6 +112,7 @@ proc_CLI = 172
|
||||
}
|
||||
|
||||
sub cleanup_at_exit() {
|
||||
%option noframe
|
||||
%asm {{
|
||||
movem.l d0,-(sp) ; keep return code
|
||||
|
||||
@@ -124,6 +127,8 @@ proc_CLI = 172
|
||||
|
||||
if sys.RexxSysBase != 0 exec.CloseLibrary(sys.RexxSysBase)
|
||||
if sys.IFFParseBase != 0 exec.CloseLibrary(sys.IFFParseBase)
|
||||
if sys.AsyncIOBase != 0 exec.CloseLibrary(sys.AsyncIOBase)
|
||||
if sys.LowLevelBase != 0 exec.CloseLibrary(sys.LowLevelBase)
|
||||
if sys.UtilityBase != 0 exec.CloseLibrary(sys.UtilityBase)
|
||||
if sys.IconBase != 0 exec.CloseLibrary(sys.IconBase)
|
||||
if sys.IntuitionBase != 0 exec.CloseLibrary(sys.IntuitionBase)
|
||||
|
||||
@@ -39,19 +39,23 @@ sys {
|
||||
pointer @shared UtilityBase
|
||||
pointer @shared RexxSysBase ; loaded from disk on demand
|
||||
pointer @shared IFFParseBase ; loaded from disk on demand
|
||||
pointer @shared AsyncIOBase ; loaded from disk on demand
|
||||
pointer @shared LowLevelBase ; kickstart 2.0+, loaded from disk on demand
|
||||
pointer @shared TimerBase ; opened on demand
|
||||
pointer @shared AHIBase ; ahi.device base, opened on demand by ahi.open(); for the low-level ahi.* LVO calls
|
||||
|
||||
^^ubyte @shared arguments ; CLI argument string (null-terminated), or NULL if Workbench launch
|
||||
|
||||
sub reset_system() {
|
||||
%option noframe
|
||||
%asm {{
|
||||
move.l 4.w,a6
|
||||
jmp -726(a6) ; ColdReboot()
|
||||
}}
|
||||
}
|
||||
|
||||
asmsub wait(long jiffies @D1) {
|
||||
; --- wait approximately the given number of jiffies (1/60th seconds)
|
||||
asmsub wait(long ticks @D1) {
|
||||
; --- wait approximately the given number of ticks (1/50th seconds, i.e. 20ms each)
|
||||
%asm {{
|
||||
move.l sys.DOSBase,a6
|
||||
jmp -198(a6) ; Delay
|
||||
@@ -68,17 +72,18 @@ sys {
|
||||
}}
|
||||
}
|
||||
|
||||
sub exit(ubyte returnvalue) {
|
||||
asmsub exit(word returnvalue @D0) {
|
||||
; -- exit the program with a return code in D0. When invoked from Prog8 via sys.exit(), all active defers in the call chain are unwound program-wide (LIFO) before system cleanup. sys.reset_system() does not run defers.
|
||||
%asm {{
|
||||
moveq.l #0,d0
|
||||
move.b sys.exit.returnvalue,d0
|
||||
and.l #$ffff,d0
|
||||
move.l p8_sys_startup.orig_stackpointer,sp
|
||||
jmp p8_sys_startup.cleanup_at_exit
|
||||
}}
|
||||
}
|
||||
|
||||
|
||||
sub set_carry() {
|
||||
%option noframe
|
||||
%asm {{
|
||||
; set both C (comparison carry) and X (rotate carry) bits
|
||||
moveq #$11,d0
|
||||
@@ -87,6 +92,7 @@ sys {
|
||||
}
|
||||
|
||||
sub clear_carry() {
|
||||
%option noframe
|
||||
%asm {{
|
||||
; clear C and X bits
|
||||
moveq #0,d0
|
||||
@@ -125,12 +131,96 @@ sys {
|
||||
}}
|
||||
}
|
||||
|
||||
asmsub stack_size() -> long @D0 {
|
||||
; -- Return the amount of stack space still available, in bytes.
|
||||
%asm {{
|
||||
move.l 4.w,a6 ; ExecBase
|
||||
move.l $114(a6),a0 ; ThisTask
|
||||
move.l sp,d0
|
||||
sub.l $3a(a0),d0 ; minus tc_SPLower
|
||||
rts
|
||||
}}
|
||||
}
|
||||
|
||||
asmsub cpuAtLeast68020() clobbers (A6, D0) -> bool @Pz {
|
||||
%asm {{
|
||||
move.l 4.w,a6
|
||||
move.w 296(a6),d0
|
||||
andi.w #$008e,d0
|
||||
seq d0
|
||||
tst.b d0
|
||||
rts
|
||||
}}
|
||||
}
|
||||
|
||||
sub die(uword code, str message) {
|
||||
%asm {{
|
||||
moveq #0,d0
|
||||
move.w 14(a5),d0
|
||||
lea .alert(pc),a1
|
||||
lea .alert_limit(pc),a3
|
||||
move.w #20,(a1)+
|
||||
move.b #16,(a1)+
|
||||
lea .prefix(pc),a2
|
||||
.copy_prefix:
|
||||
move.b (a2)+,d1
|
||||
beq.s .copy_code
|
||||
move.b d1,(a1)+
|
||||
bra.s .copy_prefix
|
||||
.copy_code:
|
||||
move.w 14(a5),d2
|
||||
moveq #3,d4
|
||||
.copy_code_digit:
|
||||
move.w d2,d3
|
||||
lsr.w #8,d3
|
||||
lsr.w #4,d3
|
||||
andi.w #$000f,d3
|
||||
cmpi.w #10,d3
|
||||
bcs.s .copy_code_number
|
||||
addi.w #'A'-10,d3
|
||||
bra.s .copy_code_store
|
||||
.copy_code_number:
|
||||
addi.w #'0',d3
|
||||
.copy_code_store:
|
||||
move.b d3,(a1)+
|
||||
lsl.w #4,d2
|
||||
dbra d4,.copy_code_digit
|
||||
clr.b (a1)+
|
||||
move.b #1,(a1)+
|
||||
move.w #20,(a1)+
|
||||
move.b #32,(a1)+
|
||||
.copy_message:
|
||||
move.l 8(a5),a0
|
||||
.copy_message_loop:
|
||||
cmpa.l a3,a1
|
||||
bhi.s .message_full
|
||||
move.b (a0)+,d1
|
||||
move.b d1,(a1)+
|
||||
bne.s .copy_message_loop
|
||||
clr.b (a1)
|
||||
bra.s .display
|
||||
.message_full:
|
||||
subq.l #1,a1
|
||||
clr.b (a1)
|
||||
clr.b 1(a1)
|
||||
.display:
|
||||
moveq #48,d1
|
||||
movea.l sys.IntuitionBase,a6
|
||||
lea .alert(pc),a0
|
||||
jsr -90(a6)
|
||||
moveq #0,d0
|
||||
move.w 14(a5),d0
|
||||
jmp sys.exit
|
||||
|
||||
even
|
||||
.alert:
|
||||
ds.b 158
|
||||
.alert_limit:
|
||||
ds.b 2
|
||||
.alert_end:
|
||||
.prefix:
|
||||
dc.b "PROGRAM DIED code $",0
|
||||
; !notreached!
|
||||
}}
|
||||
}
|
||||
}
|
||||
@@ -52,50 +52,85 @@ txt {
|
||||
private long @shared fmt_value_l
|
||||
private uword @shared fmt_value_w
|
||||
|
||||
private asmsub vprintf(str format @D1, pointer argarray @D2) {
|
||||
%asm {{
|
||||
move.l 4.w,a6
|
||||
cmp.w #36,20(a6) ; KS 2.0 = V36
|
||||
bcs.s .no_ks20
|
||||
move.l sys.DOSBase,a6
|
||||
jmp dos.VPrintf(a6)
|
||||
|
||||
.no_ks20:
|
||||
jmp p8b_txt.p8s_no_ks20_alert
|
||||
}}
|
||||
}
|
||||
|
||||
private asmsub no_ks20_alert() {
|
||||
; VPrintf number formatting is unavailable before kickstart 2.0
|
||||
%asm {{
|
||||
move.l #$00070001,d0
|
||||
lea _msg(pc),a0
|
||||
moveq #40,d1
|
||||
move.l sys.IntuitionBase,a6
|
||||
jsr -90(a6) ; DisplayAlert
|
||||
moveq #1,d0
|
||||
jmp sys.exit
|
||||
|
||||
even
|
||||
_msg:
|
||||
dc.w 20
|
||||
dc.b 16
|
||||
dc.b "textio/conv number format routines require Kickstart 2.0+",0
|
||||
dc.b 0
|
||||
; !notreached!
|
||||
}}
|
||||
}
|
||||
|
||||
|
||||
sub print_ub(ubyte value) {
|
||||
fmt_value_w = value as uword
|
||||
void dos.VPrintf("%d", &fmt_value_w)
|
||||
vprintf("%d", &fmt_value_w)
|
||||
}
|
||||
|
||||
sub print_ub0(ubyte value) {
|
||||
fmt_value_w = value as uword
|
||||
void dos.VPrintf("%03d", &fmt_value_w)
|
||||
vprintf("%03d", &fmt_value_w)
|
||||
}
|
||||
|
||||
sub print_b(byte value) {
|
||||
fmt_value_w = value as uword
|
||||
void dos.VPrintf("%d", &fmt_value_w)
|
||||
vprintf("%d", &fmt_value_w)
|
||||
}
|
||||
|
||||
sub print_uw(uword value) {
|
||||
fmt_value_l = value as long
|
||||
void dos.VPrintf("%lu", &fmt_value_l)
|
||||
vprintf("%lu", &fmt_value_l)
|
||||
}
|
||||
|
||||
sub print_uw0(uword value) {
|
||||
fmt_value_l = value as long
|
||||
void dos.VPrintf("%05lu", &fmt_value_l)
|
||||
vprintf("%05lu", &fmt_value_l)
|
||||
}
|
||||
|
||||
sub print_w(word value) {
|
||||
void dos.VPrintf("%d", &value)
|
||||
vprintf("%d", &value)
|
||||
}
|
||||
|
||||
sub print_l(long value) {
|
||||
void dos.VPrintf("%ld", &value)
|
||||
vprintf("%ld", &value)
|
||||
}
|
||||
|
||||
sub print_ulhex(long value, bool prefix) {
|
||||
void dos.VPrintf(if prefix then "$%08lx" else "%08lx", &value)
|
||||
vprintf(if prefix then "$%08lx" else "%08lx", &value)
|
||||
}
|
||||
|
||||
sub print_uwhex(uword value, bool prefix) {
|
||||
void dos.VPrintf(if prefix then "$%04x" else "%04x", &value)
|
||||
vprintf(if prefix then "$%04x" else "%04x", &value)
|
||||
}
|
||||
|
||||
sub print_ubhex(ubyte value, bool prefix) {
|
||||
fmt_value_w = value as uword
|
||||
void dos.VPrintf(if prefix then "$%02x" else "%02x", &fmt_value_w)
|
||||
vprintf(if prefix then "$%02x" else "%02x", &fmt_value_w)
|
||||
}
|
||||
|
||||
sub print_ubbin(ubyte value, bool prefix) {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
%import exec
|
||||
%import shared_amiga_trackdisk
|
||||
|
||||
trackdisk {
|
||||
%option no_symbol_prefixing, ignore_unused
|
||||
|
||||
sub opendevice(ubyte unit) -> bool {
|
||||
; open a trackdisk unit (0-3 = DF0:-DF3:)
|
||||
^^exec.MsgPort tdPort = exec.CreateMsgPort()
|
||||
if tdPort == 0 return false
|
||||
TrackIO = exec.CreateIORequest(tdPort, sizeof(exec.IOStdReq))
|
||||
if TrackIO == 0 {
|
||||
exec.DeleteMsgPort(tdPort)
|
||||
return false
|
||||
}
|
||||
|
||||
if exec.OpenDevice("trackdisk.device", unit, TrackIO, 0)==0 {
|
||||
return true
|
||||
}
|
||||
exec.DeleteIORequest(TrackIO)
|
||||
exec.DeleteMsgPort(tdPort)
|
||||
TrackIO = 0
|
||||
return false
|
||||
}
|
||||
|
||||
sub closedevice() {
|
||||
if TrackIO != 0 {
|
||||
^^exec.MsgPort tdPort = TrackIO.ReplyPort
|
||||
exec.CloseDevice(TrackIO)
|
||||
exec.DeleteIORequest(TrackIO)
|
||||
exec.DeleteMsgPort(tdPort)
|
||||
TrackIO = 0
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
%import shared_m68k_wavfile
|
||||
|
||||
wavfile {
|
||||
%option ignore_unused
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
%import shared_m68k_adpcm
|
||||
|
||||
adpcm {
|
||||
%option ignore_unused, private_symbols
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
%import shared_amiga_asyncio
|
||||
|
||||
asyncio {
|
||||
%option no_symbol_prefixing, ignore_unused
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
%import shared_amiga_lowlevel
|
||||
|
||||
lowlevel {
|
||||
%option no_symbol_prefixing, ignore_unused
|
||||
}
|
||||
@@ -29,6 +29,7 @@ p8_sys_startup {
|
||||
; }
|
||||
|
||||
sub init_system() {
|
||||
%option noframe
|
||||
%asm {{
|
||||
; save CLI arguments
|
||||
move.l d0,-(sp)
|
||||
@@ -92,6 +93,7 @@ proc_CLI = 172
|
||||
}
|
||||
|
||||
sub init_system_phase2() {
|
||||
%option noframe
|
||||
%asm {{
|
||||
moveq #0,d0
|
||||
moveq #0,d1
|
||||
@@ -113,6 +115,7 @@ proc_CLI = 172
|
||||
}
|
||||
|
||||
sub cleanup_at_exit() {
|
||||
%option noframe
|
||||
%asm {{
|
||||
movem.l d0,-(sp) ; keep return code
|
||||
|
||||
@@ -127,6 +130,8 @@ proc_CLI = 172
|
||||
|
||||
if sys.RexxSysBase != 0 exec.CloseLibrary(sys.RexxSysBase)
|
||||
if sys.IFFParseBase != 0 exec.CloseLibrary(sys.IFFParseBase)
|
||||
if sys.AsyncIOBase != 0 exec.CloseLibrary(sys.AsyncIOBase)
|
||||
if sys.LowLevelBase != 0 exec.CloseLibrary(sys.LowLevelBase)
|
||||
if sys.UtilityBase != 0 exec.CloseLibrary(sys.UtilityBase)
|
||||
if sys.IconBase != 0 exec.CloseLibrary(sys.IconBase)
|
||||
if sys.IntuitionBase != 0 exec.CloseLibrary(sys.IntuitionBase)
|
||||
|
||||
@@ -39,11 +39,14 @@ sys {
|
||||
pointer @shared UtilityBase ; kickstart 2.0+
|
||||
pointer @shared RexxSysBase ; kickstart 2.0+, loaded from disk on demand
|
||||
pointer @shared IFFParseBase ; kickstart 2.0+, loaded from disk on demand
|
||||
pointer @shared AsyncIOBase ; loaded from disk on demand
|
||||
pointer @shared LowLevelBase ; kickstart 2.0+, loaded from disk on demand
|
||||
pointer @shared TimerBase ; opened on demand
|
||||
|
||||
^^ubyte @shared arguments ; CLI argument string (null-terminated), or NULL if Workbench launch
|
||||
|
||||
sub reset_system() {
|
||||
%option noframe
|
||||
%asm {{
|
||||
move.l 4.w,a6
|
||||
move.w 20(a6),d0 ; ExecBase version
|
||||
@@ -60,8 +63,8 @@ sys {
|
||||
}}
|
||||
}
|
||||
|
||||
asmsub wait(long jiffies @D1) {
|
||||
; --- wait approximately the given number of jiffies (1/60th seconds)
|
||||
asmsub wait(long ticks @D1) {
|
||||
; --- wait approximately the given number of ticks (1/50th seconds, i.e. 20ms each)
|
||||
%asm {{
|
||||
move.l sys.DOSBase,a6
|
||||
jmp -198(a6) ; Delay
|
||||
@@ -78,17 +81,17 @@ sys {
|
||||
}}
|
||||
}
|
||||
|
||||
sub exit(ubyte returnvalue) {
|
||||
asmsub exit(word returnvalue @D0) {
|
||||
; -- exit the program with a return code in D0. When invoked from Prog8 via sys.exit(), all active defers in the call chain are unwound program-wide (LIFO) before system cleanup. sys.reset_system() does not run defers.
|
||||
%asm {{
|
||||
moveq.l #0,d0
|
||||
move.b sys.exit.returnvalue,d0
|
||||
and.l #$ffff,d0
|
||||
move.l p8_sys_startup.orig_stackpointer,sp
|
||||
jmp p8_sys_startup.cleanup_at_exit
|
||||
}}
|
||||
}
|
||||
|
||||
sub set_carry() {
|
||||
%option noframe
|
||||
%asm {{
|
||||
; set both C (comparison carry) and X (rotate carry) bits
|
||||
moveq #$11,d0
|
||||
@@ -97,6 +100,7 @@ sys {
|
||||
}
|
||||
|
||||
sub clear_carry() {
|
||||
%option noframe
|
||||
%asm {{
|
||||
; clear C and X bits
|
||||
moveq #0,d0
|
||||
@@ -135,12 +139,96 @@ sys {
|
||||
}}
|
||||
}
|
||||
|
||||
asmsub stack_size() -> long @D0 {
|
||||
; -- Return the amount of stack space still available, in bytes.
|
||||
%asm {{
|
||||
move.l 4.w,a6 ; ExecBase
|
||||
move.l $114(a6),a0 ; ThisTask
|
||||
move.l sp,d0
|
||||
sub.l $3a(a0),d0 ; minus tc_SPLower
|
||||
rts
|
||||
}}
|
||||
}
|
||||
|
||||
asmsub cpuAtLeast68020() clobbers (A6, D0) -> bool @Pz {
|
||||
%asm {{
|
||||
move.l 4.w,a6
|
||||
move.w 296(a6),d0
|
||||
andi.w #$008e,d0
|
||||
seq d0
|
||||
tst.b d0
|
||||
rts
|
||||
}}
|
||||
}
|
||||
|
||||
sub die(uword code, str message) {
|
||||
%asm {{
|
||||
moveq #0,d0
|
||||
move.w 14(a5),d0
|
||||
lea .alert(pc),a1
|
||||
lea .alert_limit(pc),a3
|
||||
move.w #20,(a1)+
|
||||
move.b #16,(a1)+
|
||||
lea .prefix(pc),a2
|
||||
.copy_prefix:
|
||||
move.b (a2)+,d1
|
||||
beq.s .copy_code
|
||||
move.b d1,(a1)+
|
||||
bra.s .copy_prefix
|
||||
.copy_code:
|
||||
move.w 14(a5),d2
|
||||
moveq #3,d4
|
||||
.copy_code_digit:
|
||||
move.w d2,d3
|
||||
lsr.w #8,d3
|
||||
lsr.w #4,d3
|
||||
andi.w #$000f,d3
|
||||
cmpi.w #10,d3
|
||||
bcs.s .copy_code_number
|
||||
addi.w #'A'-10,d3
|
||||
bra.s .copy_code_store
|
||||
.copy_code_number:
|
||||
addi.w #'0',d3
|
||||
.copy_code_store:
|
||||
move.b d3,(a1)+
|
||||
lsl.w #4,d2
|
||||
dbra d4,.copy_code_digit
|
||||
clr.b (a1)+
|
||||
move.b #1,(a1)+
|
||||
move.w #20,(a1)+
|
||||
move.b #32,(a1)+
|
||||
.copy_message:
|
||||
move.l 8(a5),a0
|
||||
.copy_message_loop:
|
||||
cmpa.l a3,a1
|
||||
bhi.s .message_full
|
||||
move.b (a0)+,d1
|
||||
move.b d1,(a1)+
|
||||
bne.s .copy_message_loop
|
||||
clr.b (a1)
|
||||
bra.s .display
|
||||
.message_full:
|
||||
subq.l #1,a1
|
||||
clr.b (a1)
|
||||
clr.b 1(a1)
|
||||
.display:
|
||||
moveq #48,d1
|
||||
movea.l sys.IntuitionBase,a6
|
||||
lea .alert(pc),a0
|
||||
jsr -90(a6)
|
||||
moveq #0,d0
|
||||
move.w 14(a5),d0
|
||||
jmp sys.exit
|
||||
|
||||
even
|
||||
.alert:
|
||||
ds.b 158
|
||||
.alert_limit:
|
||||
ds.b 2
|
||||
.alert_end:
|
||||
.prefix:
|
||||
dc.b "PROGRAM DIED code $",0
|
||||
; !notreached!
|
||||
}}
|
||||
}
|
||||
}
|
||||
@@ -81,50 +81,84 @@ txt {
|
||||
private long @shared fmt_value_l
|
||||
private uword @shared fmt_value_w
|
||||
|
||||
private asmsub vprintf(str format @D1, pointer argarray @D2) {
|
||||
%asm {{
|
||||
move.l 4.w,a6
|
||||
cmp.w #36,20(a6) ; KS 2.0 = V36
|
||||
bcs.s .no_ks20
|
||||
move.l sys.DOSBase,a6
|
||||
jmp dos.VPrintf(a6)
|
||||
|
||||
.no_ks20:
|
||||
jmp p8b_txt.p8s_no_ks20_alert
|
||||
}}
|
||||
}
|
||||
|
||||
private asmsub no_ks20_alert() {
|
||||
; VPrintf number formatting is unavailable before kickstart 2.0
|
||||
%asm {{
|
||||
move.l #$00070001,d0
|
||||
lea _msg(pc),a0
|
||||
moveq #40,d1
|
||||
move.l sys.IntuitionBase,a6
|
||||
jsr -90(a6) ; DisplayAlert
|
||||
moveq #1,d0
|
||||
jmp sys.exit
|
||||
|
||||
even
|
||||
_msg:
|
||||
dc.w 20
|
||||
dc.b 16
|
||||
dc.b "textio/conv number format routines require Kickstart 2.0+",0
|
||||
dc.b 0
|
||||
; !notreached!
|
||||
}}
|
||||
}
|
||||
|
||||
sub print_ub(ubyte value) {
|
||||
fmt_value_w = value as uword
|
||||
void dos.VPrintf("%d", &fmt_value_w)
|
||||
vprintf("%d", &fmt_value_w)
|
||||
}
|
||||
|
||||
sub print_ub0(ubyte value) {
|
||||
fmt_value_w = value as uword
|
||||
void dos.VPrintf("%03d", &fmt_value_w)
|
||||
vprintf("%03d", &fmt_value_w)
|
||||
}
|
||||
|
||||
sub print_b(byte value) {
|
||||
fmt_value_w = value as uword
|
||||
void dos.VPrintf("%d", &fmt_value_w)
|
||||
vprintf("%d", &fmt_value_w)
|
||||
}
|
||||
|
||||
sub print_uw(uword value) {
|
||||
fmt_value_l = value as long
|
||||
void dos.VPrintf("%lu", &fmt_value_l)
|
||||
vprintf("%lu", &fmt_value_l)
|
||||
}
|
||||
|
||||
sub print_uw0(uword value) {
|
||||
fmt_value_l = value as long
|
||||
void dos.VPrintf("%05lu", &fmt_value_l)
|
||||
vprintf("%05lu", &fmt_value_l)
|
||||
}
|
||||
|
||||
sub print_w(word value) {
|
||||
void dos.VPrintf("%d", &value)
|
||||
vprintf("%d", &value)
|
||||
}
|
||||
|
||||
sub print_l(long value) {
|
||||
void dos.VPrintf("%ld", &value)
|
||||
vprintf("%ld", &value)
|
||||
}
|
||||
|
||||
sub print_ulhex(long value, bool prefix) {
|
||||
void dos.VPrintf(if prefix then "$%08lx" else "%08lx", &value)
|
||||
vprintf(if prefix then "$%08lx" else "%08lx", &value)
|
||||
}
|
||||
|
||||
sub print_uwhex(uword value, bool prefix) {
|
||||
void dos.VPrintf(if prefix then "$%04x" else "%04x", &value)
|
||||
vprintf(if prefix then "$%04x" else "%04x", &value)
|
||||
}
|
||||
|
||||
sub print_ubhex(ubyte value, bool prefix) {
|
||||
fmt_value_w = value as uword
|
||||
void dos.VPrintf(if prefix then "$%02x" else "%02x", &fmt_value_w)
|
||||
vprintf(if prefix then "$%02x" else "%02x", &fmt_value_w)
|
||||
}
|
||||
|
||||
sub print_ubbin(ubyte value, bool prefix) {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
%import exec
|
||||
%import shared_amiga_trackdisk
|
||||
|
||||
trackdisk {
|
||||
%option no_symbol_prefixing, ignore_unused
|
||||
|
||||
sub opendevice(ubyte unit) -> bool {
|
||||
; open a trackdisk unit (0-3 = DF0:-DF3:) in a kickstart 1.3 compatible fashion
|
||||
^^exec.MsgPort tdPort = exec.AllocMem(sizeof(exec.MsgPort), exec.MEMF_PUBLIC | exec.MEMF_CLEAR)
|
||||
if tdPort == 0 return false
|
||||
TrackIO = exec.AllocMem(sizeof(exec.IOStdReq), exec.MEMF_PUBLIC | exec.MEMF_CLEAR)
|
||||
if TrackIO == 0 {
|
||||
exec.FreeMem(tdPort, sizeof(exec.MsgPort))
|
||||
return false
|
||||
}
|
||||
tdPort.Type = exec.NT_MSGPORT
|
||||
tdPort.Flags = exec.PA_SIGNAL
|
||||
tdPort.SigBit = exec.AllocSignal(-1) as ubyte
|
||||
tdPort.SigTask = exec.FindTask(0)
|
||||
^^exec.List listPtr = &&tdPort.Head as ^^exec.List
|
||||
exec.NewList(listPtr)
|
||||
TrackIO.ReplyPort = tdPort
|
||||
|
||||
if exec.OpenDevice("trackdisk.device", unit, TrackIO, 0)==0 {
|
||||
return true
|
||||
}
|
||||
exec.FreeSignal(tdPort.SigBit as byte)
|
||||
exec.FreeMem(TrackIO, sizeof(exec.IOStdReq))
|
||||
exec.FreeMem(tdPort, sizeof(exec.MsgPort))
|
||||
TrackIO = 0
|
||||
return false
|
||||
}
|
||||
|
||||
sub closedevice() {
|
||||
if TrackIO != 0 {
|
||||
^^exec.MsgPort tdPort = TrackIO.ReplyPort
|
||||
byte sigbit = tdPort.SigBit as byte
|
||||
exec.CloseDevice(TrackIO)
|
||||
exec.FreeSignal(sigbit)
|
||||
exec.FreeMem(TrackIO, sizeof(exec.IOStdReq))
|
||||
exec.FreeMem(tdPort, sizeof(exec.MsgPort))
|
||||
TrackIO = 0
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
%import shared_m68k_wavfile
|
||||
|
||||
wavfile {
|
||||
%option ignore_unused
|
||||
}
|
||||
@@ -0,0 +1,791 @@
|
||||
%option ignore_unused
|
||||
|
||||
vera {
|
||||
; VERA register definitions and helper routines for the 'VERA 64' board for the C64
|
||||
|
||||
%option ignore_unused, no_symbol_prefixing
|
||||
|
||||
const uword VERA_BASE = $DE00 ; TODO to be decided?
|
||||
|
||||
&ubyte VERA_ADDR_L = VERA_BASE + $0000
|
||||
&ubyte VERA_ADDR_M = VERA_BASE + $0001
|
||||
&uword VERA_ADDR = VERA_BASE + $0000 ; still need to do the _H separately
|
||||
&ubyte VERA_ADDR_H = VERA_BASE + $0002
|
||||
&ubyte VERA_DATA0 = VERA_BASE + $0003
|
||||
&ubyte VERA_DATA1 = VERA_BASE + $0004
|
||||
&ubyte VERA_CTRL = VERA_BASE + $0005
|
||||
&ubyte VERA_IEN = VERA_BASE + $0006
|
||||
&ubyte VERA_ISR = VERA_BASE + $0007
|
||||
&ubyte VERA_IRQLINE_L = VERA_BASE + $0008 ; write only
|
||||
&ubyte VERA_SCANLINE_L = VERA_BASE + $0008 ; read only
|
||||
&ubyte VERA_DC_VIDEO = VERA_BASE + $0009 ; DCSEL= 0
|
||||
&ubyte VERA_DC_HSCALE = VERA_BASE + $000A ; DCSEL= 0
|
||||
&ubyte VERA_DC_VSCALE = VERA_BASE + $000B ; DCSEL= 0
|
||||
&ubyte VERA_DC_BORDER = VERA_BASE + $000C ; DCSEL= 0
|
||||
&ubyte VERA_DC_HSTART = VERA_BASE + $0009 ; DCSEL= 1
|
||||
&ubyte VERA_DC_HSTOP = VERA_BASE + $000A ; DCSEL= 1
|
||||
&ubyte VERA_DC_VSTART = VERA_BASE + $000B ; DCSEL= 1
|
||||
&ubyte VERA_DC_VSTOP = VERA_BASE + $000C ; DCSEL= 1
|
||||
&ubyte VERA_DC_VER0 = VERA_BASE + $0009 ; DCSEL=63
|
||||
&ubyte VERA_DC_VER1 = VERA_BASE + $000A ; DCSEL=63
|
||||
&ubyte VERA_DC_VER2 = VERA_BASE + $000B ; DCSEL=63
|
||||
&ubyte VERA_DC_VER3 = VERA_BASE + $000C ; DCSEL=63
|
||||
&ubyte VERA_L0_CONFIG = VERA_BASE + $000D
|
||||
&ubyte VERA_L0_MAPBASE = VERA_BASE + $000E
|
||||
&ubyte VERA_L0_TILEBASE = VERA_BASE + $000F
|
||||
&ubyte VERA_L0_HSCROLL_L = VERA_BASE + $0010
|
||||
&ubyte VERA_L0_HSCROLL_H = VERA_BASE + $0011
|
||||
&uword VERA_L0_HSCROLL = VERA_BASE + $0010
|
||||
&ubyte VERA_L0_VSCROLL_L = VERA_BASE + $0012
|
||||
&ubyte VERA_L0_VSCROLL_H = VERA_BASE + $0013
|
||||
&uword VERA_L0_VSCROLL = VERA_BASE + $0012
|
||||
&ubyte VERA_L1_CONFIG = VERA_BASE + $0014
|
||||
&ubyte VERA_L1_MAPBASE = VERA_BASE + $0015
|
||||
&ubyte VERA_L1_TILEBASE = VERA_BASE + $0016
|
||||
&ubyte VERA_L1_HSCROLL_L = VERA_BASE + $0017
|
||||
&ubyte VERA_L1_HSCROLL_H = VERA_BASE + $0018
|
||||
&uword VERA_L1_HSCROLL = VERA_BASE + $0017
|
||||
&ubyte VERA_L1_VSCROLL_L = VERA_BASE + $0019
|
||||
&ubyte VERA_L1_VSCROLL_H = VERA_BASE + $001A
|
||||
&uword VERA_L1_VSCROLL = VERA_BASE + $0019
|
||||
&ubyte VERA_AUDIO_CTRL = VERA_BASE + $001B
|
||||
&ubyte VERA_AUDIO_RATE = VERA_BASE + $001C
|
||||
&ubyte VERA_AUDIO_DATA = VERA_BASE + $001D
|
||||
&ubyte VERA_SPI_DATA = VERA_BASE + $001E
|
||||
&ubyte VERA_SPI_CTRL = VERA_BASE + $001F
|
||||
|
||||
; Vera FX registers: (accessing depends on particular DCSEL value set in VERA_CTRL!)
|
||||
&ubyte VERA_FX_CTRL = VERA_BASE + $0009
|
||||
&ubyte VERA_FX_TILEBASE = VERA_BASE + $000a
|
||||
&ubyte VERA_FX_MAPBASE = VERA_BASE + $000b
|
||||
&ubyte VERA_FX_MULT = VERA_BASE + $000c
|
||||
&ubyte VERA_FX_X_INCR_L = VERA_BASE + $0009
|
||||
&ubyte VERA_FX_X_INCR_H = VERA_BASE + $000a
|
||||
&uword VERA_FX_X_INCR = VERA_BASE + $0009
|
||||
&ubyte VERA_FX_Y_INCR_L = VERA_BASE + $000b
|
||||
&ubyte VERA_FX_Y_INCR_H = VERA_BASE + $000c
|
||||
&uword VERA_FX_Y_INCR = VERA_BASE + $000b
|
||||
&ubyte VERA_FX_X_POS_L = VERA_BASE + $0009
|
||||
&ubyte VERA_FX_X_POS_H = VERA_BASE + $000a
|
||||
&uword VERA_FX_X_POS = VERA_BASE + $0009
|
||||
&ubyte VERA_FX_Y_POS_L = VERA_BASE + $000b
|
||||
&ubyte VERA_FX_Y_POS_H = VERA_BASE + $000c
|
||||
&uword VERA_FX_Y_POS = VERA_BASE + $000b
|
||||
&ubyte VERA_FX_X_POS_S = VERA_BASE + $0009
|
||||
&ubyte VERA_FX_Y_POS_S = VERA_BASE + $000a
|
||||
&ubyte VERA_FX_POLY_FILL_L = VERA_BASE + $000b
|
||||
&ubyte VERA_FX_POLY_FILL_H = VERA_BASE + $000c
|
||||
&uword VERA_FX_POLY_FILL = VERA_BASE + $000b
|
||||
&ubyte VERA_FX_CACHE_L = VERA_BASE + $0009
|
||||
&ubyte VERA_FX_CACHE_M = VERA_BASE + $000a
|
||||
&ubyte VERA_FX_CACHE_H = VERA_BASE + $000b
|
||||
&ubyte VERA_FX_CACHE_U = VERA_BASE + $000c
|
||||
&ubyte VERA_FX_ACCUM = VERA_BASE + $000a
|
||||
&ubyte VERA_FX_ACCUM_RESET = VERA_BASE + $0009
|
||||
|
||||
|
||||
sub reset() {
|
||||
VERA_CTRL = %10000000 ; bit 7 = Reset Adapter
|
||||
; the fpga takes milliseconds to reconfigure, so wait until vera
|
||||
; responds again by polling a register write/readback until it sticks
|
||||
repeat {
|
||||
VERA_ADDR_L = $42
|
||||
if VERA_ADDR_L == $42 {
|
||||
break
|
||||
}
|
||||
}
|
||||
VERA_CTRL = 0
|
||||
}
|
||||
|
||||
asmsub vpeek(ubyte bank @A, uword address @XY) -> ubyte @A {
|
||||
; -- get a byte from VERA's video memory
|
||||
; note: inefficient when reading multiple sequential bytes!
|
||||
%asm {{
|
||||
pha
|
||||
lda #0
|
||||
sta VERA_CTRL
|
||||
pla
|
||||
sta VERA_ADDR_H
|
||||
sty VERA_ADDR_M
|
||||
stx VERA_ADDR_L
|
||||
lda VERA_DATA0
|
||||
rts
|
||||
}}
|
||||
}
|
||||
|
||||
asmsub vaddr(ubyte bank @A, uword address @R0, ubyte addrsel @R1, byte autoIncrOrDecrByOne @Y) clobbers(A) {
|
||||
; -- setup the VERA's data address register 0 or 1 with optional auto increment or decrement of 1.
|
||||
; This is a convenience routine, and not very efficient if you call it often;
|
||||
; it's usually better to write a tailor made version of it that accounts for the repeated values.
|
||||
; Note that the vaddr_autoincr() and vaddr_autodecr() routines allow to set all possible strides, not just 1.
|
||||
; Note also that Vera's addrset is reset to 0 on exit, even if you set port #1's address.
|
||||
%asm {{
|
||||
pha
|
||||
lda cx16.r1
|
||||
and #1
|
||||
sta VERA_CTRL
|
||||
lda cx16.r0
|
||||
sta VERA_ADDR_L
|
||||
lda cx16.r0+1
|
||||
sta VERA_ADDR_M
|
||||
pla
|
||||
cpy #0
|
||||
bmi ++
|
||||
beq +
|
||||
ora #%00010000
|
||||
+ sta VERA_ADDR_H
|
||||
lda #0
|
||||
sta VERA_CTRL
|
||||
rts
|
||||
+ ora #%00011000
|
||||
sta VERA_ADDR_H
|
||||
lda #0
|
||||
sta VERA_CTRL
|
||||
rts
|
||||
}}
|
||||
}
|
||||
|
||||
asmsub vaddr_clone(ubyte port @A) clobbers (A,X,Y) {
|
||||
; -- clones Vera addresses from the given source port to the other one.
|
||||
; This is a convenience routine, and not very efficient if you call it often;
|
||||
; it's usually better to write a tailor made version of it that accounts for the repeated values.
|
||||
%asm {{
|
||||
sta VERA_CTRL
|
||||
ldx VERA_ADDR_L
|
||||
ldy VERA_ADDR_H
|
||||
sty P8ZP_SCRATCH_B1
|
||||
ldy VERA_ADDR_M
|
||||
eor #1
|
||||
sta VERA_CTRL
|
||||
stx VERA_ADDR_L
|
||||
sty VERA_ADDR_M
|
||||
ldy P8ZP_SCRATCH_B1
|
||||
sty VERA_ADDR_H
|
||||
lda #0
|
||||
sta VERA_CTRL
|
||||
rts
|
||||
}}
|
||||
}
|
||||
|
||||
asmsub vaddr_autoincr(ubyte bank @A, uword address @R0, ubyte addrsel @R1, uword autoIncrAmount @R2) clobbers(A,Y) {
|
||||
; -- setup the VERA's data address register 0 or 1, including setting up optional auto increment amount.
|
||||
; Specifiying an unsupported amount results in amount of zero. See the Vera docs about what amounts are possible.
|
||||
; This is a convenience routine, and not very efficient if you call it often;
|
||||
; it's usually better to write a tailor made version of it that accounts for the repeated values.
|
||||
%asm {{
|
||||
jsr _setup
|
||||
lda cx16.r2H
|
||||
ora cx16.r2L
|
||||
beq +
|
||||
jsr _determine_incr_bits
|
||||
+ ora P8ZP_SCRATCH_REG
|
||||
sta VERA_ADDR_H
|
||||
lda #0
|
||||
sta VERA_CTRL
|
||||
rts
|
||||
|
||||
_setup sta P8ZP_SCRATCH_REG
|
||||
lda cx16.r1
|
||||
and #1
|
||||
sta VERA_CTRL
|
||||
lda cx16.r0
|
||||
sta VERA_ADDR_L
|
||||
lda cx16.r0+1
|
||||
sta VERA_ADDR_M
|
||||
rts
|
||||
|
||||
_determine_incr_bits
|
||||
lda cx16.r2H
|
||||
bne _large
|
||||
lda cx16.r2L
|
||||
ldy #13
|
||||
- cmp _strides_lsb,y
|
||||
beq +
|
||||
dey
|
||||
bpl -
|
||||
+ tya
|
||||
asl a
|
||||
asl a
|
||||
asl a
|
||||
asl a
|
||||
rts
|
||||
_large ora cx16.r2L
|
||||
cmp #1 ; 256
|
||||
bne +
|
||||
lda #9<<4
|
||||
rts
|
||||
+ cmp #2 ; 512
|
||||
bne +
|
||||
lda #10<<4
|
||||
rts
|
||||
+ cmp #65 ; 320
|
||||
bne +
|
||||
lda #14<<4
|
||||
rts
|
||||
+ cmp #130 ; 640
|
||||
bne +
|
||||
lda #15<<4
|
||||
rts
|
||||
+ lda #0
|
||||
rts
|
||||
_strides_lsb .byte 0,1,2,4,8,16,32,64,128,255,255,40,80,160,255,255
|
||||
; !notreached!
|
||||
}}
|
||||
}
|
||||
|
||||
asmsub vaddr_autodecr(ubyte bank @A, uword address @R0, ubyte addrsel @R1, uword autoDecrAmount @R2) clobbers(A,Y) {
|
||||
; -- setup the VERA's data address register 0 or 1 including setting up optional auto decrement amount.
|
||||
; Specifiying an unsupported amount results in amount of zero. See the Vera docs about what amounts are possible.
|
||||
; This is a convenience routine, and not very efficient if you call it often;
|
||||
; it's usually better to write a tailor made version of it that accounts for the repeated values.
|
||||
%asm {{
|
||||
jsr vaddr_autoincr._setup
|
||||
lda cx16.r2H
|
||||
ora cx16.r2L
|
||||
beq +
|
||||
jsr vaddr_autoincr._determine_incr_bits
|
||||
ora #%00001000 ; autodecrement
|
||||
+ ora P8ZP_SCRATCH_REG
|
||||
sta VERA_ADDR_H
|
||||
lda #0
|
||||
sta VERA_CTRL
|
||||
rts
|
||||
}}
|
||||
}
|
||||
|
||||
asmsub vpoke(ubyte bank @A, uword address @R0, ubyte value @Y) clobbers(A) {
|
||||
; -- write a single byte to VERA's video memory
|
||||
; note: inefficient when writing multiple sequential bytes!
|
||||
%asm {{
|
||||
pha
|
||||
lda #0
|
||||
sta VERA_CTRL
|
||||
pla
|
||||
sta VERA_ADDR_H
|
||||
lda cx16.r0
|
||||
sta VERA_ADDR_L
|
||||
lda cx16.r0+1
|
||||
sta VERA_ADDR_M
|
||||
sty VERA_DATA0
|
||||
rts
|
||||
}}
|
||||
}
|
||||
|
||||
asmsub vpoke_or(ubyte bank @A, uword address @R0, ubyte value @Y) clobbers (A) {
|
||||
; -- or a single byte to the value already in the VERA's video memory at that location
|
||||
; note: inefficient when writing multiple sequential bytes!
|
||||
%asm {{
|
||||
pha
|
||||
lda #0
|
||||
sta VERA_CTRL
|
||||
pla
|
||||
sta VERA_ADDR_H
|
||||
lda cx16.r0
|
||||
sta VERA_ADDR_L
|
||||
lda cx16.r0+1
|
||||
sta VERA_ADDR_M
|
||||
tya
|
||||
ora VERA_DATA0
|
||||
sta VERA_DATA0
|
||||
rts
|
||||
}}
|
||||
}
|
||||
|
||||
asmsub vpoke_and(ubyte bank @A, uword address @R0, ubyte value @Y) clobbers(A) {
|
||||
; -- and a single byte to the value already in the VERA's video memory at that location
|
||||
; note: inefficient when writing multiple sequential bytes!
|
||||
%asm {{
|
||||
pha
|
||||
lda #0
|
||||
sta VERA_CTRL
|
||||
pla
|
||||
sta VERA_ADDR_H
|
||||
lda cx16.r0
|
||||
sta VERA_ADDR_L
|
||||
lda cx16.r0+1
|
||||
sta VERA_ADDR_M
|
||||
tya
|
||||
and VERA_DATA0
|
||||
sta VERA_DATA0
|
||||
rts
|
||||
}}
|
||||
}
|
||||
|
||||
asmsub vpoke_xor(ubyte bank @A, uword address @R0, ubyte value @Y) clobbers (A) {
|
||||
; -- xor a single byte to the value already in the VERA's video memory at that location
|
||||
; note: inefficient when writing multiple sequential bytes!
|
||||
%asm {{
|
||||
pha
|
||||
lda #0
|
||||
sta VERA_CTRL
|
||||
pla
|
||||
sta VERA_ADDR_H
|
||||
lda cx16.r0
|
||||
sta VERA_ADDR_L
|
||||
lda cx16.r0+1
|
||||
sta VERA_ADDR_M
|
||||
tya
|
||||
eor VERA_DATA0
|
||||
sta VERA_DATA0
|
||||
rts
|
||||
}}
|
||||
}
|
||||
|
||||
asmsub vpoke_mask(ubyte bank @A, uword address @R0, ubyte mask @X, ubyte value @Y) clobbers (A) {
|
||||
; -- bitwise or a single byte to the value already in the VERA's video memory at that location
|
||||
; after applying the and-mask. Note: inefficient when writing multiple sequential bytes!
|
||||
%asm {{
|
||||
sty P8ZP_SCRATCH_B1
|
||||
pha
|
||||
lda #0
|
||||
sta VERA_CTRL
|
||||
pla
|
||||
sta VERA_ADDR_H
|
||||
lda cx16.r0
|
||||
sta VERA_ADDR_L
|
||||
lda cx16.r0+1
|
||||
sta VERA_ADDR_M
|
||||
txa
|
||||
and VERA_DATA0
|
||||
ora P8ZP_SCRATCH_B1
|
||||
sta VERA_DATA0
|
||||
rts
|
||||
}}
|
||||
}
|
||||
|
||||
asmsub save_virtual_registers() clobbers(A,Y) {
|
||||
%asm {{
|
||||
ldy #31
|
||||
- lda cx16.r0,y
|
||||
sta _cx16_vreg_storage,y
|
||||
dey
|
||||
bpl -
|
||||
rts
|
||||
|
||||
.section BSS
|
||||
_cx16_vreg_storage
|
||||
.word ?,?,?,?,?,?,?,?
|
||||
.word ?,?,?,?,?,?,?,?
|
||||
.send BSS
|
||||
; !notreached!
|
||||
}}
|
||||
}
|
||||
|
||||
asmsub restore_virtual_registers() clobbers(A,Y) {
|
||||
%asm {{
|
||||
ldy #31
|
||||
- lda save_virtual_registers._cx16_vreg_storage,y
|
||||
sta cx16.r0,y
|
||||
dey
|
||||
bpl -
|
||||
rts
|
||||
}}
|
||||
}
|
||||
|
||||
|
||||
asmsub save_vera_context() clobbers(A) {
|
||||
; -- use this at the start of your IRQ handler if it uses Vera registers, to save the state
|
||||
%asm {{
|
||||
; note cannot store this on cpu hardware stack because this gets called as a subroutine
|
||||
lda VERA_ADDR_L
|
||||
sta _vera_storage
|
||||
lda VERA_ADDR_M
|
||||
sta _vera_storage+1
|
||||
lda VERA_ADDR_H
|
||||
sta _vera_storage+2
|
||||
lda VERA_CTRL
|
||||
sta _vera_storage+3
|
||||
eor #1
|
||||
sta _vera_storage+7
|
||||
sta VERA_CTRL
|
||||
lda VERA_ADDR_L
|
||||
sta _vera_storage+4
|
||||
lda VERA_ADDR_M
|
||||
sta _vera_storage+5
|
||||
lda VERA_ADDR_H
|
||||
sta _vera_storage+6
|
||||
rts
|
||||
.section BSS
|
||||
_vera_storage: .byte ?,?,?,?,?,?,?,?
|
||||
.send BSS
|
||||
; !notreached!
|
||||
}}
|
||||
}
|
||||
|
||||
asmsub restore_vera_context() clobbers(A) {
|
||||
; -- use this at the end of your IRQ handler if it uses Vera registers, to restore the state
|
||||
%asm {{
|
||||
lda vera.save_vera_context._vera_storage+7
|
||||
sta VERA_CTRL
|
||||
lda vera.save_vera_context._vera_storage+6
|
||||
sta VERA_ADDR_H
|
||||
lda vera.save_vera_context._vera_storage+5
|
||||
sta VERA_ADDR_M
|
||||
lda vera.save_vera_context._vera_storage+4
|
||||
sta VERA_ADDR_L
|
||||
lda vera.save_vera_context._vera_storage+3
|
||||
sta VERA_CTRL
|
||||
lda vera.save_vera_context._vera_storage+2
|
||||
sta VERA_ADDR_H
|
||||
lda vera.save_vera_context._vera_storage+1
|
||||
sta VERA_ADDR_M
|
||||
lda vera.save_vera_context._vera_storage+0
|
||||
sta VERA_ADDR_L
|
||||
rts
|
||||
}}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
verafx {
|
||||
; Partial Vera FX support:
|
||||
; - fast 32 bit cached writes (clear, copy)
|
||||
; - transparent write setting
|
||||
; - hardware 16 bits multiplications
|
||||
; - hardware accelerated line drawing (8 bpp screen mode only!)
|
||||
;
|
||||
; Docs:
|
||||
; https://github.com/X16Community/x16-docs/blob/fb63156cca2d6de98be0577aacbe4ddef458f896/X16%20Reference%20-%2010%20-%20VERA%20FX%20Reference.md
|
||||
; https://docs.google.com/document/d/1q34uWOiM3Be2pnaHRVgSdHySI-qsiQWPTo_gfE54PTg
|
||||
|
||||
%option no_symbol_prefixing, ignore_unused
|
||||
|
||||
sub available() -> bool {
|
||||
; returns true if Vera FX is available (Vera V0.3.1 or later), false if not.
|
||||
cx16.r0L = vera.VERA_CTRL
|
||||
cx16.r0H = 0
|
||||
vera.VERA_CTRL = $7e
|
||||
if vera.VERA_DC_VER0 == $56 {
|
||||
; Vera version number is valid. Vera fx is available on Vera version 0.3.1 and later.
|
||||
if vera.VERA_DC_VER1>0
|
||||
cx16.r0H = 1
|
||||
else
|
||||
cx16.r0H = mkword(vera.VERA_DC_VER2, vera.VERA_DC_VER3) >= $0301 as ubyte
|
||||
}
|
||||
vera.VERA_CTRL = cx16.r0L
|
||||
return cx16.r0H as bool
|
||||
}
|
||||
|
||||
sub clear(ubyte vbank, uword vaddr, ubyte data, uword num_longwords) {
|
||||
; use cached 4-byte write to quickly clear a portion of the video memory to a given byte value
|
||||
; this routine is around 3 times faster as gfx_hires/gfx_lores.clear_screen()
|
||||
vera.VERA_CTRL = 0
|
||||
vera.VERA_ADDR_H = vbank | %00110000 ; 4-byte increment
|
||||
vera.VERA_ADDR_M = msb(vaddr)
|
||||
vera.VERA_ADDR_L = lsb(vaddr)
|
||||
vera.VERA_CTRL = 6<<1 ; dcsel = 6, fill the 32 bits cache
|
||||
vera.VERA_FX_CACHE_L = data
|
||||
vera.VERA_FX_CACHE_M = data
|
||||
vera.VERA_FX_CACHE_H = data
|
||||
vera.VERA_FX_CACHE_U = data
|
||||
vera.VERA_CTRL = 2<<1 ; dcsel = 2
|
||||
vera.VERA_FX_MULT = 0
|
||||
vera.VERA_FX_CTRL = %01000000 ; cache write enable
|
||||
|
||||
cx16.r0 = num_longwords>>3
|
||||
if cx16.r0H==0 {
|
||||
repeat cx16.r0L {
|
||||
unroll 8 vera.VERA_DATA0=0 ; write 8*4 bytes at a time, unrolled
|
||||
}
|
||||
} else {
|
||||
repeat cx16.r0 {
|
||||
unroll 8 vera.VERA_DATA0=0 ; write 8*4 bytes at a time, unrolled
|
||||
}
|
||||
}
|
||||
|
||||
repeat lsb(num_longwords) & 7 {
|
||||
vera.VERA_DATA0=0 ; write 4 bytes at a time (remaining longs)
|
||||
}
|
||||
|
||||
vera.VERA_FX_CTRL = 0 ; cache write disable
|
||||
vera.VERA_CTRL = 0
|
||||
}
|
||||
|
||||
sub copy(ubyte srcbank, uword srcaddr, ubyte tgtbank, uword tgtaddr, uword num_longwords) {
|
||||
; use cached 4-byte writes to quickly copy a portion of the video memory to somewhere else
|
||||
; this routine is about 50% faster as a plain byte-by-byte copy
|
||||
vera.VERA_CTRL = 1
|
||||
vera.VERA_ADDR_H = srcbank | %00010000 ; source: 1-byte increment
|
||||
vera.VERA_ADDR_M = msb(srcaddr)
|
||||
vera.VERA_ADDR_L = lsb(srcaddr)
|
||||
vera.VERA_CTRL = 0
|
||||
vera.VERA_ADDR_H = tgtbank | %00110000 ; target: 4-byte increment
|
||||
vera.VERA_ADDR_M = msb(tgtaddr)
|
||||
vera.VERA_ADDR_L = lsb(tgtaddr)
|
||||
vera.VERA_CTRL = 2<<1 ; dcsel = 2
|
||||
vera.VERA_FX_MULT = 0
|
||||
vera.VERA_FX_CTRL = %01100000 ; cache write enable + cache fill enable
|
||||
|
||||
cx16.r0 = num_longwords>>1
|
||||
|
||||
if cx16.r0H==0 {
|
||||
repeat cx16.r0L {
|
||||
unroll 2 %asm {{
|
||||
lda vera.VERA_DATA1
|
||||
lda vera.VERA_DATA1
|
||||
lda vera.VERA_DATA1
|
||||
lda vera.VERA_DATA1
|
||||
lda #0
|
||||
sta vera.VERA_DATA0
|
||||
}}
|
||||
}
|
||||
} else {
|
||||
repeat cx16.r0 {
|
||||
unroll 2 %asm {{
|
||||
lda vera.VERA_DATA1
|
||||
lda vera.VERA_DATA1
|
||||
lda vera.VERA_DATA1
|
||||
lda vera.VERA_DATA1
|
||||
lda #0
|
||||
sta vera.VERA_DATA0
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
if lsb(num_longwords) & 1 == 1 {
|
||||
%asm {{
|
||||
lda vera.VERA_DATA1
|
||||
lda vera.VERA_DATA1
|
||||
lda vera.VERA_DATA1
|
||||
lda vera.VERA_DATA1
|
||||
lda #0
|
||||
sta vera.VERA_DATA0
|
||||
}}
|
||||
}
|
||||
|
||||
vera.VERA_FX_CTRL = 0 ; cache write disable
|
||||
vera.VERA_CTRL = 0
|
||||
}
|
||||
|
||||
|
||||
asmsub mult16(uword value1 @R0, uword value2 @R1) clobbers(X) -> uword @AY {
|
||||
; Returns the lower 16 bits unsigned result of R0*R1 in AY
|
||||
; Note: only the lower 16 bits! (the upper 16 bits are not valid for unsigned word multiplications, only for signed)
|
||||
; Verafx doesn't support unsigned values like this for full 32 bit result.
|
||||
; Note: clobbers VRAM $1f9bc - $1f9bf (inclusive)
|
||||
%asm {{
|
||||
jmp muls16
|
||||
}}
|
||||
}
|
||||
|
||||
asmsub muls16(word value1 @R0, word value2 @R1) clobbers(X) -> word @AY {
|
||||
; Returns just the lower 16 bits signed result of the multiplication in cx16.AY.
|
||||
; Note: clobbers R0, R1, and VRAM $1f9bc - $1f9bf (inclusive)
|
||||
%asm {{
|
||||
jsr muls
|
||||
lda cx16.r0L
|
||||
ldy cx16.r0H
|
||||
rts
|
||||
}}
|
||||
}
|
||||
|
||||
|
||||
asmsub muls(word value1 @R0, word value2 @R1) clobbers(X) -> long @R0R1 {
|
||||
; Returns the 32 bits signed result in R0:R1 (lower word, upper word).
|
||||
; Vera Fx multiplication support only works on signed values!
|
||||
; Note: clobbers VRAM $1f9bc - $1f9bf (inclusive)
|
||||
%asm {{
|
||||
lda #(2 << 1)
|
||||
sta vera.VERA_CTRL ; $9F25
|
||||
lda #0
|
||||
sta vera.VERA_FX_CTRL ; $9F29 (mainly to reset Addr1 Mode to 0)
|
||||
lda #%00010000
|
||||
sta vera.VERA_FX_MULT ; $9F2C
|
||||
lda #(6 << 1)
|
||||
sta vera.VERA_CTRL ; $9F25
|
||||
lda cx16.r0
|
||||
sta vera.VERA_FX_CACHE_L ; $9F29
|
||||
lda cx16.r0+1
|
||||
sta vera.VERA_FX_CACHE_M ; $9F2A
|
||||
lda cx16.r1
|
||||
sta vera.VERA_FX_CACHE_H ; $9F2B
|
||||
lda cx16.r1+1
|
||||
sta vera.VERA_FX_CACHE_U ; $9F2C
|
||||
lda vera.VERA_FX_ACCUM_RESET ; $9F29 (DCSEL=6)
|
||||
|
||||
; Set the ADDR0 pointer to $1f9bc and write our multiplication result there
|
||||
; (these are the 4 bytes just before the PSG registers start)
|
||||
lda #(2 << 1)
|
||||
sta vera.VERA_CTRL
|
||||
lda #%01000000 ; Cache Write Enable
|
||||
sta vera.VERA_FX_CTRL
|
||||
lda #$bc
|
||||
sta vera.VERA_ADDR_L
|
||||
lda #$f9
|
||||
sta vera.VERA_ADDR_M
|
||||
lda #$01
|
||||
sta vera.VERA_ADDR_H ; no increment
|
||||
lda #0
|
||||
sta vera.VERA_DATA0 ; multiply and write out result
|
||||
lda #%00010001 ; $01 with Increment 1
|
||||
sta vera.VERA_ADDR_H ; so we can read out the result
|
||||
lda vera.VERA_DATA0 ; store the lower 16 bits of the result in R0
|
||||
ldy vera.VERA_DATA0
|
||||
sta cx16.r0L
|
||||
sty cx16.r0H
|
||||
lda vera.VERA_DATA0 ; store the upper 16 bits of the result in R1
|
||||
ldy vera.VERA_DATA0 ; store the upper 16 bits of the result in R1
|
||||
sta cx16.r1L
|
||||
sty cx16.r1H
|
||||
lda #0
|
||||
sta vera.VERA_FX_CTRL ; Cache write disable
|
||||
sta vera.VERA_FX_MULT ; $9F2C reset multiply bit
|
||||
sta vera.VERA_CTRL ; reset DCSEL
|
||||
rts
|
||||
}}
|
||||
}
|
||||
|
||||
sub line(uword x1, ubyte y1, uword x2, ubyte y2, ubyte color) {
|
||||
; Use the Vera FX line draw helper to draw a line very fast in a 320x240 256 color (8 bpp) bitmap screen
|
||||
; (the default cx16 screen mode 128, as used by the gfx_lores module, with the bitmap at vram address 0).
|
||||
; WARNING: ONLY WORKS IN 8 BPP SCREEN MODE! The helper has a hardware bug in 4 bpp mode.
|
||||
; No bounds checking or clipping is done, all coordinates must lie within the screen (0..319, 0..239).
|
||||
; Also resets the address increments of DATA0 and DATA1 to 0 afterwards.
|
||||
; The line is always drawn from top to bottom (y1<=y2 after sorting), this avoids the negative
|
||||
; (decrement) y-increments that the helper handles poorly. x can go either left or right.
|
||||
ubyte @zp octant
|
||||
uword @zp dx
|
||||
uword @zp dy
|
||||
if y1>y2 {
|
||||
cx16.r0 = x1
|
||||
x1 = x2
|
||||
x2 = cx16.r0
|
||||
octant = y1
|
||||
y1 = y2
|
||||
y2 = octant
|
||||
}
|
||||
dy = y2
|
||||
dy -= y1
|
||||
uword @zp length
|
||||
if x2>=x1 {
|
||||
dx = x2-x1
|
||||
length = dx
|
||||
octant = 0 ; x goes right
|
||||
} else {
|
||||
dx = x1-x2
|
||||
length = dx
|
||||
octant = 1 ; x goes left
|
||||
}
|
||||
if dy>length {
|
||||
octant |= 2
|
||||
length = dy
|
||||
dy = dx
|
||||
}
|
||||
; slope in 0.9 fixed point format for the FX increment register (1.0 = $200), rounded to nearest.
|
||||
; Computed as (dy/length in 0.8 fixed point) << 1, which stays within 16 bits:
|
||||
; dy<=239 so dy<<8 <= 61240, and dy<=length so the quotient is <=256, doubled to <=512.
|
||||
uword slope = 0
|
||||
if length!=0 {
|
||||
slope = (((dy << 8) + (length>>1)) / length) << 1
|
||||
}
|
||||
length++
|
||||
ubyte remainder_pixels = lsb(length) & 7
|
||||
ubyte full_octets = lsb(length>>3)
|
||||
|
||||
; 4 "octants" (y1<=y2 always after the sorting above, so the line always goes down):
|
||||
; octant 0 = right/down, shallow slope (dx>=dy): always step +1 in x, sometimes step +320 in y
|
||||
; octant 1 = left/down, shallow slope: always step -1 in x, sometimes step +320 in y
|
||||
; octant 2 = right/down, steep slope (dy>dx): always step +320 in y, sometimes step +1 in x
|
||||
; octant 3 = left/down, steep slope: always step +320 in y, sometimes step -1 in x
|
||||
; address increment values: +1 = $10, -1 = $18 (decrement), +320 = $e0
|
||||
ubyte[4] @shared always_incr_table = [ $10, $18, $e0, $e0 ]
|
||||
ubyte[4] @shared sometimes_incr_table = [ $e0, $e0, $10, $18 ]
|
||||
|
||||
%asm {{
|
||||
; set up the FX line draw helper and the start address in ADDR1
|
||||
lda #(2<<1)
|
||||
sta vera.VERA_CTRL ; dcsel = 2
|
||||
lda #%00000001
|
||||
sta vera.VERA_FX_CTRL ; addr1 mode = line draw helper (8 bpp)
|
||||
lda #(3<<1)
|
||||
sta vera.VERA_CTRL ; dcsel = 3
|
||||
lda slope
|
||||
sta vera.VERA_FX_X_INCR ; (writing X_INCR also centers the subpixel position and resets overflow)
|
||||
lda slope+1
|
||||
sta vera.VERA_FX_X_INCR+1
|
||||
; ADDR0 provides the 'sometimes' increment for the helper
|
||||
lda #0
|
||||
sta vera.VERA_CTRL ; addrsel = 0
|
||||
ldx octant
|
||||
lda sometimes_incr_table,x
|
||||
sta vera.VERA_ADDR_H
|
||||
; ADDR1 = start pixel, gets the 'always' increment
|
||||
lda #1
|
||||
sta vera.VERA_CTRL ; addrsel = 1 (bit 0)
|
||||
lda x1
|
||||
sta vera.VERA_ADDR_L
|
||||
lda x1+1
|
||||
sta vera.VERA_ADDR_M
|
||||
lda always_incr_table,x
|
||||
sta vera.VERA_ADDR_H
|
||||
|
||||
; add the y-offset to the start address in ADDR1
|
||||
ldy y1
|
||||
lda vera.VERA_ADDR_L
|
||||
clc
|
||||
adc times320_lo,y
|
||||
sta vera.VERA_ADDR_L
|
||||
lda vera.VERA_ADDR_M
|
||||
adc times320_mid,y
|
||||
sta vera.VERA_ADDR_M
|
||||
lda vera.VERA_ADDR_H
|
||||
and #$01
|
||||
adc times320_hi,y
|
||||
sta P8ZP_SCRATCH_B1
|
||||
lda vera.VERA_ADDR_H
|
||||
and #$f8
|
||||
ora P8ZP_SCRATCH_B1
|
||||
sta vera.VERA_ADDR_H
|
||||
|
||||
; draw the line: first the remainder pixels one at a time, then unrolled 8 pixels at a time
|
||||
ldy remainder_pixels
|
||||
beq +
|
||||
lda color
|
||||
- sta vera.VERA_DATA1
|
||||
dey
|
||||
bne -
|
||||
+ ldy full_octets
|
||||
beq _done
|
||||
lda color
|
||||
- sta vera.VERA_DATA1
|
||||
sta vera.VERA_DATA1
|
||||
sta vera.VERA_DATA1
|
||||
sta vera.VERA_DATA1
|
||||
sta vera.VERA_DATA1
|
||||
sta vera.VERA_DATA1
|
||||
sta vera.VERA_DATA1
|
||||
sta vera.VERA_DATA1
|
||||
dey
|
||||
bne -
|
||||
_done
|
||||
; reset the FX registers back to normal
|
||||
lda #(2<<1)
|
||||
sta vera.VERA_CTRL ; dcsel = 2
|
||||
ldx #0
|
||||
stx vera.VERA_FX_CTRL ; addr1 mode = normal again
|
||||
lda #1
|
||||
sta vera.VERA_CTRL ; addrsel = 1 (bit 0)
|
||||
stx vera.VERA_ADDR_H ; reset ADDR1 (DATA1) address increment
|
||||
stx vera.VERA_CTRL ; addrsel = 0
|
||||
stx vera.VERA_ADDR_H ; reset ADDR0 (DATA0) address increment
|
||||
rts
|
||||
|
||||
; multiplication by 320 lookup table (used to add the y-offset to the start address above)
|
||||
times320 := 320*range(240)
|
||||
times320_lo .byte <times320
|
||||
times320_mid .byte >times320
|
||||
times320_hi .byte `times320
|
||||
}}
|
||||
}
|
||||
|
||||
sub transparency(bool enable) {
|
||||
; Set transparent write mode for VeraFX cached writes and also for normal writes to DATA0/DATA.
|
||||
; If enabled, pixels with value 0 do not modify VRAM when written (so they are "transparent")
|
||||
vera.VERA_CTRL = 2<<1 ; dcsel = 2
|
||||
if enable
|
||||
vera.VERA_FX_CTRL |= %10000000
|
||||
else
|
||||
vera.VERA_FX_CTRL &= %01111111
|
||||
vera.VERA_CTRL = 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,878 @@
|
||||
%import syslib
|
||||
%import buffers
|
||||
%import vera
|
||||
|
||||
%option ignore_unused
|
||||
|
||||
vera_gfx {
|
||||
; optimized graphics routines for just the single screen mode: lores 320*240, 256c (8bpp)
|
||||
; bitmap image needs to start at VRAM address $00000.
|
||||
|
||||
%option ignore_unused
|
||||
|
||||
const uword WIDTH = 320
|
||||
const ubyte HEIGHT = 240
|
||||
|
||||
sub init() {
|
||||
; 320x240 8bpp bitmap on layer 1, bitmap at vram $00000, set via direct vera register pokes
|
||||
vera.VERA_CTRL = 0
|
||||
vera.VERA_DC_VIDEO = (vera.VERA_DC_VIDEO & %11001100) | %00100001 ; layer 1 only, force VGA output (a vera reset leaves output mode 0=disabled)
|
||||
vera.VERA_DC_HSCALE = 64
|
||||
vera.VERA_DC_VSCALE = 64
|
||||
vera.VERA_L1_CONFIG = %00000111 ; bitmap mode, 8 bpp
|
||||
vera.VERA_L1_MAPBASE = 0
|
||||
vera.VERA_L1_TILEBASE = 0 ; bitmap base $00000
|
||||
vera.VERA_L1_HSCROLL = 0
|
||||
vera.VERA_L1_VSCROLL = 0
|
||||
|
||||
clear_screen(0)
|
||||
|
||||
copy_petscii_charset()
|
||||
|
||||
drawmode_eor(false)
|
||||
}
|
||||
|
||||
sub copy_petscii_charset() {
|
||||
; copy the c64 petscii charset (all 256 characters) from the CHARGEN rom to Vera VRAM at $1f000 - $1f800
|
||||
; this makes text() work; the charset is stored at vram bank 1, address $f000 (= $1f000)
|
||||
; (a temporary ram copy must be made first, because the vera registers are inaccessible while the chargen rom is mapped in)
|
||||
uword chargen_copy = memory("vera_chargen_copy", 256*8, 0)
|
||||
sys.set_irqd()
|
||||
c64.banks(%011) ; enable CHAREN, so the character rom is visible at $d000
|
||||
sys.memcopy($d000, chargen_copy, 256*8)
|
||||
c64.banks(%111) ; enable I/O (and thus the vera) again
|
||||
sys.clear_irqd()
|
||||
|
||||
vera.vaddr(1, $f000, 0, 1) ; set up the vera data0 address with auto increment of 1 (so we can just write data0)
|
||||
repeat 256*8 {
|
||||
vera.VERA_DATA0 = @(chargen_copy)
|
||||
chargen_copy++
|
||||
}
|
||||
}
|
||||
|
||||
sub drawmode_eor(bool enabled) {
|
||||
; with EOR drawing mode you can have non destructive drawing (2*EOR=restore original)
|
||||
eor_mode = enabled
|
||||
}
|
||||
|
||||
private bool eor_mode
|
||||
|
||||
sub clear_screen(ubyte color) {
|
||||
if verafx.available() {
|
||||
; use verafx cache writes to quickly clear the screen
|
||||
const ubyte vbank = 0
|
||||
const uword vaddr = 0
|
||||
vera.VERA_CTRL = 0
|
||||
vera.VERA_ADDR_H = vbank | %00110000 ; 4-byte increment
|
||||
vera.VERA_ADDR_M = msb(vaddr)
|
||||
vera.VERA_ADDR_L = lsb(vaddr)
|
||||
vera.VERA_CTRL = 6<<1 ; dcsel = 6, fill the 32 bits cache
|
||||
vera.VERA_FX_CACHE_L = color
|
||||
vera.VERA_FX_CACHE_M = color
|
||||
vera.VERA_FX_CACHE_H = color
|
||||
vera.VERA_FX_CACHE_U = color
|
||||
vera.VERA_CTRL = 2<<1 ; dcsel = 2
|
||||
vera.VERA_FX_MULT = 0
|
||||
vera.VERA_FX_CTRL = %01000000 ; cache write enable
|
||||
repeat 320/4/4 {
|
||||
%asm {{
|
||||
ldy #240
|
||||
lda #0
|
||||
- sta vera.VERA_DATA0
|
||||
sta vera.VERA_DATA0
|
||||
sta vera.VERA_DATA0
|
||||
sta vera.VERA_DATA0
|
||||
dey
|
||||
bne -
|
||||
}}
|
||||
}
|
||||
vera.VERA_FX_CTRL = 0 ; cache write disable
|
||||
vera.VERA_CTRL = 0
|
||||
return
|
||||
}
|
||||
; fallback to cpu clear
|
||||
vera.VERA_CTRL=0
|
||||
vera.VERA_ADDR=0
|
||||
vera.VERA_ADDR_H = 1<<4 ; 1 pixel auto increment
|
||||
repeat HEIGHT {
|
||||
%asm {{
|
||||
lda p8v_color
|
||||
ldy #p8c_WIDTH/8
|
||||
- .rept 8
|
||||
sta vera.VERA_DATA0
|
||||
.endrept
|
||||
dey
|
||||
bne -
|
||||
}}
|
||||
}
|
||||
vera.VERA_ADDR=0
|
||||
vera.VERA_ADDR_H = 0
|
||||
}
|
||||
|
||||
sub rect(uword xx, ubyte yy, uword rwidth, ubyte rheight, ubyte color) {
|
||||
if rwidth==0 or rheight==0
|
||||
return
|
||||
horizontal_line(xx, yy, rwidth, color)
|
||||
if rheight==1
|
||||
return
|
||||
horizontal_line(xx, yy+rheight-1, rwidth, color)
|
||||
vertical_line(xx, yy+1, rheight-2, color)
|
||||
if rwidth==1
|
||||
return
|
||||
vertical_line(xx+rwidth-1, yy+1, rheight-2, color)
|
||||
}
|
||||
|
||||
sub safe_rect(uword xx, ubyte yy, uword rwidth, ubyte rheight, ubyte color) {
|
||||
; does bounds checking and clipping
|
||||
safe_horizontal_line(xx, yy, rwidth, color)
|
||||
if rheight==1
|
||||
return
|
||||
uword bottomyy = yy as uword + rheight -1
|
||||
if bottomyy<HEIGHT
|
||||
safe_horizontal_line(xx, lsb(bottomyy), rwidth, color)
|
||||
safe_vertical_line(xx, yy+1, rheight-2, color)
|
||||
if rwidth==1
|
||||
return
|
||||
safe_vertical_line(xx+rwidth-1, yy+1, rheight-2, color)
|
||||
}
|
||||
|
||||
sub fillrect(uword xx, ubyte yy, uword rwidth, ubyte rheight, ubyte color) {
|
||||
; Draw a filled rectangle of the given size and color.
|
||||
; To fill the whole screen, use clear_screen(color) instead - it is much faster.
|
||||
if rwidth==0
|
||||
return
|
||||
repeat rheight {
|
||||
horizontal_line(xx, yy, rwidth, color)
|
||||
yy++
|
||||
}
|
||||
}
|
||||
|
||||
sub safe_fillrect(uword xx, ubyte yy, uword rwidth, ubyte rheight, ubyte color) {
|
||||
; Draw a filled rectangle of the given size and color.
|
||||
; To fill the whole screen, use clear_screen(color) instead - it is much faster.
|
||||
; This safe version does bounds checking and clipping.
|
||||
if xx>=WIDTH or yy>=HEIGHT
|
||||
return
|
||||
if msb(xx)&$80!=0 {
|
||||
rwidth += xx
|
||||
xx = 0
|
||||
}
|
||||
if xx>=WIDTH
|
||||
return
|
||||
if xx+rwidth>WIDTH
|
||||
rwidth = WIDTH-xx
|
||||
if rwidth>WIDTH
|
||||
return
|
||||
|
||||
if yy as uword + rheight > HEIGHT
|
||||
rheight = HEIGHT-yy
|
||||
if rheight>HEIGHT
|
||||
return
|
||||
|
||||
repeat rheight {
|
||||
horizontal_line(xx, yy, rwidth, color)
|
||||
yy++
|
||||
}
|
||||
}
|
||||
|
||||
sub horizontal_line(uword xx, ubyte yy, uword length, ubyte color) {
|
||||
if length==0
|
||||
return
|
||||
position(xx, yy)
|
||||
; set vera auto-increment to 1 pixel
|
||||
vera.VERA_ADDR_H = vera.VERA_ADDR_H & %00000111 | (1<<4)
|
||||
if eor_mode {
|
||||
vera.vaddr_clone(0) ; also setup port 1, for reading
|
||||
%asm {{
|
||||
ldx p8v_length+1
|
||||
beq +
|
||||
ldy #0
|
||||
- lda p8v_color
|
||||
eor vera.VERA_DATA1
|
||||
sta vera.VERA_DATA0
|
||||
iny
|
||||
bne -
|
||||
dex
|
||||
bne -
|
||||
+ ldy p8v_length ; remaining
|
||||
beq +
|
||||
- lda p8v_color
|
||||
eor vera.VERA_DATA1
|
||||
sta vera.VERA_DATA0
|
||||
dey
|
||||
bne -
|
||||
+
|
||||
}}
|
||||
} else {
|
||||
%asm {{
|
||||
lda p8v_color
|
||||
ldx p8v_length+1
|
||||
beq +
|
||||
ldy #0
|
||||
- sta vera.VERA_DATA0
|
||||
iny
|
||||
bne -
|
||||
dex
|
||||
bne -
|
||||
+ ldy p8v_length ; remaining
|
||||
beq +
|
||||
- sta vera.VERA_DATA0
|
||||
dey
|
||||
bne -
|
||||
+
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
sub safe_horizontal_line(uword xx, ubyte yy, uword length, ubyte color) {
|
||||
; does bounds checking and clipping
|
||||
if yy>=HEIGHT
|
||||
return
|
||||
if msb(xx)&$80!=0 {
|
||||
length += xx
|
||||
xx = 0
|
||||
}
|
||||
if xx>=WIDTH
|
||||
return
|
||||
if xx+length>WIDTH
|
||||
length = WIDTH-xx
|
||||
if length>WIDTH
|
||||
return
|
||||
|
||||
horizontal_line(xx, yy, length, color)
|
||||
}
|
||||
|
||||
sub vertical_line(uword xx, ubyte yy, ubyte lheight, ubyte color) {
|
||||
if lheight==0
|
||||
return
|
||||
position(xx, yy)
|
||||
; set vera auto-increment to 320 pixel increment (=next line)
|
||||
vera.VERA_ADDR_H = vera.VERA_ADDR_H & %00000111 | (14<<4)
|
||||
if eor_mode {
|
||||
vera.vaddr_clone(0) ; also setup port 1, for reading
|
||||
%asm {{
|
||||
ldy p8v_lheight
|
||||
beq +
|
||||
- lda p8v_color
|
||||
eor vera.VERA_DATA1
|
||||
sta vera.VERA_DATA0
|
||||
dey
|
||||
bne -
|
||||
+
|
||||
}}
|
||||
} else {
|
||||
%asm {{
|
||||
ldy p8v_lheight
|
||||
lda p8v_color
|
||||
- sta vera.VERA_DATA0
|
||||
dey
|
||||
bne -
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
sub safe_vertical_line(uword xx, ubyte yy, ubyte lheight, ubyte color) {
|
||||
; does bounds checking and clipping
|
||||
if yy>=HEIGHT
|
||||
return
|
||||
if msb(xx)&$80!=0 or xx>=WIDTH
|
||||
return
|
||||
if yy as uword + lheight > HEIGHT
|
||||
lheight = HEIGHT-yy
|
||||
if lheight>HEIGHT
|
||||
return
|
||||
|
||||
vertical_line(xx, yy, lheight, color)
|
||||
}
|
||||
|
||||
sub line(uword x1, ubyte y1, uword x2, ubyte y2, ubyte color) {
|
||||
; Bresenham algorithm.
|
||||
; This code special-cases various quadrant loops to allow simple ++ and -- operations.
|
||||
; NOTE: this is about twice as fast as the kernal routine GRAPH_draw_line
|
||||
; it trades memory for speed (uses inline plot routine and multiplication lookup tables)
|
||||
;
|
||||
; NOTE: is currently still a regular 6502 routine, could likely be made much faster with the VeraFX line helper.
|
||||
|
||||
cx16.r4L = color ; cache color in r4L for internal_line_plot
|
||||
cx16.r3L = y2 ; ensure zeropage
|
||||
cx16.r1L = y1 ; ensure zeropage
|
||||
|
||||
if cx16.r1L > cx16.r3L {
|
||||
; make sure dy is always positive to have only 4 instead of 8 special cases
|
||||
cx16.r0 = x1
|
||||
x1 = x2
|
||||
x2 = cx16.r0
|
||||
cx16.r0L = cx16.r1L
|
||||
cx16.r1L = cx16.r3L
|
||||
cx16.r3L = cx16.r0L
|
||||
}
|
||||
word @zp dx = x2 as word
|
||||
word @zp dy = cx16.r3L
|
||||
dx -= x1
|
||||
dy -= cx16.r1L
|
||||
|
||||
if dx==0 {
|
||||
vertical_line(x1, cx16.r1L, lsb(dy)+1, color)
|
||||
return
|
||||
}
|
||||
if dy==0 {
|
||||
if x1>x2
|
||||
x1=x2
|
||||
horizontal_line(x1, cx16.r1L, abs(dx) as uword +1, color)
|
||||
return
|
||||
}
|
||||
|
||||
bool positive_ix = true
|
||||
if dx < 0 {
|
||||
dx = -dx
|
||||
positive_ix = false
|
||||
}
|
||||
word @zp dx2 = dx*2
|
||||
word @zp dy2 = dy*2
|
||||
word @zp d ; error term (initialized below based on shallow/steep)
|
||||
|
||||
cx16.r0 = x1 ; ensure zeropage
|
||||
cx16.r2 = x2 ; ensure zeropage
|
||||
|
||||
vera.VERA_CTRL = 0
|
||||
if dx >= dy {
|
||||
d = dx >> 1 ; Initialize error to DX/2 for shallow lines
|
||||
if positive_ix {
|
||||
repeat {
|
||||
internal_line_plot()
|
||||
if cx16.r0==cx16.r2
|
||||
return
|
||||
cx16.r0++
|
||||
d += dy2
|
||||
if d > dx {
|
||||
cx16.r1L++
|
||||
d -= dx2
|
||||
}
|
||||
}
|
||||
} else {
|
||||
repeat {
|
||||
internal_line_plot()
|
||||
if cx16.r0==cx16.r2
|
||||
return
|
||||
cx16.r0--
|
||||
d += dy2
|
||||
if d > dx {
|
||||
cx16.r1L++
|
||||
d -= dx2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
d = dy >> 1 ; Initialize error to DY/2 for steep lines
|
||||
if positive_ix {
|
||||
repeat {
|
||||
internal_line_plot()
|
||||
if cx16.r1L == cx16.r3L
|
||||
return
|
||||
cx16.r1L++
|
||||
d += dx2
|
||||
if d > dy {
|
||||
cx16.r0++
|
||||
d -= dy2
|
||||
}
|
||||
}
|
||||
} else {
|
||||
repeat {
|
||||
internal_line_plot()
|
||||
if cx16.r1L == cx16.r3L
|
||||
return
|
||||
cx16.r1L++
|
||||
d += dx2
|
||||
if d > dy {
|
||||
cx16.r0--
|
||||
d -= dy2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private asmsub internal_line_plot() {
|
||||
; Internal plot routine for line algorithm.
|
||||
; Uses: x in cx16.r0, y in cx16.r1L, color in cx16.r4L
|
||||
; Checks eor_mode flag for XOR vs normal drawing.
|
||||
%asm {{
|
||||
ldy cx16.r1L
|
||||
clc
|
||||
lda times320_lo,y
|
||||
adc cx16.r0L
|
||||
sta vera.VERA_ADDR_L
|
||||
lda times320_mid,y
|
||||
adc cx16.r0H
|
||||
sta vera.VERA_ADDR_M
|
||||
lda #0
|
||||
adc times320_hi,y
|
||||
sta vera.VERA_ADDR_H
|
||||
|
||||
lda p8v_eor_mode
|
||||
bne +
|
||||
lda cx16.r4L
|
||||
sta vera.VERA_DATA0
|
||||
rts
|
||||
+ lda cx16.r4L
|
||||
eor vera.VERA_DATA0
|
||||
sta vera.VERA_DATA0
|
||||
rts
|
||||
}}
|
||||
}
|
||||
|
||||
sub circle(uword @zp xcenter, ubyte @zp ycenter, ubyte radius, ubyte color) {
|
||||
; Warning: NO BOUNDS CHECKS. Make sure circle fits in the screen.
|
||||
; Midpoint algorithm.
|
||||
if radius==0
|
||||
return
|
||||
|
||||
ubyte @zp xx = radius
|
||||
ubyte @zp yy = 0
|
||||
word @zp decisionOver2 = (1 as word)-xx
|
||||
; R14 = internal plot X
|
||||
; R15 = internal plot Y
|
||||
|
||||
while xx>=yy {
|
||||
cx16.r14 = xcenter + xx
|
||||
cx16.r15 = ycenter + yy
|
||||
plotq()
|
||||
cx16.r14 = xcenter - xx
|
||||
plotq()
|
||||
cx16.r14 = xcenter + xx
|
||||
cx16.r15 = ycenter - yy
|
||||
plotq()
|
||||
cx16.r14 = xcenter - xx
|
||||
plotq()
|
||||
cx16.r14 = xcenter + yy
|
||||
cx16.r15 = ycenter + xx
|
||||
plotq()
|
||||
cx16.r14 = xcenter - yy
|
||||
plotq()
|
||||
cx16.r14 = xcenter + yy
|
||||
cx16.r15 = ycenter - xx
|
||||
plotq()
|
||||
cx16.r14 = xcenter - yy
|
||||
plotq()
|
||||
|
||||
yy++
|
||||
if decisionOver2>=0 {
|
||||
xx--
|
||||
decisionOver2 -= xx*$0002
|
||||
}
|
||||
decisionOver2 += yy*$0002
|
||||
decisionOver2++
|
||||
}
|
||||
|
||||
sub plotq() {
|
||||
; cx16.r14 = x, cx16.r15 = y, color=color.
|
||||
plot(cx16.r14, cx16.r15L, color)
|
||||
}
|
||||
}
|
||||
|
||||
sub safe_circle(uword @zp xcenter, uword @zp ycenter, ubyte radius, ubyte color) {
|
||||
; This version does bounds checks and clipping, but is a lot slower.
|
||||
; Midpoint algorithm.
|
||||
if radius==0
|
||||
return
|
||||
|
||||
ubyte @zp xx = radius
|
||||
ubyte @zp yy = 0
|
||||
word @zp decisionOver2 = (1 as word)-xx
|
||||
; R14 = internal plot X
|
||||
; R15 = internal plot Y
|
||||
|
||||
while xx>=yy {
|
||||
cx16.r14 = xcenter + xx
|
||||
cx16.r15 = ycenter + yy
|
||||
plotq()
|
||||
cx16.r14 = xcenter - xx
|
||||
plotq()
|
||||
cx16.r14 = xcenter + xx
|
||||
cx16.r15 = ycenter - yy
|
||||
plotq()
|
||||
cx16.r14 = xcenter - xx
|
||||
plotq()
|
||||
cx16.r14 = xcenter + yy
|
||||
cx16.r15 = ycenter + xx
|
||||
plotq()
|
||||
cx16.r14 = xcenter - yy
|
||||
plotq()
|
||||
cx16.r14 = xcenter + yy
|
||||
cx16.r15 = ycenter - xx
|
||||
plotq()
|
||||
cx16.r14 = xcenter - yy
|
||||
plotq()
|
||||
|
||||
yy++
|
||||
if decisionOver2>=0 {
|
||||
xx--
|
||||
decisionOver2 -= xx*$0002
|
||||
}
|
||||
decisionOver2 += yy*$0002
|
||||
decisionOver2++
|
||||
}
|
||||
|
||||
sub plotq() {
|
||||
; cx16.r14 = x, cx16.r15 = y, color=color.
|
||||
if cx16.r15 < HEIGHT
|
||||
safe_plot(cx16.r14, cx16.r15L, color)
|
||||
}
|
||||
}
|
||||
|
||||
sub disc(uword @zp xcenter, ubyte @zp ycenter, ubyte @zp radius, ubyte color) {
|
||||
; Warning: NO BOUNDS CHECKS. Make sure circle fits in the screen.
|
||||
; Midpoint algorithm, filled
|
||||
if radius==0
|
||||
return
|
||||
ubyte @zp yy = 0
|
||||
word @zp decisionOver2 = (1 as word)-radius
|
||||
ubyte pendingRadius
|
||||
ubyte pendingWidth
|
||||
bool hasPending = false
|
||||
while radius>=yy {
|
||||
horizontal_line(xcenter-radius, ycenter+yy, radius*$0002+1, color)
|
||||
horizontal_line(xcenter-radius, ycenter-yy, radius*$0002+1, color)
|
||||
if hasPending and pendingRadius != radius {
|
||||
if pendingRadius != pendingWidth {
|
||||
horizontal_line(xcenter-pendingWidth, ycenter+pendingRadius, pendingWidth*$0002+1, color)
|
||||
horizontal_line(xcenter-pendingWidth, ycenter-pendingRadius, pendingWidth*$0002+1, color)
|
||||
}
|
||||
hasPending = false
|
||||
}
|
||||
if not hasPending {
|
||||
pendingRadius = radius
|
||||
hasPending = true
|
||||
}
|
||||
pendingWidth = yy
|
||||
yy++
|
||||
if decisionOver2>=0 {
|
||||
radius--
|
||||
decisionOver2 -= radius*$0002
|
||||
}
|
||||
decisionOver2 += yy*$0002
|
||||
decisionOver2++
|
||||
}
|
||||
if hasPending and pendingRadius != pendingWidth {
|
||||
horizontal_line(xcenter-pendingWidth, ycenter+pendingRadius, pendingWidth*$0002+1, color)
|
||||
horizontal_line(xcenter-pendingWidth, ycenter-pendingRadius, pendingWidth*$0002+1, color)
|
||||
}
|
||||
}
|
||||
|
||||
sub safe_disc(uword @zp xcenter, uword @zp ycenter, ubyte @zp radius, ubyte color) {
|
||||
; This version does bounds checks and clipping, but is a lot slower.
|
||||
; Midpoint algorithm, filled
|
||||
if radius==0
|
||||
return
|
||||
ubyte @zp yy = 0
|
||||
word @zp decisionOver2 = (1 as word)-radius
|
||||
ubyte pendingRadius
|
||||
ubyte pendingWidth
|
||||
bool hasPending = false
|
||||
|
||||
while radius>=yy {
|
||||
uword liney = ycenter+yy
|
||||
if msb(liney)==0
|
||||
safe_horizontal_line(xcenter-radius, lsb(liney), radius*$0002+1, color)
|
||||
liney = ycenter-yy
|
||||
if msb(liney)==0
|
||||
safe_horizontal_line(xcenter-radius, lsb(liney), radius*$0002+1, color)
|
||||
|
||||
if hasPending and pendingRadius != radius {
|
||||
if pendingRadius != pendingWidth {
|
||||
liney = ycenter+pendingRadius
|
||||
if msb(liney)==0
|
||||
safe_horizontal_line(xcenter-pendingWidth, lsb(liney), pendingWidth*$0002+1, color)
|
||||
liney = ycenter-pendingRadius
|
||||
if msb(liney)==0
|
||||
safe_horizontal_line(xcenter-pendingWidth, lsb(liney), pendingWidth*$0002+1, color)
|
||||
}
|
||||
hasPending = false
|
||||
}
|
||||
if not hasPending {
|
||||
pendingRadius = radius
|
||||
hasPending = true
|
||||
}
|
||||
pendingWidth = yy
|
||||
|
||||
yy++
|
||||
if decisionOver2>=0 {
|
||||
radius--
|
||||
decisionOver2 -= radius*$0002
|
||||
}
|
||||
decisionOver2 += yy*$0002
|
||||
decisionOver2++
|
||||
}
|
||||
if hasPending and pendingRadius != pendingWidth {
|
||||
uword flushLiney = ycenter+pendingRadius
|
||||
if msb(flushLiney)==0
|
||||
safe_horizontal_line(xcenter-pendingWidth, lsb(flushLiney), pendingWidth*$0002+1, color)
|
||||
flushLiney = ycenter-pendingRadius
|
||||
if msb(flushLiney)==0
|
||||
safe_horizontal_line(xcenter-pendingWidth, lsb(flushLiney), pendingWidth*$0002+1, color)
|
||||
}
|
||||
}
|
||||
|
||||
asmsub plot(uword x @AX, ubyte y @Y, ubyte color @R0) {
|
||||
; x in r0, y in r1, color.
|
||||
%asm {{
|
||||
clc
|
||||
adc times320_lo,y
|
||||
sta vera.VERA_ADDR_L
|
||||
txa
|
||||
adc times320_mid,y
|
||||
sta vera.VERA_ADDR_M
|
||||
lda #0
|
||||
adc times320_hi,y
|
||||
sta vera.VERA_ADDR_H
|
||||
|
||||
lda p8v_eor_mode
|
||||
bne +
|
||||
lda cx16.r0L
|
||||
sta vera.VERA_DATA0
|
||||
rts
|
||||
+ lda cx16.r0L
|
||||
eor vera.VERA_DATA0
|
||||
sta vera.VERA_DATA0
|
||||
rts
|
||||
}}
|
||||
}
|
||||
|
||||
sub safe_plot(uword xx, ubyte yy, ubyte color) {
|
||||
; A plot that does bounds checks to see if the pixel is inside the screen.
|
||||
if msb(xx)&$80!=0
|
||||
return
|
||||
if xx >= WIDTH or yy >= HEIGHT
|
||||
return
|
||||
plot(xx, yy, color)
|
||||
}
|
||||
|
||||
asmsub pget(uword x @AX, ubyte y @Y) -> ubyte @A {
|
||||
; returns the color of the pixel
|
||||
%asm {{
|
||||
jsr p8s_position
|
||||
lda vera.VERA_DATA0
|
||||
rts
|
||||
}}
|
||||
}
|
||||
|
||||
sub fill(uword x, ubyte y, ubyte new_color, ubyte stack_rambank) {
|
||||
; reuse a few virtual registers in ZP for variables
|
||||
&ubyte fillm = &cx16.r7L
|
||||
&ubyte seedm = &cx16.r8L
|
||||
&ubyte cmask = &cx16.r8H
|
||||
&ubyte vub = &cx16.r13L
|
||||
&ubyte nvub = &cx16.r13H
|
||||
ubyte[4] amask = [$c0,$30,$0c,$03] ; array of cmask bytes
|
||||
|
||||
; Non-recursive scanline flood fill.
|
||||
; based loosely on code found here https://www.codeproject.com/Articles/6017/QuickFill-An-efficient-flood-fill-algorithm
|
||||
; with the fixes applied to the seedfill_4 routine as mentioned in the comments.
|
||||
; Also see https://lodev.org/cgtutor/floodfill.html
|
||||
word @zp xx = x as word
|
||||
word @zp yy = y as word
|
||||
word x1
|
||||
word x2
|
||||
byte dy
|
||||
cx16.r10L = new_color
|
||||
stack.init()
|
||||
|
||||
sub push_stack(word sxl, word sxr, word sy, byte sdy) {
|
||||
cx16.r0s = sy+sdy
|
||||
if cx16.r0s>=0 and cx16.r0s<=HEIGHT-1 {
|
||||
stack.push_w(sxl as uword)
|
||||
stack.push_w(sxr as uword)
|
||||
stack.push_w(sy as uword)
|
||||
stack.push_b(sdy as ubyte)
|
||||
}
|
||||
}
|
||||
sub pop_stack() {
|
||||
dy = stack.pop_b() as byte
|
||||
yy = stack.pop_w() as word
|
||||
x2 = stack.pop_w() as word
|
||||
x1 = stack.pop_w() as word
|
||||
yy+=dy
|
||||
}
|
||||
cx16.r11L = pget(xx as uword, lsb(yy)) ; old_color
|
||||
if cx16.r11L == cx16.r10L
|
||||
return
|
||||
if xx<0 or xx>WIDTH-1 or yy<0 or yy>HEIGHT-1
|
||||
return
|
||||
push_stack(xx, xx, yy, 1)
|
||||
push_stack(xx, xx, yy + 1, -1)
|
||||
word left = 0
|
||||
while not stack.isempty() {
|
||||
pop_stack()
|
||||
xx = x1
|
||||
if fill_scanline_left_8bpp() goto skip
|
||||
left = xx + 1
|
||||
if left < x1
|
||||
push_stack(left, x1 - 1, yy, -dy)
|
||||
xx = x1 + 1
|
||||
|
||||
do {
|
||||
fill_scanline_right_8bpp()
|
||||
push_stack(left, xx - 1, yy, dy)
|
||||
if xx > x2 + 1
|
||||
push_stack(x2 + 1, xx - 1, yy, -dy)
|
||||
skip:
|
||||
xx++
|
||||
while xx <= x2 {
|
||||
if pget(xx as uword, lsb(yy)) == cx16.r11L
|
||||
break
|
||||
xx++
|
||||
}
|
||||
left = xx
|
||||
} until xx>x2
|
||||
}
|
||||
|
||||
sub set_vera_address(bool decr) {
|
||||
; set both data0 and data1 addresses
|
||||
position(xx as uword, lsb(yy))
|
||||
cx16.r0 = vera.VERA_ADDR
|
||||
cx16.r1L = vera.VERA_ADDR_H & 1 | if decr %00011000 else %00010000
|
||||
vera.VERA_ADDR_H = cx16.r1L
|
||||
vera.VERA_CTRL = 1
|
||||
vera.VERA_ADDR = cx16.r0
|
||||
vera.VERA_ADDR_H = cx16.r1L
|
||||
vera.VERA_CTRL = 0
|
||||
}
|
||||
|
||||
sub fill_scanline_left_8bpp() -> bool {
|
||||
set_vera_address(true)
|
||||
cx16.r9s = xx
|
||||
while xx >= 0 {
|
||||
if vera.VERA_DATA0 != cx16.r11L
|
||||
break
|
||||
vera.VERA_DATA1 = cx16.r10L
|
||||
xx--
|
||||
}
|
||||
return xx==cx16.r9s
|
||||
}
|
||||
|
||||
sub fill_scanline_right_8bpp() {
|
||||
set_vera_address(false)
|
||||
while xx <= WIDTH-1 {
|
||||
if vera.VERA_DATA0 != cx16.r11L
|
||||
break
|
||||
vera.VERA_DATA1 = cx16.r10L
|
||||
xx++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const ubyte charset_bank = $1
|
||||
private const uword charset_addr = $f000 ; in bank 1, so $1f000
|
||||
|
||||
sub text(uword @zp xx, ubyte yy, ubyte color, str textptr) {
|
||||
; -- Write some text at the given pixel position. The text string must be in an encoding approprite for the charset.
|
||||
; You must also have called text_charset() first to select and prepare the character set to use.
|
||||
uword chardataptr
|
||||
ubyte[8] @shared char_bitmap_bytes_left
|
||||
ubyte[8] @shared char_bitmap_bytes_right
|
||||
|
||||
while @(textptr)!=0 {
|
||||
chardataptr = charset_addr + (@(textptr) as uword)*8
|
||||
vera.vaddr(charset_bank, chardataptr, 1, 1)
|
||||
repeat 8 {
|
||||
position(xx,lsb(yy))
|
||||
yy++
|
||||
%asm {{
|
||||
ldx p8v_color
|
||||
lda vera.VERA_DATA1
|
||||
sta P8ZP_SCRATCH_B1
|
||||
ldy #8
|
||||
- asl P8ZP_SCRATCH_B1
|
||||
bcc +
|
||||
stx vera.VERA_DATA0 ; write a pixel
|
||||
bcs ++
|
||||
+ lda vera.VERA_DATA0 ; don't write a pixel, but do advance to the next address
|
||||
+ dey
|
||||
bne -
|
||||
}}
|
||||
}
|
||||
xx+=8
|
||||
yy-=8
|
||||
textptr++
|
||||
}
|
||||
}
|
||||
|
||||
asmsub position(uword x @AX, ubyte y @Y) {
|
||||
%asm {{
|
||||
clc
|
||||
adc times320_lo,y
|
||||
sta vera.VERA_ADDR_L
|
||||
txa
|
||||
adc times320_mid,y
|
||||
sta vera.VERA_ADDR_M
|
||||
lda #%00010000 ; auto increment on
|
||||
adc times320_hi,y
|
||||
sta vera.VERA_ADDR_H
|
||||
rts
|
||||
}}
|
||||
}
|
||||
|
||||
inline asmsub next_pixel(ubyte color @A) {
|
||||
; -- sets the next pixel byte to the graphics chip.
|
||||
; for 8 bpp screens this will plot 1 pixel.
|
||||
; for 2 bpp screens it will plot 4 pixels at once (color = bit pattern).
|
||||
%asm {{
|
||||
sta vera.VERA_DATA0
|
||||
}}
|
||||
}
|
||||
|
||||
asmsub next_pixels(uword pixels @AY, uword amount @R0) clobbers(A, X, Y) {
|
||||
; -- sets the next bunch of pixels from a prepared array of bytes.
|
||||
; for 8 bpp screens this will plot 1 pixel per byte.
|
||||
; for 2 bpp screens it will plot 4 pixels at once (colors are the bit patterns per byte).
|
||||
%asm {{
|
||||
sta P8ZP_SCRATCH_W1
|
||||
sty P8ZP_SCRATCH_W1+1
|
||||
ldx cx16.r0+1
|
||||
beq +
|
||||
ldy #0
|
||||
- lda (P8ZP_SCRATCH_W1),y
|
||||
sta vera.VERA_DATA0
|
||||
iny
|
||||
bne -
|
||||
inc P8ZP_SCRATCH_W1+1 ; next page of 256 pixels
|
||||
dex
|
||||
bne -
|
||||
|
||||
+ ldx cx16.r0 ; remaining pixels
|
||||
beq +
|
||||
ldy #0
|
||||
- lda (P8ZP_SCRATCH_W1),y
|
||||
sta vera.VERA_DATA0
|
||||
iny
|
||||
dex
|
||||
bne -
|
||||
+ rts
|
||||
}}
|
||||
}
|
||||
|
||||
asmsub set_8_pixels_from_bits(ubyte bits @R0, ubyte oncolor @A, ubyte offcolor @Y) clobbers(X) {
|
||||
; this is only useful in 256 color mode where one pixel equals one byte value.
|
||||
%asm {{
|
||||
ldx #8
|
||||
- asl cx16.r0
|
||||
bcc +
|
||||
sta vera.VERA_DATA0
|
||||
bcs ++
|
||||
+ sty vera.VERA_DATA0
|
||||
+ dex
|
||||
bne -
|
||||
rts
|
||||
}}
|
||||
}
|
||||
|
||||
%asm {{
|
||||
; multiplication by 320 lookup table
|
||||
times320 := 320*range(240)
|
||||
|
||||
times320_lo .byte <times320
|
||||
times320_mid .byte >times320
|
||||
times320_hi .byte `times320
|
||||
}}
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
%import vera
|
||||
|
||||
%option ignore_unused
|
||||
|
||||
vera_palette {
|
||||
; Manipulate the Vera's display color vera_palette.
|
||||
; Should you want to restore the full default palette, you can call cbm.CINT()
|
||||
; The first 16 colors can be restored to their default with set_default16()
|
||||
; NOTE: assume R0, R1 and R2 are clobbered when using routines in this library!
|
||||
|
||||
%option ignore_unused
|
||||
|
||||
sub set_color(ubyte index, uword color) {
|
||||
internal_set_vera_palette_addr(index)
|
||||
vera.VERA_DATA0 = lsb(color)
|
||||
vera.VERA_DATA0 = msb(color)
|
||||
vera.VERA_ADDR_H &= 1
|
||||
}
|
||||
|
||||
sub get_color(ubyte index) -> uword {
|
||||
internal_set_vera_palette_addr(index)
|
||||
cx16.r0L = vera.VERA_DATA0
|
||||
cx16.r0H = vera.VERA_DATA0
|
||||
vera.VERA_ADDR_H &= 1
|
||||
return cx16.r0
|
||||
}
|
||||
|
||||
sub set_rgb_be(uword palette_ptr, uword num_colors, ubyte startindex) {
|
||||
; 1 word per color entry, $0rgb in big endian format (split arrays)
|
||||
internal_set_vera_palette_addr(startindex)
|
||||
repeat num_colors {
|
||||
vera.VERA_DATA0 = @(palette_ptr+num_colors)
|
||||
vera.VERA_DATA0 = @(palette_ptr)
|
||||
palette_ptr++
|
||||
}
|
||||
vera.VERA_ADDR_H &= 1
|
||||
}
|
||||
|
||||
sub set_rgb_be_nosplit(uword palette_ptr, uword num_colors, ubyte startindex) {
|
||||
; 1 word per color entry, $0rgb in big endian format (linear arrays)
|
||||
internal_set_vera_palette_addr(startindex)
|
||||
repeat num_colors {
|
||||
vera.VERA_DATA0 = @(palette_ptr+1)
|
||||
vera.VERA_DATA0 = @(palette_ptr)
|
||||
palette_ptr += 2
|
||||
}
|
||||
vera.VERA_ADDR_H &= 1
|
||||
}
|
||||
|
||||
sub set_rgb(uword palette_words_ptr, uword num_colors, ubyte startindex) {
|
||||
; 1 word per color entry (in little endian format as layed out in video memory, so $gb;$0r) (split arrays)
|
||||
internal_set_vera_palette_addr(startindex)
|
||||
repeat num_colors {
|
||||
vera.VERA_DATA0 = @(palette_words_ptr)
|
||||
vera.VERA_DATA0 = @(palette_words_ptr+num_colors)
|
||||
palette_words_ptr++
|
||||
}
|
||||
vera.VERA_ADDR_H &= 1
|
||||
}
|
||||
|
||||
sub set_rgb_nosplit(uword palette_words_ptr, uword num_colors, ubyte startindex) {
|
||||
; 1 word per color entry (in little endian format as layed out in video memory, so $gb;$0r) (linear arrays)
|
||||
internal_set_vera_palette_addr(startindex)
|
||||
repeat num_colors {
|
||||
vera.VERA_DATA0 = @(palette_words_ptr)
|
||||
palette_words_ptr++
|
||||
vera.VERA_DATA0 = @(palette_words_ptr)
|
||||
palette_words_ptr++
|
||||
}
|
||||
vera.VERA_ADDR_H &= 1
|
||||
}
|
||||
|
||||
sub set_rgb8(uword palette_bytes_ptr, uword num_colors, ubyte startindex) {
|
||||
; 3 bytes per color entry, adjust color depth from 8 to 4 bits per channel.
|
||||
internal_set_vera_palette_addr(startindex)
|
||||
ubyte red
|
||||
ubyte greenblue
|
||||
repeat num_colors {
|
||||
cx16.r1 = color8to4(palette_bytes_ptr)
|
||||
palette_bytes_ptr+=3
|
||||
vera.VERA_DATA0 = cx16.r1H ; $GB
|
||||
vera.VERA_DATA0 = cx16.r1L ; $0R
|
||||
}
|
||||
vera.VERA_ADDR_H &= 1
|
||||
}
|
||||
|
||||
sub set_all_black() {
|
||||
internal_set_vera_palette_addr(0)
|
||||
repeat 256 {
|
||||
vera.VERA_DATA0 = 0
|
||||
vera.VERA_DATA0 = 0
|
||||
}
|
||||
vera.VERA_ADDR_H &= 1
|
||||
}
|
||||
|
||||
sub set_all_white() {
|
||||
internal_set_vera_palette_addr(0)
|
||||
repeat 256 {
|
||||
vera.VERA_DATA0 = $ff
|
||||
vera.VERA_DATA0 = $0f
|
||||
}
|
||||
vera.VERA_ADDR_H &= 1
|
||||
}
|
||||
|
||||
sub set_grayscale(ubyte startindex) {
|
||||
; set 16 consecutive colors to a grayscale gradient from black to white
|
||||
internal_set_vera_palette_addr(startindex)
|
||||
cx16.r0L=0
|
||||
repeat 16 {
|
||||
vera.VERA_DATA0 = cx16.r0L
|
||||
vera.VERA_DATA0 = cx16.r0L
|
||||
cx16.r0L += $11
|
||||
}
|
||||
vera.VERA_ADDR_H &= 1
|
||||
}
|
||||
|
||||
sub color8to4(uword colorpointer) -> uword {
|
||||
; accurately convert 24 bits (3 bytes) RGB color, in that order in memory, to 16 bits $GB;$0R colorvalue
|
||||
cx16.r1 = colorpointer
|
||||
cx16.r0 = channel8to4(@(cx16.r1)) ; (red) -> $00:0R
|
||||
cx16.r0H = channel8to4(@(cx16.r1+1))<<4 ; (green) -> $G0:0R
|
||||
cx16.r0H |= channel8to4(@(cx16.r1+2)) ; (blue) -> $GB:0R
|
||||
return cx16.r0
|
||||
}
|
||||
|
||||
sub channel8to4(ubyte channelvalue) -> ubyte {
|
||||
; accurately convert a single 8 bit color channel value to 4 bits, see https://threadlocalmutex.com/?p=48
|
||||
return msb(channelvalue * $000f + 135)
|
||||
}
|
||||
|
||||
sub fade_step_multi(ubyte startindex, ubyte endindex, uword target_rgb) -> bool {
|
||||
; Perform one color fade step for multiple consecutive palette entries.
|
||||
; startindex = palette index of first color to fade
|
||||
; endindex = palette index of last color to fade
|
||||
; target_rgb = $RGB color value to fade towards
|
||||
; Returns true if one or more colors were changed, false if no fade steps were done anymore.
|
||||
; So you usually keep calling this until it returns false.
|
||||
bool changed = false
|
||||
while startindex <= endindex {
|
||||
if fade_step(startindex, target_rgb)
|
||||
changed=true
|
||||
startindex++
|
||||
if_z
|
||||
break
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
sub fade_step_colors(ubyte startindex, ubyte endindex, uword target_colors) -> bool {
|
||||
; Perform one color fade step for multiple consecutive palette entries, to different target colors.
|
||||
; startindex = palette index of first color to fade
|
||||
; endindex = palette index of last color to fade, inclusive
|
||||
; target_colors = address of uword $RGB array of colors to fade towards,
|
||||
; in *linear* storage format (@nosplit) which is usually how palette blobs are loaded from disk.
|
||||
; Returns true if one or more colors were changed, false if no fade steps were done anymore.
|
||||
; So you usually keep calling this until it returns false.
|
||||
bool changed = false
|
||||
while startindex <= endindex {
|
||||
if fade_step(startindex, peekw(target_colors))
|
||||
changed=true
|
||||
target_colors += 2
|
||||
startindex++
|
||||
if_z
|
||||
break
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
sub fade_step(ubyte index, uword target_rgb) -> bool {
|
||||
; Perform one color fade step for a single palette entry.
|
||||
; index = palette index of the color to fade
|
||||
; target_rgb = $RGB color value to fade towards
|
||||
; Returns true if the color was changed, false if no fade step was done anymore.
|
||||
; So you usually keep calling this until it returns false.
|
||||
uword color = vera_palette.get_color(index)
|
||||
cx16.r0L = msb(color) ; r
|
||||
cx16.r1L = lsb(color) >> 4 ; g
|
||||
cx16.r2L = lsb(color) & 15 ; b
|
||||
cx16.r0H = msb(target_rgb) & 15 ; r2
|
||||
cx16.r1H = lsb(target_rgb) >> 4 ; g2
|
||||
cx16.r2H = lsb(target_rgb) & 15 ; b2
|
||||
|
||||
ubyte changed
|
||||
|
||||
; use cmp() + status bits branches, to avoid multiple compares that could be done just once
|
||||
cmp(cx16.r0L, cx16.r0H)
|
||||
if_ne {
|
||||
if_cc
|
||||
cx16.r0L++
|
||||
else
|
||||
cx16.r0L--
|
||||
changed++
|
||||
}
|
||||
cmp(cx16.r1L, cx16.r1H)
|
||||
if_ne {
|
||||
if_cc
|
||||
cx16.r1L++
|
||||
else
|
||||
cx16.r1L--
|
||||
changed++
|
||||
}
|
||||
cmp(cx16.r2L, cx16.r2H)
|
||||
if_ne {
|
||||
if_cc
|
||||
cx16.r2L++
|
||||
else
|
||||
cx16.r2L--
|
||||
changed++
|
||||
}
|
||||
|
||||
vera_palette.set_color(index, mkword(cx16.r0L, cx16.r1L<<4 | cx16.r2L))
|
||||
return changed!=0
|
||||
}
|
||||
|
||||
sub set_c64pepto() {
|
||||
; set first 16 colors to the "Pepto" PAL commodore-64 palette http://www.pepto.de/projects/colorvic/
|
||||
uword[] @nosplit colors = [
|
||||
$000, ; 0 = black
|
||||
$FFF, ; 1 = white
|
||||
$833, ; 2 = red
|
||||
$7cc, ; 3 = cyan
|
||||
$839, ; 4 = purple
|
||||
$5a4, ; 5 = green
|
||||
$229, ; 6 = blue
|
||||
$ef7, ; 7 = yellow
|
||||
$852, ; 8 = orange
|
||||
$530, ; 9 = brown
|
||||
$c67, ; 10 = light red
|
||||
$444, ; 11 = dark grey
|
||||
$777, ; 12 = medium grey
|
||||
$af9, ; 13 = light green
|
||||
$76e, ; 14 = light blue
|
||||
$bbb ; 15 = light grey
|
||||
]
|
||||
set_rgb_nosplit(colors, len(colors), 0)
|
||||
}
|
||||
|
||||
sub set_c64ntsc() {
|
||||
; set first 16 colors to a NTSC commodore-64 palette
|
||||
uword[] @nosplit colors = [
|
||||
$000, ; 0 = black
|
||||
$FFF, ; 1 = white
|
||||
$934, ; 2 = red
|
||||
$9ff, ; 3 = cyan
|
||||
$73f, ; 4 = purple
|
||||
$4b1, ; 5 = green
|
||||
$20c, ; 6 = blue
|
||||
$ee6, ; 7 = yellow
|
||||
$b53, ; 8 = orange
|
||||
$830, ; 9 = brown
|
||||
$f8a, ; 10 = light red
|
||||
$444, ; 11 = dark grey
|
||||
$999, ; 12 = medium grey
|
||||
$9f9, ; 13 = light green
|
||||
$36f, ; 14 = light blue
|
||||
$ccc ; 15 = light grey
|
||||
]
|
||||
set_rgb_nosplit(colors, len(colors), 0)
|
||||
}
|
||||
|
||||
sub set_default16() {
|
||||
; set first 16 colors to the defaults on the X16
|
||||
; (doesn't use the rom table so this works on roms older than 49 as well)
|
||||
uword[] @nosplit colors = [
|
||||
$000, ; 0 = black
|
||||
$fff, ; 1 = white
|
||||
$800, ; 2 = red
|
||||
$afe, ; 3 = cyan
|
||||
$c4c, ; 4 = purple
|
||||
$0c5, ; 5 = green
|
||||
$00a, ; 6 = blue
|
||||
$ee7, ; 7 = yellow
|
||||
$d85, ; 8 = orange
|
||||
$640, ; 9 = brown
|
||||
$f77, ; 10 = light red
|
||||
$333, ; 11 = dark grey
|
||||
$777, ; 12 = medium grey
|
||||
$af6, ; 13 = light green
|
||||
$08f, ; 14 = light blue
|
||||
$bbb ; 15 = light grey
|
||||
]
|
||||
set_rgb_nosplit(colors, len(colors), 0)
|
||||
}
|
||||
|
||||
|
||||
private sub internal_set_vera_palette_addr(ubyte index) {
|
||||
vera.VERA_CTRL = 0
|
||||
vera.VERA_ADDR_H = %00010001
|
||||
vera.VERA_ADDR = $fa00+(index *$0002)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
%import vera
|
||||
|
||||
%option ignore_unused
|
||||
|
||||
vera_sprites {
|
||||
; Simple routines to control sprites.
|
||||
|
||||
; They're not written for high performance, but for simplicity.
|
||||
; That's why they control 1 sprite at a time.
|
||||
; The exceptions are pos_batch() and pos_batch_split(); these are quite efficient
|
||||
; to update sprite positions of multiple sprites in one call.
|
||||
|
||||
; HIGH PERFORMANCE sprite handling would probably have a copy of the sprite registers for each sprite instead,
|
||||
; and a unrolled loop that copies those into the VERA registers when needed.
|
||||
|
||||
; note: sprites z-order will be in front of all layers.
|
||||
; note: collision mask is not supported here yet.
|
||||
; note: "palette offset" is counted as 0-15 (vera multiplies the offset by 16 to get at the actual color index)
|
||||
|
||||
const ubyte SIZE_8 = 0
|
||||
const ubyte SIZE_16 = 1
|
||||
const ubyte SIZE_32 = 2
|
||||
const ubyte SIZE_64 = 3
|
||||
const ubyte COLORS_16 = 0
|
||||
const ubyte COLORS_256 = 128
|
||||
const uword VERA_SPRITEREGS = $fc00 ; $1fc00
|
||||
private uword @zp sprite_reg
|
||||
|
||||
sub init(ubyte spritenum,
|
||||
ubyte databank, uword dataaddr,
|
||||
ubyte width_flag, ubyte height_flag,
|
||||
ubyte colors_flag, ubyte palette_offset) {
|
||||
pos(spritenum, -64, -64) ; move sprite off-screen initially
|
||||
vera.VERA_DC_VIDEO |= %01000000 ; enable sprites globally
|
||||
dataaddr >>= 5
|
||||
dataaddr |= (databank as uword)<<11
|
||||
sprite_reg = VERA_SPRITEREGS + spritenum*$0008
|
||||
vera.vpoke(1, sprite_reg, lsb(dataaddr)) ; address 12:5
|
||||
vera.vpoke(1, sprite_reg+1, colors_flag | msb(dataaddr)) ; 4 bpp + address 16:13
|
||||
vera.vpoke(1, sprite_reg+6, %00001100) ; z depth %11 = in front of both layers, no flips
|
||||
vera.vpoke(1, sprite_reg+7, height_flag<<6 | width_flag<<4 | palette_offset&15) ; 64x64 pixels, palette offset
|
||||
}
|
||||
|
||||
sub reset(ubyte spritenum_start, ubyte count) {
|
||||
; resets all sprite attributes for the given sprite range
|
||||
; this removes these sprites from the screen completely
|
||||
; (without disabling sprites globally so the mouse cursor remains visible)
|
||||
if spritenum_start > 127
|
||||
return
|
||||
if count + spritenum_start > 128
|
||||
return
|
||||
vera.VERA_CTRL = $00
|
||||
vera.VERA_ADDR_H = $11
|
||||
vera.VERA_ADDR = VERA_SPRITEREGS + spritenum_start * $0008
|
||||
repeat count {
|
||||
unroll 8 vera.VERA_DATA0 = $00
|
||||
}
|
||||
}
|
||||
|
||||
sub data(ubyte spritenum, ubyte bank, uword addr) {
|
||||
addr >>= 5
|
||||
addr |= (bank as uword)<<11
|
||||
sprite_reg = VERA_SPRITEREGS + spritenum*$0008
|
||||
vera.vpoke(1, sprite_reg, lsb(addr)) ; address 12:5
|
||||
vera.vpoke_mask(1, sprite_reg+1, %11110000, msb(addr)) ; address 16:13
|
||||
}
|
||||
|
||||
inline asmsub get_data_ptr(ubyte spritenum @A) -> ubyte @R1, uword @R0 {
|
||||
; -- returns the VRAM address where the sprite's bitmap data is stored
|
||||
; R1 (byte) = the vera bank (0 or 1), R0 (word) = the address.
|
||||
%asm {{
|
||||
jsr p8b_sprites.p8s_get_data_ptr_internal
|
||||
}}
|
||||
}
|
||||
|
||||
private sub get_data_ptr_internal(ubyte spritenum) {
|
||||
sprite_reg = VERA_SPRITEREGS + spritenum*$0008
|
||||
cx16.r0L = vera.vpeek(1, sprite_reg)
|
||||
cx16.r0H = vera.vpeek(1, sprite_reg+1)
|
||||
cx16.r1L = cx16.r0H & %00001000 !=0 as ubyte ; bank
|
||||
cx16.r0 <<= 5 ; address
|
||||
}
|
||||
|
||||
sub pos(ubyte spritenum, word xpos, word ypos) {
|
||||
sprite_reg = VERA_SPRITEREGS + 2 + spritenum*$0008
|
||||
vera.vpoke(1, sprite_reg, lsb(xpos))
|
||||
vera.vpoke(1, sprite_reg+1, msb(xpos))
|
||||
vera.vpoke(1, sprite_reg+2, lsb(ypos))
|
||||
vera.vpoke(1, sprite_reg+3, msb(ypos))
|
||||
}
|
||||
|
||||
sub pos_batch(ubyte first_spritenum, ubyte num_sprites, uword xpositions_ptr, uword ypositions_ptr) {
|
||||
; -- note: the x and y positions word arrays must both be split word arrays in this version of the routine! (the default)
|
||||
sprite_reg = VERA_SPRITEREGS + 2 + first_spritenum*$0008
|
||||
vera.vaddr_autoincr(1, sprite_reg, 0, 8)
|
||||
vera.vaddr_autoincr(1, sprite_reg+1, 1, 8)
|
||||
tovera(xpositions_ptr)
|
||||
sprite_reg += 2
|
||||
vera.vaddr_autoincr(1, sprite_reg, 0, 8)
|
||||
vera.vaddr_autoincr(1, sprite_reg+1, 1, 8)
|
||||
tovera(ypositions_ptr)
|
||||
|
||||
sub tovera(uword positions @R0) {
|
||||
repeat num_sprites {
|
||||
vera.VERA_DATA0 = @(cx16.r0)
|
||||
vera.VERA_DATA1 = @(cx16.r0+num_sprites)
|
||||
cx16.r0 ++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub pos_batch_nosplit(ubyte first_spritenum, ubyte num_sprites, uword xpositions_ptr, uword ypositions_ptr) {
|
||||
; -- note: the x and y positions word arrays must both be regular linear arrays in this version of the routine!
|
||||
sprite_reg = VERA_SPRITEREGS + 2 + first_spritenum*$0008
|
||||
vera.vaddr_autoincr(1, sprite_reg, 0, 8)
|
||||
vera.vaddr_autoincr(1, sprite_reg+1, 1, 8)
|
||||
tovera(xpositions_ptr)
|
||||
sprite_reg += 2
|
||||
vera.vaddr_autoincr(1, sprite_reg, 0, 8)
|
||||
vera.vaddr_autoincr(1, sprite_reg+1, 1, 8)
|
||||
tovera(ypositions_ptr)
|
||||
|
||||
sub tovera(uword positions @R0) {
|
||||
repeat num_sprites {
|
||||
vera.VERA_DATA0 = @(cx16.r0)
|
||||
cx16.r0 ++
|
||||
vera.VERA_DATA1 = @(cx16.r0)
|
||||
cx16.r0 ++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub setx(ubyte spritenum, word xpos) {
|
||||
sprite_reg = VERA_SPRITEREGS + 2 + spritenum*$0008
|
||||
vera.vpoke(1, sprite_reg, lsb(xpos))
|
||||
vera.vpoke(1, sprite_reg+1, msb(xpos))
|
||||
}
|
||||
|
||||
sub sety(ubyte spritenum, word ypos) {
|
||||
sprite_reg = VERA_SPRITEREGS + 4 + spritenum*$0008
|
||||
vera.vpoke(1, sprite_reg, lsb(ypos))
|
||||
vera.vpoke(1, sprite_reg+1, msb(ypos))
|
||||
}
|
||||
|
||||
sub move(ubyte spritenum, word dx, word dy) {
|
||||
; move a sprite based on its current position
|
||||
sprite_reg = VERA_SPRITEREGS + 2 + spritenum*$0008
|
||||
cx16.r1s = mkword(vera.vpeek(1, sprite_reg+1), vera.vpeek(1, sprite_reg)) as word + dx
|
||||
cx16.r2s = mkword(vera.vpeek(1, sprite_reg+3), vera.vpeek(1, sprite_reg+2)) as word + dy
|
||||
vera.vpoke(1, sprite_reg, cx16.r1L)
|
||||
vera.vpoke(1, sprite_reg+1, cx16.r1H)
|
||||
vera.vpoke(1, sprite_reg+2, cx16.r2L)
|
||||
vera.vpoke(1, sprite_reg+3, cx16.r2H)
|
||||
}
|
||||
|
||||
sub movex(ubyte spritenum, word dx) {
|
||||
; move a sprite horizontally based on its current position
|
||||
sprite_reg = VERA_SPRITEREGS + 2 + spritenum*$0008
|
||||
cx16.r1s = mkword(vera.vpeek(1, sprite_reg+1), vera.vpeek(1, sprite_reg)) as word + dx
|
||||
vera.vpoke(1, sprite_reg, cx16.r1L)
|
||||
vera.vpoke(1, sprite_reg+1, cx16.r1H)
|
||||
}
|
||||
|
||||
sub movey(ubyte spritenum, word dy) {
|
||||
; move a sprite vertically based on its current position
|
||||
sprite_reg = VERA_SPRITEREGS + 4 + spritenum*$0008
|
||||
cx16.r1s = mkword(vera.vpeek(1, sprite_reg+1), vera.vpeek(1, sprite_reg)) as word + dy
|
||||
vera.vpoke(1, sprite_reg, cx16.r1L)
|
||||
vera.vpoke(1, sprite_reg+1, cx16.r1H)
|
||||
}
|
||||
|
||||
sub getx(ubyte spritenum) -> word {
|
||||
sprite_reg = VERA_SPRITEREGS + 2 + spritenum*$0008
|
||||
return mkword(vera.vpeek(1, sprite_reg+1), vera.vpeek(1, sprite_reg)) as word
|
||||
}
|
||||
|
||||
sub gety(ubyte spritenum) -> word {
|
||||
sprite_reg = VERA_SPRITEREGS + 4 + spritenum*$0008
|
||||
return mkword(vera.vpeek(1, sprite_reg+1), vera.vpeek(1, sprite_reg)) as word
|
||||
}
|
||||
|
||||
sub getxy(ubyte spritenum) -> word, word {
|
||||
sprite_reg = VERA_SPRITEREGS + 2 + spritenum*$0008
|
||||
cx16.r0s = mkword(vera.vpeek(1, sprite_reg+1), vera.vpeek(1, sprite_reg)) as word
|
||||
sprite_reg += 2
|
||||
cx16.r1s = mkword(vera.vpeek(1, sprite_reg+1), vera.vpeek(1, sprite_reg)) as word
|
||||
return cx16.r0s, cx16.r1s
|
||||
}
|
||||
|
||||
sub hide(ubyte spritenum) {
|
||||
vera.vpoke_and(1, VERA_SPRITEREGS + 6 + spritenum*$0008, %11110011)
|
||||
}
|
||||
|
||||
sub show(ubyte spritenum) {
|
||||
vera.vpoke_or(1, VERA_SPRITEREGS + 6 + spritenum*$0008, %00001100)
|
||||
}
|
||||
|
||||
sub zdepth(ubyte spritenum, ubyte depth) {
|
||||
vera.vpoke_mask(1, VERA_SPRITEREGS + 6 + spritenum*$0008, %11110011, depth<<2)
|
||||
}
|
||||
|
||||
sub flipx(ubyte spritenum, bool flipped) {
|
||||
vera.vpoke_mask(1, VERA_SPRITEREGS + 6 + spritenum*$0008, %11111110, flipped as ubyte)
|
||||
}
|
||||
|
||||
sub flipy(ubyte spritenum, bool flipped) {
|
||||
vera.vpoke_mask(1, VERA_SPRITEREGS + 6 + spritenum*$0008, %11111101, (flipped as ubyte)<<1)
|
||||
}
|
||||
|
||||
sub set_palette_offset(ubyte spritenum, ubyte offset) {
|
||||
vera.vpoke_mask(1, VERA_SPRITEREGS + 7 + spritenum*$0008, %11110000, offset&15)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,11 @@ adpcm {
|
||||
; $ sox --guard source.mp3 -r 8000 -c 1 -e ima-adpcm out.wav trim 01:27.50 00:09
|
||||
; $ ffmpeg -i source.mp3 -ss 00:01:27.50 -to 00:01:36.50 -ar 8000 -ac 1 -c:a adpcm_ima_wav -block_size 256 -map_metadata -1 -bitexact out.wav
|
||||
; And/or use a tool such as https://github.com/dbry/adpcm-xq (make sure to set the correct block size, -b8)
|
||||
|
||||
; Possible pre-filter step to improve encoded sound quality:
|
||||
; ffmpeg -i input.wav -af "highpass=f=50, lowpass=f=10000, acompressor=threshold=-12dB:ratio=3:attack=8:release=60, loudnorm=I=-16:TP=-1.5, aresample=22050" intermediate.wav
|
||||
; (note that the filter parameters depend on the sample rate! Ask google what the correct parameters should be for different sample rates!)
|
||||
|
||||
;
|
||||
; NOTE: sox may generate IMA-ADPCM files with a block size different than 256 bytes, which is not supported by this decoder. Use ffmpeg instead.
|
||||
;
|
||||
@@ -31,27 +36,10 @@ adpcm {
|
||||
|
||||
%option ignore_unused, private_symbols
|
||||
|
||||
byte[] t_index = [ -1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8]
|
||||
uword[] t_step = [
|
||||
7, 8, 9, 10, 11, 12, 13, 14,
|
||||
16, 17, 19, 21, 23, 25, 28, 31,
|
||||
34, 37, 41, 45, 50, 55, 60, 66,
|
||||
73, 80, 88, 97, 107, 118, 130, 143,
|
||||
157, 173, 190, 209, 230, 253, 279, 307,
|
||||
337, 371, 408, 449, 494, 544, 598, 658,
|
||||
724, 796, 876, 963, 1060, 1166, 1282, 1411,
|
||||
1552, 1707, 1878, 2066, 2272, 2499, 2749, 3024,
|
||||
3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484,
|
||||
7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899,
|
||||
15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794,
|
||||
32767]
|
||||
|
||||
uword @requirezp predict ; decoded 16 bit pcm sample for first channel.
|
||||
uword @requirezp predict_2 ; decoded 16 bit pcm sample for second channel.
|
||||
ubyte @requirezp index
|
||||
ubyte @requirezp index_2
|
||||
uword @requirezp pstep
|
||||
uword @requirezp pstep_2
|
||||
uword @requirezp rowptr ; pointer to current adpcm_deltas row for first channel.
|
||||
uword @requirezp rowptr_2 ; pointer to current adpcm_deltas row for second channel.
|
||||
|
||||
public sub decode_block_mono(uword @requirezp nibblesptr) {
|
||||
; Decodes one 256 byte block of adpcm data, into the Vera's PCM FIFO buffer.
|
||||
@@ -160,86 +148,248 @@ adpcm {
|
||||
sub init(uword startPredict, ubyte startIndex) {
|
||||
; initialize first decoding channel.
|
||||
predict = startPredict
|
||||
index = startIndex
|
||||
pstep = t_step[index]
|
||||
rowptr = &deltas_table + ((startIndex as uword) << 5)
|
||||
}
|
||||
|
||||
sub init_second(uword startPredict_2, ubyte startIndex_2) {
|
||||
; initialize second decoding channel.
|
||||
predict_2 = startPredict_2
|
||||
index_2 = startIndex_2
|
||||
pstep_2 = t_step[index_2]
|
||||
rowptr_2 = &deltas_table + ((startIndex_2 as uword) << 5)
|
||||
}
|
||||
|
||||
sub decode_nibble(ubyte @zp nibble) {
|
||||
/*
|
||||
Prog8 equivalents of the optimized asm routines below:
|
||||
|
||||
sub decode_nibble(ubyte nibble) {
|
||||
; Decoder for a single nibble for the first channel. (value of 'nibble' needs to be strictly 0-15 !)
|
||||
; This is the hotspot of the decoder algorithm!
|
||||
; Note that the generated assembly from this is pretty efficient,
|
||||
; rewriting it by hand in asm seems to improve it only ~10%.
|
||||
cx16.r0s = 0 ; difference
|
||||
if nibble & %0100 !=0
|
||||
cx16.r0s += pstep
|
||||
pstep >>= 1
|
||||
if nibble & %0010 !=0
|
||||
cx16.r0s += pstep
|
||||
pstep >>= 1
|
||||
if nibble & %0001 !=0
|
||||
cx16.r0s += pstep
|
||||
pstep >>= 1
|
||||
cx16.r0s += pstep
|
||||
if nibble & %1000 !=0
|
||||
predict -= cx16.r0
|
||||
else
|
||||
predict += cx16.r0
|
||||
|
||||
; NOTE: the original C/Python code uses a 32 bits prediction value and clips it to a 16 bit word
|
||||
; but for speed reasons we only work with 16 bit words here all the time (with possible clipping error)
|
||||
; if predicted > 32767:
|
||||
; predicted = 32767
|
||||
; elif predicted < -32767:
|
||||
; predicted = - 32767
|
||||
|
||||
index += t_index[nibble] as ubyte
|
||||
if_neg
|
||||
index = 0
|
||||
else if index >= len(t_step)-1
|
||||
index = len(t_step)-1
|
||||
pstep = t_step[index]
|
||||
; This is the hotspot of the decoder algorithm.
|
||||
predict += peekw(rowptr + (nibble as uword * 2))
|
||||
rowptr = clamp(rowptr + t_rowdelta[nibble] as uword, &deltas_table, &deltas_table + 2816)
|
||||
}
|
||||
|
||||
sub decode_nibble_second(ubyte @zp nibble) {
|
||||
sub decode_nibble_second(ubyte nibble) {
|
||||
; Decoder for a single nibble for the second channel. (value of 'nibble' needs to be strictly 0-15 !)
|
||||
; This is the hotspot of the decoder algorithm!
|
||||
; Note that the generated assembly from this is pretty efficient,
|
||||
; rewriting it by hand in asm seems to improve it only ~10%.
|
||||
cx16.r0s = 0 ; difference
|
||||
if nibble & %0100 !=0
|
||||
cx16.r0s += pstep_2
|
||||
pstep_2 >>= 1
|
||||
if nibble & %0010 !=0
|
||||
cx16.r0s += pstep_2
|
||||
pstep_2 >>= 1
|
||||
if nibble & %0001 !=0
|
||||
cx16.r0s += pstep_2
|
||||
pstep_2 >>= 1
|
||||
cx16.r0s += pstep_2
|
||||
if nibble & %1000 !=0
|
||||
predict_2 -= cx16.r0
|
||||
else
|
||||
predict_2 += cx16.r0
|
||||
|
||||
; NOTE: the original C/Python code uses a 32 bits prediction value and clips it to a 16 bit word
|
||||
; but for speed reasons we only work with 16 bit words here all the time (with possible clipping error)
|
||||
; if predicted > 32767:
|
||||
; predicted = 32767
|
||||
; elif predicted < -32767:
|
||||
; predicted = - 32767
|
||||
|
||||
index_2 += t_index[nibble] as ubyte
|
||||
if_neg
|
||||
index_2 = 0
|
||||
else if index_2 >= len(t_step)-1
|
||||
index_2 = len(t_step)-1
|
||||
pstep_2 = t_step[index_2]
|
||||
; This is the hotspot of the decoder algorithm.
|
||||
predict_2 += peekw(rowptr_2 + (nibble as uword * 2))
|
||||
rowptr_2 = clamp(rowptr_2 + t_rowdelta[nibble] as uword, &deltas_table, &deltas_table + 2816)
|
||||
}
|
||||
|
||||
private word[] t_rowdelta = [-32,-32,-32,-32,64,128,192,256,-32,-32,-32,-32,64,128,192,256]
|
||||
|
||||
*/
|
||||
|
||||
asmsub decode_nibble(ubyte nibble @A) clobbers(A, Y) {
|
||||
; Decoder for a single nibble for the first channel. (value of 'nibble' needs to be strictly 0-15 !)
|
||||
; This is the hotspot of the decoder algorithm.
|
||||
%asm {{
|
||||
sta P8ZP_SCRATCH_B1 ; save nibble
|
||||
asl a ; nibble*2; C=0 guaranteed since nibble<=15
|
||||
tay
|
||||
lda (p8v_rowptr),y ; delta LSB
|
||||
adc p8v_predict ; C=0 after asl
|
||||
sta p8v_predict
|
||||
iny
|
||||
lda (p8v_rowptr),y ; delta MSB
|
||||
adc p8v_predict+1
|
||||
sta p8v_predict+1
|
||||
ldy P8ZP_SCRATCH_B1 ; original nibble
|
||||
clc
|
||||
lda p8v_rowptr
|
||||
adc t_rowdelta_lsb,y ; rowptr += signed word row delta
|
||||
sta p8v_rowptr
|
||||
lda p8v_rowptr+1
|
||||
adc t_rowdelta_msb,y
|
||||
sta p8v_rowptr+1
|
||||
; clamp rowptr to [adpcm_deltas, adpcm_deltas+88*32]
|
||||
lda p8v_rowptr+1
|
||||
cmp #>adpcm_deltas
|
||||
bcc _set_min
|
||||
bne _check_max
|
||||
lda p8v_rowptr
|
||||
cmp #<adpcm_deltas
|
||||
bcc _set_min
|
||||
_check_max:
|
||||
lda p8v_rowptr+1
|
||||
cmp #(>adpcm_deltas)+11
|
||||
bcc _done
|
||||
bne _set_max
|
||||
lda p8v_rowptr
|
||||
cmp #<adpcm_deltas
|
||||
beq _done
|
||||
bcc _done
|
||||
_set_max:
|
||||
lda #<(adpcm_deltas+2816)
|
||||
sta p8v_rowptr
|
||||
lda #(>adpcm_deltas)+11
|
||||
sta p8v_rowptr+1
|
||||
bne _done
|
||||
_set_min:
|
||||
lda #<adpcm_deltas
|
||||
sta p8v_rowptr
|
||||
lda #>adpcm_deltas
|
||||
sta p8v_rowptr+1
|
||||
_done:
|
||||
rts
|
||||
}}
|
||||
}
|
||||
|
||||
asmsub decode_nibble_second(ubyte nibble @A) clobbers(A, Y) {
|
||||
; Decoder for a single nibble for the second channel. (value of 'nibble' needs to be strictly 0-15 !)
|
||||
; This is the hotspot of the decoder algorithm.
|
||||
%asm {{
|
||||
sta P8ZP_SCRATCH_B1 ; save nibble
|
||||
asl a ; nibble*2; C=0 guaranteed since nibble<=15
|
||||
tay
|
||||
lda (p8v_rowptr_2),y ; delta LSB
|
||||
adc p8v_predict_2 ; C=0 after asl
|
||||
sta p8v_predict_2
|
||||
iny
|
||||
lda (p8v_rowptr_2),y ; delta MSB
|
||||
adc p8v_predict_2+1
|
||||
sta p8v_predict_2+1
|
||||
ldy P8ZP_SCRATCH_B1 ; original nibble
|
||||
clc
|
||||
lda p8v_rowptr_2
|
||||
adc t_rowdelta_lsb,y ; rowptr_2 += signed word row delta
|
||||
sta p8v_rowptr_2
|
||||
lda p8v_rowptr_2+1
|
||||
adc t_rowdelta_msb,y
|
||||
sta p8v_rowptr_2+1
|
||||
; clamp rowptr_2 to [adpcm_deltas, adpcm_deltas+88*32]
|
||||
lda p8v_rowptr_2+1
|
||||
cmp #>adpcm_deltas
|
||||
bcc _set_min
|
||||
bne _check_max
|
||||
lda p8v_rowptr_2
|
||||
cmp #<adpcm_deltas
|
||||
bcc _set_min
|
||||
_check_max:
|
||||
lda p8v_rowptr_2+1
|
||||
cmp #(>adpcm_deltas)+11
|
||||
bcc _done
|
||||
bne _set_max
|
||||
lda p8v_rowptr_2
|
||||
cmp #<adpcm_deltas
|
||||
beq _done
|
||||
bcc _done
|
||||
_set_max:
|
||||
lda #<(adpcm_deltas+2816)
|
||||
sta p8v_rowptr_2
|
||||
lda #(>adpcm_deltas)+11
|
||||
sta p8v_rowptr_2+1
|
||||
bne _done
|
||||
_set_min:
|
||||
lda #<adpcm_deltas
|
||||
sta p8v_rowptr_2
|
||||
lda #>adpcm_deltas
|
||||
sta p8v_rowptr_2+1
|
||||
_done:
|
||||
rts
|
||||
}}
|
||||
}
|
||||
|
||||
deltas_table:
|
||||
%asm {{
|
||||
; Precomputed IMA ADPCM delta table: 89 step sizes * 16 nibbles = 1424 signed 16-bit values.
|
||||
; Delta = step/8 + (bit2? step) + (bit1? step/2) + (bit0? step/4), negated if nibble bit 3 set.
|
||||
; Stored as interleaved signed 16-bit (.sint) words; index into it is (index<<4)|nibble.
|
||||
; Note: deltas for large steps exceed +32767; they are stored two's-complement (e.g. +36862 as -28674).
|
||||
; The 16-bit wrapping add in the decoder treats these the same as their unsigned representation.
|
||||
|
||||
adpcm_deltas:
|
||||
.sint 0, 1, 3, 4, 7, 8, 10, 11, 0, -1, -3, -4, -7, -8, -10, -11
|
||||
.sint 1, 3, 5, 7, 9, 11, 13, 15, -1, -3, -5, -7, -9, -11, -13, -15
|
||||
.sint 1, 3, 5, 7, 10, 12, 14, 16, -1, -3, -5, -7, -10, -12, -14, -16
|
||||
.sint 1, 3, 6, 8, 11, 13, 16, 18, -1, -3, -6, -8, -11, -13, -16, -18
|
||||
.sint 1, 3, 6, 8, 12, 14, 17, 19, -1, -3, -6, -8, -12, -14, -17, -19
|
||||
.sint 1, 4, 7, 10, 13, 16, 19, 22, -1, -4, -7, -10, -13, -16, -19, -22
|
||||
.sint 1, 4, 7, 10, 14, 17, 20, 23, -1, -4, -7, -10, -14, -17, -20, -23
|
||||
.sint 1, 4, 8, 11, 15, 18, 22, 25, -1, -4, -8, -11, -15, -18, -22, -25
|
||||
.sint 2, 6, 10, 14, 18, 22, 26, 30, -2, -6, -10, -14, -18, -22, -26, -30
|
||||
.sint 2, 6, 10, 14, 19, 23, 27, 31, -2, -6, -10, -14, -19, -23, -27, -31
|
||||
.sint 2, 6, 11, 15, 21, 25, 30, 34, -2, -6, -11, -15, -21, -25, -30, -34
|
||||
.sint 2, 7, 12, 17, 23, 28, 33, 38, -2, -7, -12, -17, -23, -28, -33, -38
|
||||
.sint 2, 7, 13, 18, 25, 30, 36, 41, -2, -7, -13, -18, -25, -30, -36, -41
|
||||
.sint 3, 9, 15, 21, 28, 34, 40, 46, -3, -9, -15, -21, -28, -34, -40, -46
|
||||
.sint 3, 10, 17, 24, 31, 38, 45, 52, -3, -10, -17, -24, -31, -38, -45, -52
|
||||
.sint 3, 10, 18, 25, 34, 41, 49, 56, -3, -10, -18, -25, -34, -41, -49, -56
|
||||
.sint 4, 12, 21, 29, 38, 46, 55, 63, -4, -12, -21, -29, -38, -46, -55, -63
|
||||
.sint 4, 13, 22, 31, 41, 50, 59, 68, -4, -13, -22, -31, -41, -50, -59, -68
|
||||
.sint 5, 15, 25, 35, 46, 56, 66, 76, -5, -15, -25, -35, -46, -56, -66, -76
|
||||
.sint 5, 16, 27, 38, 50, 61, 72, 83, -5, -16, -27, -38, -50, -61, -72, -83
|
||||
.sint 6, 18, 31, 43, 56, 68, 81, 93, -6, -18, -31, -43, -56, -68, -81, -93
|
||||
.sint 6, 19, 33, 46, 61, 74, 88, 101, -6, -19, -33, -46, -61, -74, -88, -101
|
||||
.sint 7, 22, 37, 52, 67, 82, 97, 112, -7, -22, -37, -52, -67, -82, -97, -112
|
||||
.sint 8, 24, 41, 57, 74, 90, 107, 123, -8, -24, -41, -57, -74, -90, -107, -123
|
||||
.sint 9, 27, 45, 63, 82, 100, 118, 136, -9, -27, -45, -63, -82, -100, -118, -136
|
||||
.sint 10, 30, 50, 70, 90, 110, 130, 150, -10, -30, -50, -70, -90, -110, -130, -150
|
||||
.sint 11, 33, 55, 77, 99, 121, 143, 165, -11, -33, -55, -77, -99, -121, -143, -165
|
||||
.sint 12, 36, 60, 84, 109, 133, 157, 181, -12, -36, -60, -84, -109, -133, -157, -181
|
||||
.sint 13, 39, 66, 92, 120, 146, 173, 199, -13, -39, -66, -92, -120, -146, -173, -199
|
||||
.sint 14, 43, 73, 102, 132, 161, 191, 220, -14, -43, -73, -102, -132, -161, -191, -220
|
||||
.sint 16, 48, 81, 113, 146, 178, 211, 243, -16, -48, -81, -113, -146, -178, -211, -243
|
||||
.sint 17, 52, 88, 123, 160, 195, 231, 266, -17, -52, -88, -123, -160, -195, -231, -266
|
||||
.sint 19, 58, 97, 136, 176, 215, 254, 293, -19, -58, -97, -136, -176, -215, -254, -293
|
||||
.sint 21, 64, 107, 150, 194, 237, 280, 323, -21, -64, -107, -150, -194, -237, -280, -323
|
||||
.sint 23, 70, 118, 165, 213, 260, 308, 355, -23, -70, -118, -165, -213, -260, -308, -355
|
||||
.sint 26, 78, 130, 182, 235, 287, 339, 391, -26, -78, -130, -182, -235, -287, -339, -391
|
||||
.sint 28, 85, 143, 200, 258, 315, 373, 430, -28, -85, -143, -200, -258, -315, -373, -430
|
||||
.sint 31, 94, 157, 220, 284, 347, 410, 473, -31, -94, -157, -220, -284, -347, -410, -473
|
||||
.sint 34, 103, 173, 242, 313, 382, 452, 521, -34, -103, -173, -242, -313, -382, -452, -521
|
||||
.sint 38, 114, 191, 267, 345, 421, 498, 574, -38, -114, -191, -267, -345, -421, -498, -574
|
||||
.sint 42, 126, 210, 294, 379, 463, 547, 631, -42, -126, -210, -294, -379, -463, -547, -631
|
||||
.sint 46, 138, 231, 323, 417, 509, 602, 694, -46, -138, -231, -323, -417, -509, -602, -694
|
||||
.sint 51, 153, 255, 357, 459, 561, 663, 765, -51, -153, -255, -357, -459, -561, -663, -765
|
||||
.sint 56, 168, 280, 392, 505, 617, 729, 841, -56, -168, -280, -392, -505, -617, -729, -841
|
||||
.sint 61, 184, 308, 431, 555, 678, 802, 925, -61, -184, -308, -431, -555, -678, -802, -925
|
||||
.sint 68, 204, 340, 476, 612, 748, 884, 1020, -68, -204, -340, -476, -612, -748, -884, -1020
|
||||
.sint 74, 223, 373, 522, 672, 821, 971, 1120, -74, -223, -373, -522, -672, -821, -971, -1120
|
||||
.sint 82, 246, 411, 575, 740, 904, 1069, 1233, -82, -246, -411, -575, -740, -904, -1069, -1233
|
||||
.sint 90, 271, 452, 633, 814, 995, 1176, 1357, -90, -271, -452, -633, -814, -995, -1176, -1357
|
||||
.sint 99, 298, 497, 696, 895, 1094, 1293, 1492, -99, -298, -497, -696, -895, -1094, -1293, -1492
|
||||
.sint 109, 328, 547, 766, 985, 1204, 1423, 1642, -109, -328, -547, -766, -985, -1204, -1423, -1642
|
||||
.sint 120, 360, 601, 841, 1083, 1323, 1564, 1804, -120, -360, -601, -841, -1083, -1323, -1564, -1804
|
||||
.sint 132, 397, 662, 927, 1192, 1457, 1722, 1987, -132, -397, -662, -927, -1192, -1457, -1722, -1987
|
||||
.sint 145, 436, 728, 1019, 1311, 1602, 1894, 2185, -145, -436, -728, -1019, -1311, -1602, -1894, -2185
|
||||
.sint 160, 480, 801, 1121, 1442, 1762, 2083, 2403, -160, -480, -801, -1121, -1442, -1762, -2083, -2403
|
||||
.sint 176, 528, 881, 1233, 1587, 1939, 2292, 2644, -176, -528, -881, -1233, -1587, -1939, -2292, -2644
|
||||
.sint 194, 582, 970, 1358, 1746, 2134, 2522, 2910, -194, -582, -970, -1358, -1746, -2134, -2522, -2910
|
||||
.sint 213, 639, 1066, 1492, 1920, 2346, 2773, 3199, -213, -639, -1066, -1492, -1920, -2346, -2773, -3199
|
||||
.sint 234, 703, 1173, 1642, 2112, 2581, 3051, 3520, -234, -703, -1173, -1642, -2112, -2581, -3051, -3520
|
||||
.sint 258, 774, 1291, 1807, 2324, 2840, 3357, 3873, -258, -774, -1291, -1807, -2324, -2840, -3357, -3873
|
||||
.sint 284, 852, 1420, 1988, 2556, 3124, 3692, 4260, -284, -852, -1420, -1988, -2556, -3124, -3692, -4260
|
||||
.sint 312, 936, 1561, 2185, 2811, 3435, 4060, 4684, -312, -936, -1561, -2185, -2811, -3435, -4060, -4684
|
||||
.sint 343, 1030, 1717, 2404, 3092, 3779, 4466, 5153, -343, -1030, -1717, -2404, -3092, -3779, -4466, -5153
|
||||
.sint 378, 1134, 1890, 2646, 3402, 4158, 4914, 5670, -378, -1134, -1890, -2646, -3402, -4158, -4914, -5670
|
||||
.sint 415, 1246, 2078, 2909, 3742, 4573, 5405, 6236, -415, -1246, -2078, -2909, -3742, -4573, -5405, -6236
|
||||
.sint 457, 1372, 2287, 3202, 4117, 5032, 5947, 6862, -457, -1372, -2287, -3202, -4117, -5032, -5947, -6862
|
||||
.sint 503, 1509, 2516, 3522, 4529, 5535, 6542, 7548, -503, -1509, -2516, -3522, -4529, -5535, -6542, -7548
|
||||
.sint 553, 1660, 2767, 3874, 4981, 6088, 7195, 8302, -553, -1660, -2767, -3874, -4981, -6088, -7195, -8302
|
||||
.sint 608, 1825, 3043, 4260, 5479, 6696, 7914, 9131, -608, -1825, -3043, -4260, -5479, -6696, -7914, -9131
|
||||
.sint 669, 2008, 3348, 4687, 6027, 7366, 8706, 10045, -669, -2008, -3348, -4687, -6027, -7366, -8706, -10045
|
||||
.sint 736, 2209, 3683, 5156, 6630, 8103, 9577, 11050, -736, -2209, -3683, -5156, -6630, -8103, -9577, -11050
|
||||
.sint 810, 2431, 4052, 5673, 7294, 8915, 10536, 12157, -810, -2431, -4052, -5673, -7294, -8915, -10536, -12157
|
||||
.sint 891, 2674, 4457, 6240, 8023, 9806, 11589, 13372, -891, -2674, -4457, -6240, -8023, -9806, -11589, -13372
|
||||
.sint 980, 2941, 4902, 6863, 8825, 10786, 12747, 14708, -980, -2941, -4902, -6863, -8825, -10786, -12747, -14708
|
||||
.sint 1078, 3235, 5393, 7550, 9708, 11865, 14023, 16180, -1078, -3235, -5393, -7550, -9708, -11865, -14023, -16180
|
||||
.sint 1186, 3559, 5932, 8305, 10679, 13052, 15425, 17798, -1186, -3559, -5932, -8305, -10679, -13052, -15425, -17798
|
||||
.sint 1305, 3915, 6526, 9136, 11747, 14357, 16968, 19578, -1305, -3915, -6526, -9136, -11747, -14357, -16968, -19578
|
||||
.sint 1435, 4306, 7178, 10049, 12922, 15793, 18665, 21536, -1435, -4306, -7178, -10049, -12922, -15793, -18665, -21536
|
||||
.sint 1579, 4737, 7896, 11054, 14214, 17372, 20531, 23689, -1579, -4737, -7896, -11054, -14214, -17372, -20531, -23689
|
||||
.sint 1737, 5211, 8686, 12160, 15636, 19110, 22585, 26059, -1737, -5211, -8686, -12160, -15636, -19110, -22585, -26059
|
||||
.sint 1911, 5733, 9555, 13377, 17200, 21022, 24844, 28666, -1911, -5733, -9555, -13377, -17200, -21022, -24844, -28666
|
||||
.sint 2102, 6306, 10511, 14715, 18920, 23124, 27329, 31533, -2102, -6306, -10511, -14715, -18920, -23124, -27329, -31533
|
||||
.sint 2312, 6937, 11562, 16187, 20812, 25437, 30062, -30849, -2312, -6937, -11562, -16187, -20812, -25437, -30062, 30849
|
||||
.sint 2543, 7630, 12718, 17805, 22893, 27980, -32468, -27381, -2543, -7630, -12718, -17805, -22893, -27980, 32468, 27381
|
||||
.sint 2798, 8394, 13990, 19586, 25183, 30779, -29161, -23565, -2798, -8394, -13990, -19586, -25183, -30779, 29161, 23565
|
||||
.sint 3077, 9232, 15388, 21543, 27700, -31681, -25525, -19370, -3077, -9232, -15388, -21543, -27700, 31681, 25525, 19370
|
||||
.sint 3385, 10156, 16928, 23699, 30471, -28294, -21522, -14751, -3385, -10156, -16928, -23699, -30471, 28294, 21522, 14751
|
||||
.sint 3724, 11172, 18621, 26069, -32018, -24570, -17121, -9673, -3724, -11172, -18621, -26069, 32018, 24570, 17121, 9673
|
||||
.sint 4095, 12286, 20478, 28669, -28674, -20483, -12291, -4100, -4095, -12286, -20478, -28669, 28674, 20483, 12291, 4100
|
||||
; Row pointer deltas (t_index * 32) as signed 16-bit values, split into LSB/MSB tables.
|
||||
t_rowdelta_lsb:
|
||||
.byte 224, 224, 224, 224, 64, 128, 192, 0, 224, 224, 224, 224, 64, 128, 192, 0
|
||||
t_rowdelta_msb:
|
||||
.byte 255, 255, 255, 255, 0, 0, 0, 1, 255, 255, 255, 255, 0, 0, 0, 1
|
||||
}}
|
||||
}
|
||||
@@ -832,68 +832,69 @@ set_byte:
|
||||
cx16.screen_set_charset(charset, 0)
|
||||
}
|
||||
|
||||
private inline asmsub vera_addr_inc(ubyte amount @A) clobbers(A) {
|
||||
; -- add amount to the current VERA address register (port 0)
|
||||
%asm {{
|
||||
stz cx16.VERA_CTRL
|
||||
clc
|
||||
adc cx16.VERA_ADDR_L
|
||||
sta cx16.VERA_ADDR_L
|
||||
lda cx16.VERA_ADDR_M
|
||||
adc #0
|
||||
sta cx16.VERA_ADDR_M
|
||||
lda cx16.VERA_ADDR_H
|
||||
adc #0
|
||||
sta cx16.VERA_ADDR_H
|
||||
}}
|
||||
}
|
||||
|
||||
private inline asmsub vera_addr_next() clobbers(A) {
|
||||
; -- increment the current VERA address register (port 0) by one.
|
||||
; This is smaller and faster than vera_addr_inc(1) because it skips
|
||||
; the high bytes when there is no carry.
|
||||
%asm {{
|
||||
stz cx16.VERA_CTRL
|
||||
inc cx16.VERA_ADDR_L
|
||||
bne +
|
||||
inc cx16.VERA_ADDR_M
|
||||
bne +
|
||||
inc cx16.VERA_ADDR_H
|
||||
+ }}
|
||||
}
|
||||
|
||||
sub text(uword @zp xx, uword yy, ubyte color, str sctextptr) {
|
||||
; -- Write some text at the given pixel position. The text string must be in screencode encoding (not petscii!).
|
||||
; You must also have called text_charset() first to select and prepare the character set to use.
|
||||
uword chardataptr
|
||||
ubyte[8] @shared char_bitmap_bytes_left
|
||||
ubyte[8] @shared char_bitmap_bytes_right
|
||||
ubyte[4] @shared mask4c = [%00111111, %11001111, %11110011, %11111100]
|
||||
ubyte[4] @shared colorshift
|
||||
|
||||
; prepare the color-shifted values for each pixel group once per call.
|
||||
cx16.r11L = color & 3
|
||||
colorshift[0] = cx16.r11L << 6
|
||||
colorshift[1] = cx16.r11L << 4
|
||||
colorshift[2] = cx16.r11L << 2
|
||||
colorshift[3] = cx16.r11L
|
||||
|
||||
; we're going to use a few cx16 registers to make sure every variable is in zeropage in the inner loop.
|
||||
cx16.r11L = color
|
||||
while @(sctextptr)!=0 {
|
||||
chardataptr = charset_addr + (@(sctextptr) as uword)*8
|
||||
cx16.vaddr(charset_bank, chardataptr, 1, 1) ; for reading the chardata from Vera data channel 1
|
||||
position(xx, yy) ; only calculated once, we update vera address in the loop instead
|
||||
cx16.vaddr(charset_bank, chardataptr, 1, 1) ; read chardata from vera data channel 1
|
||||
position(xx, yy)
|
||||
cx16.VERA_ADDR_H &= $0f ; no auto increment
|
||||
cx16.r7 = xx
|
||||
repeat 8 {
|
||||
cx16.r10L = cx16.VERA_DATA1 ; get the next 8 horizontal character bits
|
||||
cx16.r7 = xx
|
||||
cx16.r10L = cx16.VERA_DATA1 ; get the next 8 horizontal character bits
|
||||
repeat 8 {
|
||||
cx16.r10L <<= 1
|
||||
if_cs {
|
||||
cx16.r2L = cx16.r7L & 3 ; xbits
|
||||
when cx16.r11L & 3 {
|
||||
1 -> cx16.r12L = gfx_hires.plot.shiftedleft_4c_1[cx16.r2L]
|
||||
2 -> cx16.r12L = gfx_hires.plot.shiftedleft_4c_2[cx16.r2L]
|
||||
3 -> cx16.r12L = gfx_hires.plot.shiftedleft_4c_3[cx16.r2L]
|
||||
else -> cx16.r12L = 0
|
||||
}
|
||||
cx16.VERA_DATA0 = cx16.VERA_DATA0 & gfx_hires.plot.mask4c[cx16.r2L] | cx16.r12L
|
||||
cx16.r2L = cx16.r7L & 3
|
||||
cx16.VERA_DATA0 = (cx16.VERA_DATA0 & mask4c[cx16.r2L]) | colorshift[cx16.r2L]
|
||||
}
|
||||
cx16.r7++
|
||||
if (cx16.r7 & 3) == 0 {
|
||||
; increment the pixel address by one
|
||||
%asm {{
|
||||
stz cx16.VERA_CTRL
|
||||
clc
|
||||
lda cx16.VERA_ADDR_L
|
||||
adc #1
|
||||
sta cx16.VERA_ADDR_L
|
||||
lda cx16.VERA_ADDR_M
|
||||
adc #0
|
||||
sta cx16.VERA_ADDR_M
|
||||
lda cx16.VERA_ADDR_H
|
||||
adc #0
|
||||
sta cx16.VERA_ADDR_H
|
||||
}}
|
||||
}
|
||||
if (cx16.r7 & 3) == 0
|
||||
vera_addr_next()
|
||||
}
|
||||
|
||||
; increment pixel address to the next line
|
||||
%asm {{
|
||||
stz cx16.VERA_CTRL
|
||||
clc
|
||||
lda cx16.VERA_ADDR_L
|
||||
adc #(640-8)/4
|
||||
sta cx16.VERA_ADDR_L
|
||||
lda cx16.VERA_ADDR_M
|
||||
adc #0
|
||||
sta cx16.VERA_ADDR_M
|
||||
lda cx16.VERA_ADDR_H
|
||||
adc #0
|
||||
sta cx16.VERA_ADDR_H
|
||||
}}
|
||||
vera_addr_inc(158) ; advance to next row (160 bytes minus the 2 bytes already stepped)
|
||||
}
|
||||
xx+=8
|
||||
sctextptr++
|
||||
|
||||
@@ -745,7 +745,7 @@ skip:
|
||||
private const ubyte charset_bank = $1
|
||||
private const uword charset_addr = $f000 ; in bank 1, so $1f000
|
||||
|
||||
sub text(uword @zp xx, uword yy, ubyte color, str textptr) {
|
||||
sub text(uword @zp xx, ubyte yy, ubyte color, str textptr) {
|
||||
; -- Write some text at the given pixel position. The text string must be in an encoding approprite for the charset.
|
||||
; You must also have called text_charset() first to select and prepare the character set to use.
|
||||
uword chardataptr
|
||||
@@ -755,9 +755,8 @@ skip:
|
||||
while @(textptr)!=0 {
|
||||
chardataptr = charset_addr + (@(textptr) as uword)*8
|
||||
cx16.vaddr(charset_bank, chardataptr, 1, 1)
|
||||
position(xx, yy)
|
||||
repeat 8 {
|
||||
position(xx,lsb(yy))
|
||||
yy++
|
||||
%asm {{
|
||||
ldx p8v_color
|
||||
lda cx16.VERA_DATA1
|
||||
@@ -771,9 +770,22 @@ skip:
|
||||
+ dey
|
||||
bne -
|
||||
}}
|
||||
; advance to next row (320 bytes minus the 8 pixels already stepped)
|
||||
%asm {{
|
||||
stz cx16.VERA_CTRL
|
||||
clc
|
||||
lda cx16.VERA_ADDR_L
|
||||
adc #<(320-8)
|
||||
sta cx16.VERA_ADDR_L
|
||||
lda cx16.VERA_ADDR_M
|
||||
adc #>(320-8)
|
||||
sta cx16.VERA_ADDR_M
|
||||
lda cx16.VERA_ADDR_H
|
||||
adc #0
|
||||
sta cx16.VERA_ADDR_H
|
||||
}}
|
||||
}
|
||||
xx+=8
|
||||
yy-=8
|
||||
textptr++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ sprites {
|
||||
const ubyte COLORS_16 = 0
|
||||
const ubyte COLORS_256 = 128
|
||||
const uword VERA_SPRITEREGS = $fc00 ; $1fc00
|
||||
uword @zp sprite_reg
|
||||
private uword @zp sprite_reg
|
||||
|
||||
sub init(ubyte spritenum,
|
||||
ubyte databank, uword dataaddr,
|
||||
|
||||
+115
-71
@@ -186,6 +186,51 @@ result .byte ?,?,?,? ; routine could be faster if this were in Zeropage.
|
||||
.pend
|
||||
|
||||
|
||||
multiply_ubyte_word .proc
|
||||
; -- multiply a 16-bit word by an UNSIGNED 8-bit value, giving the low 16 bits
|
||||
; input: A = the 8-bit value (the high byte of A/Y is ignored, so this may be
|
||||
; a zero-extended ubyte), multiply_ubyte_word.multiplier = the 16-bit word
|
||||
; output: low 16 bits of the product in A/Y
|
||||
;
|
||||
; Same calling convention as multiply_words, but only the 8 bits in A are significant, so the
|
||||
; shift-add runs 8 times instead of 16. The 16-bit accumulator discards the carries out of the
|
||||
; top, which is exactly the mod-2^16 truncation Prog8's word semantics require (the true
|
||||
; product of a byte and a word can need 24 bits).
|
||||
;
|
||||
; The byte operand must be UNSIGNED: a signed byte is sign-extended into A/Y by the caller and
|
||||
; this routine deliberately ignores the high byte, so it would compute the unsigned product.
|
||||
; The compiler only emits a call here when the operand is isUnsignedByte.
|
||||
|
||||
multiplier = P8ZP_SCRATCH_W1 ; 2 bytes, the word, shifted left in place
|
||||
_result = P8ZP_SCRATCH_W2 ; 2 bytes
|
||||
_multiplicand = P8ZP_SCRATCH_B1 ; 1 byte, the 8-bit value
|
||||
|
||||
sta _multiplicand
|
||||
lda #0
|
||||
sta _result
|
||||
sta _result+1
|
||||
ldx #8
|
||||
-
|
||||
lsr _multiplicand ; next bit of the 8-bit value, LSB first
|
||||
bcc +
|
||||
clc
|
||||
lda _result
|
||||
adc multiplier
|
||||
sta _result
|
||||
lda _result+1
|
||||
adc multiplier+1
|
||||
sta _result+1
|
||||
+
|
||||
asl multiplier ; word <<= 1
|
||||
rol multiplier+1
|
||||
dex
|
||||
bne -
|
||||
lda _result
|
||||
ldy _result+1
|
||||
rts
|
||||
.pend
|
||||
|
||||
|
||||
multiply_longs .proc
|
||||
; 32-bit SIGNED multiplication.
|
||||
; long1 in cx16.r12/r13, long2 in cx16.r14/r15, result in cx16.r14/r15
|
||||
@@ -259,9 +304,17 @@ _shift
|
||||
ror P8ZP_SCRATCH_W1+1
|
||||
ror P8ZP_SCRATCH_W1
|
||||
|
||||
; Early exit once the multiplier is exhausted: the remaining iterations
|
||||
; would only add zero, so skip them. Branch to the sign fixup and not to
|
||||
; _done, or a negative product would never be negated.
|
||||
lda P8ZP_SCRATCH_W1
|
||||
ora P8ZP_SCRATCH_W2
|
||||
beq _signfix
|
||||
|
||||
dex
|
||||
bne _loop
|
||||
|
||||
_signfix
|
||||
; Apply sign if needed
|
||||
lda P8ZP_SCRATCH_B1
|
||||
bpl _done
|
||||
@@ -914,14 +967,6 @@ mul_word_25 .proc
|
||||
rts
|
||||
.pend
|
||||
|
||||
mul_byte_40 .proc
|
||||
and #7
|
||||
tay
|
||||
lda _forties,y
|
||||
rts
|
||||
_forties .byte 0*40, 1*40, 2*40, 3*40, 4*40, 5*40, 6*40, 7*40 & 255
|
||||
.pend
|
||||
|
||||
mul_word_40 .proc
|
||||
; AY = (AY*4 + AY)*8
|
||||
sta P8ZP_SCRATCH_W1
|
||||
@@ -948,14 +993,6 @@ mul_word_40 .proc
|
||||
rts
|
||||
.pend
|
||||
|
||||
mul_byte_50 .proc
|
||||
and #7
|
||||
tay
|
||||
lda _fifties, y
|
||||
rts
|
||||
_fifties .byte 0*50, 1*50, 2*50, 3*50, 4*50, 5*50, 6*50 & 255, 7*50 & 255
|
||||
.pend
|
||||
|
||||
mul_word_50 .proc
|
||||
; AY = AY * 25 * 2
|
||||
jsr mul_word_25
|
||||
@@ -966,14 +1003,6 @@ mul_word_50 .proc
|
||||
rts
|
||||
.pend
|
||||
|
||||
mul_byte_80 .proc
|
||||
and #3
|
||||
tay
|
||||
lda _eighties, y
|
||||
rts
|
||||
_eighties .byte 0*80, 1*80, 2*80, 3*80
|
||||
.pend
|
||||
|
||||
mul_word_80 .proc
|
||||
; AY = AY * 40 * 2
|
||||
jsr mul_word_40
|
||||
@@ -984,14 +1013,6 @@ mul_word_80 .proc
|
||||
rts
|
||||
.pend
|
||||
|
||||
mul_byte_100 .proc
|
||||
and #3
|
||||
tay
|
||||
lda _hundreds, y
|
||||
rts
|
||||
_hundreds .byte 0*100, 1*100, 2*100, 3*100 & 255
|
||||
.pend
|
||||
|
||||
mul_word_100 .proc
|
||||
; AY = AY * 25 * 4
|
||||
jsr mul_word_25
|
||||
@@ -1004,38 +1025,6 @@ mul_word_100 .proc
|
||||
rts
|
||||
.pend
|
||||
|
||||
mul_word_320 .proc
|
||||
; AY = A * 256 + A * 64 (msb in Y doesn't matter)
|
||||
sta P8ZP_SCRATCH_B1
|
||||
ldy #0
|
||||
sty P8ZP_SCRATCH_REG
|
||||
asl a
|
||||
rol P8ZP_SCRATCH_REG
|
||||
asl a
|
||||
rol P8ZP_SCRATCH_REG
|
||||
asl a
|
||||
rol P8ZP_SCRATCH_REG
|
||||
asl a
|
||||
rol P8ZP_SCRATCH_REG
|
||||
asl a
|
||||
rol P8ZP_SCRATCH_REG
|
||||
asl a
|
||||
rol P8ZP_SCRATCH_REG
|
||||
pha
|
||||
clc
|
||||
lda P8ZP_SCRATCH_B1
|
||||
adc P8ZP_SCRATCH_REG
|
||||
tay
|
||||
pla
|
||||
rts
|
||||
.pend
|
||||
|
||||
mul_word_640 .proc
|
||||
; AY = (A * 2 * 320) (msb in Y doesn't matter)
|
||||
asl a
|
||||
jmp mul_word_320
|
||||
.pend
|
||||
|
||||
|
||||
; ----------- end optimized multiplications -----------
|
||||
|
||||
@@ -1124,7 +1113,36 @@ square_long .proc
|
||||
; Input: cx16.R14:R15 = 32-bit value to square (R14=low word, R15=high word)
|
||||
; Output: cx16.R14:R15 = 32-bit square result (R14=low word, R15=high word)
|
||||
; Clobbers: A, X, Y
|
||||
;
|
||||
; The square below only squares the low word, so it is only valid when the long actually
|
||||
; fits in a signed 16-bit word (the high word is the sign extension of the low word's
|
||||
; sign bit). The range test at the top of the routine handles that: values outside it
|
||||
; tail-call the general multiply_longs instead. Note a high word of $0000 alone is not
|
||||
; sufficient: 40000 as a signed word is -25536, and squaring that low word would be
|
||||
; wrong. Doing the test here rather than inline at every sqr() call site costs the code
|
||||
; size only once.
|
||||
|
||||
; Range check: is the long representable as a signed 16-bit word?
|
||||
lda cx16.r14+1
|
||||
and #128
|
||||
beq _nonneg
|
||||
; negative low word: the high word must be the sign extension ($FF,$FF)
|
||||
lda cx16.r15
|
||||
cmp #$FF
|
||||
bne _out_of_range
|
||||
lda cx16.r15+1
|
||||
cmp #$FF
|
||||
bne _out_of_range
|
||||
jmp _square
|
||||
|
||||
_nonneg
|
||||
; non-negative low word: the high word must be zero
|
||||
lda cx16.r15
|
||||
ora cx16.r15+1
|
||||
bne _out_of_range
|
||||
; fall through into the fast path
|
||||
|
||||
_square
|
||||
lda cx16.r14
|
||||
ldy cx16.r14+1
|
||||
ldx cx16.r15+1
|
||||
@@ -1146,20 +1164,31 @@ square_long .proc
|
||||
sta cx16.r15+1
|
||||
|
||||
rts
|
||||
|
||||
_out_of_range
|
||||
; r12:r13 is a 32-bit pair, so copy all four bytes, not just the low word
|
||||
lda cx16.r14
|
||||
sta cx16.r12
|
||||
lda cx16.r14+1
|
||||
sta cx16.r12+1
|
||||
lda cx16.r15
|
||||
sta cx16.r13
|
||||
lda cx16.r15+1
|
||||
sta cx16.r13+1
|
||||
jmp multiply_longs
|
||||
.pend
|
||||
|
||||
|
||||
square .proc
|
||||
; -- calculate square of signed word (actually -255..255) in AY, result in AY
|
||||
; -- calculate square of signed word in AY, result in AY
|
||||
; routine by Lee Davison, source: http://6502.org/source/integers/square.htm
|
||||
; using this routine is a lot faster as doing a regular multiplication (for words)
|
||||
;
|
||||
; Calculates the 16 bit unsigned integer square of the signed 16 bit integer in
|
||||
; Numberl/Numberh. The result is always in the range 0 to 65025 and is held in
|
||||
; Squarel/Squareh
|
||||
;
|
||||
; The maximum input range is only +/-255 and no checking is done to ensure that
|
||||
; this is so.
|
||||
; The fast path below (by Lee Davison) only computes (abs(x) and 255) squared, so it is
|
||||
; only valid for the input range -255..255. The range test at the top of the routine
|
||||
; handles that: values outside it tail-call the general multiply_words instead, which
|
||||
; yields the correct mod-2^16 result that Prog8's word semantics require. Doing the
|
||||
; test here rather than inline at every sqr() call site costs the code size only once.
|
||||
;
|
||||
; This routine is useful if you are trying to draw circles as for any circle
|
||||
; x^2+y^2=r^2 where x and y are the co-ordinates of any point on the circle and
|
||||
@@ -1171,6 +1200,21 @@ squarel = P8ZP_SCRATCH_W2 ; square low byte
|
||||
squareh = P8ZP_SCRATCH_W2+1 ; square high byte
|
||||
tempsq = P8ZP_SCRATCH_B1 ; temp byte for intermediate result
|
||||
|
||||
; Range check: the fast path is only valid for -255..255. The high byte alone
|
||||
; settles it: $00 is 0..255 (always in range), $FF is -256..-1 (in range except
|
||||
; -256 itself, whose square is $10000 and so also needs the general path).
|
||||
cpy #0
|
||||
beq _in_range
|
||||
cpy #$FF
|
||||
bne _out_of_range
|
||||
cmp #0
|
||||
bne _in_range
|
||||
_out_of_range
|
||||
sta multiply_words.multiplier
|
||||
sty multiply_words.multiplier+1
|
||||
jmp multiply_words
|
||||
|
||||
_in_range
|
||||
sta numberl
|
||||
sty numberh
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
%import shared_m68k_adpcm
|
||||
|
||||
adpcm {
|
||||
%option ignore_unused, private_symbols
|
||||
}
|
||||
@@ -27,6 +27,7 @@ p8_sys_startup {
|
||||
}
|
||||
|
||||
sub init_system_phase2() {
|
||||
%option noframe
|
||||
%asm {{
|
||||
moveq #0,d0
|
||||
moveq #0,d1
|
||||
@@ -48,6 +49,7 @@ p8_sys_startup {
|
||||
}
|
||||
|
||||
sub cleanup_at_exit() {
|
||||
%option noframe
|
||||
%asm {{
|
||||
movea.l #qemu.CTRL_REG_CMD,a1
|
||||
move.l #qemu.CTRL_CMD_HALT,(a1)
|
||||
|
||||
@@ -310,6 +310,7 @@ sys {
|
||||
|
||||
|
||||
sub reset_system() {
|
||||
%option noframe
|
||||
%asm {{
|
||||
movea.l #qemu.CTRL_REG_CMD,a1
|
||||
move.l #qemu.CTRL_CMD_RESET,(a1)
|
||||
@@ -318,6 +319,7 @@ sys {
|
||||
}
|
||||
|
||||
sub poweroff_system() {
|
||||
%option noframe
|
||||
%asm {{
|
||||
movea.l #qemu.CTRL_REG_CMD,a1
|
||||
move.l #qemu.CTRL_CMD_HALT,(a1)
|
||||
@@ -325,7 +327,8 @@ sys {
|
||||
}}
|
||||
}
|
||||
|
||||
sub die(ubyte code, str message) {
|
||||
sub die(uword code, str message) {
|
||||
%option noframe
|
||||
; -- kill the program: print death message to goldfish TTY, then exit.
|
||||
str @shared warning = iso:"\n\nPROGRAM DIED: "
|
||||
%asm {{
|
||||
@@ -346,7 +349,7 @@ sys {
|
||||
moveq #10,d0
|
||||
move.l d0,qemu.TTY_PUT_CHAR
|
||||
}}
|
||||
exit(code)
|
||||
exit(code as word)
|
||||
}
|
||||
|
||||
sub wait(uword jiffies) {
|
||||
@@ -355,16 +358,19 @@ sys {
|
||||
}
|
||||
|
||||
|
||||
sub exit(ubyte returnvalue) {
|
||||
asmsub exit(word returnvalue @D0) {
|
||||
; -- exit the program with a return code in D0. When invoked from Prog8 via sys.exit(), all active defers in the call chain are unwound program-wide (LIFO) before system cleanup. sys.reset_system()/poweroff_system() do not run defers.
|
||||
%asm {{
|
||||
moveq.l #0,d0
|
||||
move.b sys.exit.returnvalue,d0
|
||||
and.l #$ffff,d0
|
||||
movea.l #qemu.CTRL_REG_CMD,a1
|
||||
move.l #qemu.CTRL_CMD_HALT,(a1)
|
||||
stop #0
|
||||
; !notreached!
|
||||
}}
|
||||
poweroff_system()
|
||||
}
|
||||
|
||||
sub set_carry() {
|
||||
%option noframe
|
||||
%asm {{
|
||||
; set both C (comparison carry) and X (rotate carry) bits
|
||||
moveq #$11,d0
|
||||
@@ -373,6 +379,7 @@ sys {
|
||||
}
|
||||
|
||||
sub clear_carry() {
|
||||
%option noframe
|
||||
%asm {{
|
||||
moveq #0,d0
|
||||
move.w d0,ccr
|
||||
@@ -387,12 +394,14 @@ sys {
|
||||
}
|
||||
|
||||
sub clear_irqd() {
|
||||
%option noframe
|
||||
%asm {{
|
||||
andi.w #$f8ff,sr ; privileged !
|
||||
}}
|
||||
}
|
||||
|
||||
sub set_irqd() {
|
||||
%option noframe
|
||||
%asm {{
|
||||
ori.w #$0700,sr ; privileged !
|
||||
}}
|
||||
@@ -465,4 +474,21 @@ sys {
|
||||
lea prog8_program_end,a0
|
||||
}}
|
||||
}
|
||||
|
||||
asmsub stack_size() -> long @D0 {
|
||||
; -- Return the amount of stack space still available, in bytes.
|
||||
%asm {{
|
||||
move.l sp,d0
|
||||
lea prog8_program_end,a0
|
||||
sub.l a0,d0
|
||||
rts
|
||||
}}
|
||||
}
|
||||
|
||||
asmsub cpuAtLeast68020() -> bool @Pz {
|
||||
%asm {{
|
||||
moveq #0,d0 ; qemu68k always targets a 68020 or newer
|
||||
rts
|
||||
}}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
%import shared_m68k_wavfile
|
||||
|
||||
wavfile {
|
||||
%option ignore_unused
|
||||
}
|
||||
Loaded 100 of 259 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user