These utility modules provide a system for efficiently preserving Z80 register states between macro calls, ensuring only the needed registers are preserved.
- The internal macros use utils.clobbers to declare which registers they clobber (in lieu of push/pop calls). You can utilise them too
- The caller can utilise utils.preserve to wrap the calls and declare which registers should not be clobbered
- The utilities will work together to ensure only the clobbered AND protected registers get preserved
This provides the following advantages:
- vs the macros manually preserving everything they clobber:
- The utilities ensure only the necessary registers are preserved. If the caller doesn't care about them (and they often don't) they won't be push/popped, saving 21-28 cycles for each
- vs the caller manually preserving the registers it cares about:
- The utilities ensure the caller doesn't need to know which registers will get clobbered
- If the macros being called are updated and the clobber list ever changes, the new push/pops will be auto-generated without the caller needing to be updated
- Allows an optional auto-preserve mode to make this process easier (at the cost of efficiency)
To automatically preserve all registers by default, consider enabling the utils.registers.AUTO_PRESERVE
setting. When a clobber scope is encountered it will then automatically preserve all registers that get clobbered.
.redefine utils.registers.AUTO_PRESERVE 0 ; deactivate (default)
.redefine utils.registers.AUTO_PRESERVE 1 ; activate
You can opt-out of this on a per-call basis using utils.preserve
to create more-specific preserve scopes for particular calls.
Macros that clobber registers can utilise utils.clobbers
and utils.clobbers.end
in place of the push
/pop
calls they would normally make. This is referred to as a 'clobber scope'.
.macro "normal way"
push af
push de
push hl
...
pop hl ; pop in reverse order
pop de
pop af
.endm
.macro "with utils/clobbers.asm"
utils.clobbers "af" "de" "hl"
... ; code
utils.clobbers.end
.endm
Acceptable arguments are (case-insensitive):
- Main registers:
"af"
,"bc"
,"de"
,"hl"
,"ix"
,"iy"
,"i"
- Shadow registers:
"af'"
,"bc'"
,"de'"
,"hl'"
Clobber scopes can be nested within one another, so when calling other macros it's possible for these to define their own clobber scopes. If you need to call a macro that isn't clobber scope aware, the calling macro will need to take responsibility to wrap the call.
Using utils.clobbers
directly inside sections
could produce unpredictable results as they won't be aware of the context they're call
ed in. You'll instead just need to wrap the call
in its own macro that defines the clobber scopes.
.section "someRoutine" free
someRoutine:
... ; code that clobbers af and hl
ret
.ends
.macro "someRoutine" ; WLA-DX allows you to use the same name if you wish
utils.clobbers "af" "hl"
call someRoutine
utils.clobbers.end
.endm
If the macro produces code with multiple exit points (i.e. jumps that skip over utils.clobbers.end
), you can use utils.clobbers.withBranching
with special jump instructions to ensure relevant registers are restored before the jumps. If there are no registers to restore these will just perform their vanilla jump instructions.
Care should be taken to ensure the jumps don't jump outside of multiple clobber scopes and preserve scopes. The macros don't know where the jump locations are so aren't able to determine if multiple scopes should be restored.
utils.clobbers.withBranching "af"
utils.clobbers.endBranch ; call before unconditional jp or jr
utils.clobbers.end.jrz, _someLabel ; if Z, restore and jr
utils.clobbers.end.jrnz _someLabel ; if NZ, restore and jr
utils.clobbers.end.jrc, _someLabel ; if carry set, restore and jr
utils.clobbers.end.jrnc, _someLabel ; if carry is reset, restore and jr
utils.clobbers.end.jpz _someLabel ; if Z, restore and jp
utils.clobbers.end.jpnz _someLabel ; if NZ, restore and jp
utils.clobbers.end.jpc _someLabel ; if carry, restore and jp
utils.clobbers.end.jpnc _someLabel ; if not carry, restore and jp
utils.clobbers.end.jppe _someLabel ; if parity/overflow, restore and jp
utils.clobbers.end.jppo _someLabel ; if not parity/overflow, restore and jp
utils.clobbers.end.jpm _someLabel ; if sign, restore and jp
utils.clobbers.end.jpp _someLabel ; if not sign, restore and jp
utils.clobbers.end.retc ; return if carry set
utils.clobbers.end.retnc ; return if carry is reset
utils.clobbers.end
In most cases utils.clobbers.withBranching
results in the same performance as utils.clobbers
, but in certain edge cases it knows to opt out of some optimisations to ensure there isn't a mismatch between the registers that get pushed and popped onto the stack.
If using conditional jumps, utils.clobbers.end
is still needed to mark the end of the clobber scope and restore the registers if the jumps don't occur.
If the restore instructions aren't needed (i.e. there's an unconditional jump at the end that will always jump over it) you can use utils.clobbers.closeBranch
to close off the branching clobber scope without generating the uneeded restore instructions.
utils.clobbers.withBranching "af"
utils.clobbers.endBranch ; restore registers
jp somewhere ; unconditional jump
utils.clobbers.closeBranch ; close off branching clobber scope without restoring
Callers that rely on register states to be preserved can wrap the macro invokation with utils.preserve
and utils.restore
. This is referred to as a 'preserve scope'.
.macro "myMacro"
ld bc, $bc00
ld de, $de00
registers.preserve "bc", "de"
; If either of these clobber BC or DE, they will push them to the stack beforehand
macroThatClobsThings ; might push BC or DE
otherMacroThatClobsThings ; might push BC or DE (if not already pushed)
utils.restore ; pops BC/DE if they were pushed
; BC will still be $bc00
; DE will still be $de00
.endm
Acceptable arguments are (case-insensitive):
- Main registers:
"af"
,"bc"
,"de"
,"hl"
,"ix"
,"iy"
,"i"
- Shadow registers:
"af'"
,"bc'"
,"de'"
,"hl'"
As each clobber scope is encountered in the macro chain, it will now be aware which registers the caller wishes to preserve and so preserves any that match. If the call passes through multiple nested clobber scopes that clobber the same particular register, only the first-encountered (outer) scope will preserve the register rather than it being preserved multiple times.
Calling utils.preserve
with no arguments will default to ensuring all registers that get clobbered are preserved.
Like clobber scopes, it's possible to nest preserve scopes. Inner scopes are aware of what registers the outer scope needs preserving.
utils.preserve
can be used within a section
that calls macros.