diff --git a/.gitmodules b/.gitmodules index 89e0d99..f44e124 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,3 @@ -[submodule "lib/zest"] - path = tests/zest +[submodule "tests/lib/zest"] + path = tests/lib/zest url = https://github.com/lajohnston/zest - branch = main diff --git a/README.md b/README.md index 32e2e9c..ea897c1 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,9 @@ See example programs in the `examples` directory. Build the examples using the ` - [vdp.asm](./docs/vdp.md) - Graphics chip register settings ### Additional utils + - [utils/ram.asm](./docs/utils/ram.md) - Utilities for setting values in RAM +- [utils/clobbers.asm, utils/preserve, utils.restore](./docs/utils/registerPreservation.md) - Efficiently preserve Z80 register states ## Design Principles @@ -58,13 +60,17 @@ Over time the routines will be optimised for speed and size without one aspect a ### Unsafe Register Preservation -As part of the speed priority the library routines will happily 'clobber' register values without preserving them onto the stack. This is because `push` and `pop` calls on multiple register pairs is expensive, and you may not even need some of them preserved. +The cost of unecessary `push` and `pop` calls can add up, so for efficiency reasons the library routines will happily clobber registers and rely on the caller to preserve only what it actually needs. In many cases the caller can be refactored to avoid preserving to registers entirely. + +View [utils/registerPreservation.md](./docs/utils/registerPreservation.md)) for some alternative strategies, which include: -The register values before and after a call may therefore change. This shifts the responsibility of preservation to you, mainly for efficiency reasons; you know best what registers you actually care about, so only need to preserve those. The flipside of this is that it is a bit of a 'gotcha' to be aware of. +1. Enabling the `utils.registers.AUTO_PRESERVE` setting to ensure all registers are preserved by default +2. Wrapping macro calls in `utils.preserve` and `utils.restore` to state which registers the caller wants preserving; only the registers that get clobbered will be `push`/`pop`ped +3. A combination of the two; enable auto-preserve but override it where possible by using `utils.preserve` and `utils.restore` to only preserve what each caller needs ### Decoupled -SMSLib modules for the most part are independent of one another so you can pick or choose only the ones you want. For convenience you can just include `smslib.asm` and this will pull the main ones in for you. In either case WLA-DX will only assemble the code you actually use. +SMSLib modules for the most part are independent of one another so you can pick and choose only the ones you want. For convenience you can just include `smslib.asm` and this will pull the main ones in for you. In either case WLA-DX will only assemble the code you actually use so long as it's not executed with the `-k` (keep) option. ### Namespaced Prefixes diff --git a/docs/input.md b/docs/input.md index 33797c3..e960612 100644 --- a/docs/input.md +++ b/docs/input.md @@ -213,30 +213,45 @@ input.ifYDirReleased, up, down, + +: ``` -### input.loadADirX +### input.loadDirX -Loads register A with the directional x-axis: -1 = left, 1 = right, none = 0. An optional multiplier multiplies this result at assemble time. +Loads the given register with the directional x-axis: -1 = left, 1 = right, none = 0. An optional multiplier multiplies this result at assemble time. ```asm input.readPort1 -input.loadADirX ; left = -1, right = 1, none = 0 -input.loadADirX 5 ; left = -5, right = 5, none = 0 +input.loadDirX "a" ; left = -1, right = 1, none = 0 +input.loadDirX "a", 5 ; left = -5, right = 5, none = 0 ; Reverse with a negative multiplier -input.loadADirX -1 ; left = 1, right = -1, none = 0 +input.loadDirX "a" -1 ; left = 1, right = -1, none = 0 + +; Load a different register +input.loadDirX "b" +input.loadDirX "c" +input.loadDirX "bc" +input.loadDirX "de" +input.loadDirX "hl", 6 +; etc. ``` -### input.loadADirY +### input.loadDirY -Loads register A with the directional y-axis: -1 = up, 1 = down, none = 0. An optional multiplier multiplies this result at assemble time. +Loads the given register with the directional y-axis: -1 = up, 1 = down, none = 0. An optional multiplier multiplies this result at assemble time. ```asm input.readPort1 -input.loadADirX ; up = -1, down = 1, none = 0 -input.loadADirX 4 ; up = -4, down = 4, none = 0 +input.loadDirY "a" ; up = -1, down = 1, none = 0 +input.loadDirY "a", 4 ; up = -4, down = 4, none = 0 ; Reverse with a negative multiplier -input.loadADirX -1 ; up = 1, down = -1, none = 0 -``` \ No newline at end of file +input.loadDirY "a", -1 ; up = 1, down = -1, none = 0 + +; Load a different register +input.loadDirY "b" +input.loadDirY "c" +input.loadDirY "bc" +input.loadDirY "de", 10 +; etc. +``` diff --git a/docs/palette.md b/docs/palette.md index d37f3b5..bf4e0c0 100644 --- a/docs/palette.md +++ b/docs/palette.md @@ -36,9 +36,14 @@ You can then write these into the VDP VRAM the following macros. Set the color palette index ready to write to: ```asm -palette.setIndex 0 ; set the first color index -palette.setIndex palette.SPRITE_PALETTE ; first color in 'sprite' palette -palette.setIndex palette.SPRITE_PALETTE + 1 ; second color in 'sprite' palette +palette.setIndex 0 ; set the first color index +palette.setIndex 31 ; set the last color index + +palette.setIndex palette.BACKGROUND ; 1st color in background palette (index 0) +palette.setIndex palette.BACKGROUND + 1 ; 2nd color in background palette (index 1) + +palette.setIndex palette.SPRITE ; 1st color in sprite palette (index 16) +palette.setIndex palette.SPRITE + 1 ; 2nd color in sprite palette (index 17) ``` ### palette.writeBytes diff --git a/docs/patterns.md b/docs/patterns.md index 4db720a..1e1ddc3 100644 --- a/docs/patterns.md +++ b/docs/patterns.md @@ -1,10 +1,39 @@ # Patterns (patterns.asm) -Loads patterns (tiles) into the VRAM, which can be used for background images or sprites. +Loads patterns (tiles) into VRAM, which can be used for background images or sprites. -This only deals with uncompressed tile data and is provided for example purposes to get you started. For an actual game you would want to compress pattern data using an algorithm such as zx7 or aPLib and use the appropriate lib to decompress the data and write it to VRAM. +This module only deals with uncompressed tile data and is provided for example purposes to get you started. For an actual game you might want to compress pattern data using an algorithm such as zx7 or aPLib and use the appropriate lib to decompress the data and write it to VRAM. -## patterns.writeBytes +## Data format + +Patterns are an image of 8x8 (64) pixels. Each pixel is a 4-bit (half a byte) color palette index reference, making a total of 32-bytes (64 / 2). + +The 4-bit color palette index value (0-15) can reference either index 0-15 or index 16-31 of the palette, depending on the context where the pattern is used; sprites use indices 16-31 whereas background tiles in the tilemap contain a bit that determines which to use. If the pattern is used for a sprite then the first color (index 16) is used as the 'transparent' color and is not visible. + +Each row consists of 4 bytes. The pixels in each are encoded in bitplanes, so the bits of each are strewn across these 4-bytes. This is hard to explain, but given 8 pixels in a row (A, B, C, D, E, F, G, H), the row will be encoded as: + +- Byte 0: A0 B0 C0 D0 E0 F0 G0 H0 +- Byte 1: A1 B1 C1 D1 E1 F1 G1 H1 +- Byte 2: A2 B2 C2 D2 E2 F2 G2 H2 +- Byte 3: A3 B3 C3 D3 E3 F3 G3 H3 + +If pixel A's index was **1111** and the rest were 0000, the encoding would be: + +- Byte 0: **1**000 0000 +- Byte 1: **1**000 0000 +- Byte 2: **1**000 0000 +- Byte 3: **1**000 0000 + +If pixel B's index was **1101** and the rest were 0000, the encoding would be: + +- Byte 0: 0**1**00 0000 +- Byte 1: 0**1**00 0000 +- Byte 2: 0**0**00 0000 +- Byte 3: 0**1**00 0000 + +## Usage + +### patterns.writeBytes Writes uncompressed pattern data into VRAM. @@ -19,7 +48,7 @@ patterns.setIndex 16 patterns.writeBytes uncompressedPatternData, patternDataSize ``` -## patterns.writeSlice +### patterns.writeSlice Lets you pick out certain tiles from the binary data and write them individually. @@ -42,30 +71,9 @@ An optional third parameter lets you skip a certain number of patterns in the da patterns.writeSlice otherPatternData, 1, 9 ``` -## Data format - -Patterns are an image of 8x8 pixels. Each pixel is a 4-bit color palette index reference, making a total of 32-bytes. - -The 4-bit color palette index value (0-15) can reference either index 0-15 or index 16-31 of the palette, depending on the context where the pattern is used; sprites use indices 16-31 whereas background tiles in the tilemap contain a bit that determines which to use. If the pattern is used for a sprite then color index 0 is used as the transparent color (i.e. not drawn) - -Pixels are encoded in bitplanes so the bits of each are strewn across 2-bytes. This is hard to explain, but given 4 pixels (A, B, C, D), the ordering of each bit will be bitplane encoded as: - -- Byte 1: A1 B1 C1 D1, A2 B2 C2 D2 -- Byte 2: A3 B3 C3 D3, A4 B4 C4 D4 - -If A was 1111 and the rest were 0000, the encoding would be: - -- Byte 1: **1**000 **1**000 -- Byte 2: **1**000 **1**000 - -If B was 1101 and the rest were 0000, the encoding would be: - -- Byte 1: 0**1**00 0**1**00 (first two bits- 1, 1) -- Byte 2: 0**0**00 0**1**00 (second two bits - 0, 1) - -## Settings +### Settings -If you wish to change these defaults, use `.define` to define these value at some point before you import the library. +If you wish to change the below defaults, use `.define` to define these value at some point before you import the library. ### patterns.VRAM_ADDRESS diff --git a/docs/tilemap.md b/docs/tilemap.md index 46af120..c6c3881 100644 --- a/docs/tilemap.md +++ b/docs/tilemap.md @@ -128,11 +128,11 @@ A suggested workflow is to maintain a pointer to the top-left visible tile of yo 1. Adjust the tilemap by x and y number of pixels 2. If row or column scrolling detected, adjust row/col of the top-left pointer - - (Optional) Prevent the pointer going out of bounds -4. Write the new row and/or column into the RAM buffers - - Right edge will be top-left pointer + 31 columns - - Bottom edge will be top-left pointer + 24 rows -5. During VBlank, transfer these changes from the RAM buffer to the VDP + - (Optional) Prevent the pointer going out of bounds +3. Write the new row and/or column into the RAM buffers + - Right edge will be top-left pointer + 31 columns + - Bottom edge will be top-left pointer + 24 rows +4. During VBlank, transfer these changes from the RAM buffer to the VDP ### Adjust position @@ -225,7 +225,7 @@ If a column scroll is detected you can transfer the new column data to the colum tilemap.loadDEColBuffer ; point DE to the column buffer tilemap.loadBColBytes ; load B with the number of bytes to write to the buffer -tilemap.loadBCColBytes ; or, load BC with the value (for use with ldi) +tilemap.loadBCColBytes ; or, load BC with the value (for use with ldi/ldir) ``` You will need a routine to write sequential tile data for a column from top to bottom. The routine should write a tile, jump to the next row, then write another tile, until the byte counter is 0. @@ -250,6 +250,7 @@ During VBlank you can safely write the buffered data to VRAM. The easiest way to ```asm tilemap.writeScrollBuffers ``` + ### Bounds checking If you detect a row or column scroll will take the tilemap out of bounds, you can call the following scroll-stop routines to stop the column and/or row scroll from happening. This will set the pixel scroll to the farthest edge of the in-bounds tiles, but later calls to `tilemap.ifRowScroll` and/or `tilemap.ifColScroll` will no longer detect a scroll and thus not trigger out-of-bounds rows or columns to be drawn. diff --git a/docs/utils/registerPreservation.md b/docs/utils/registerPreservation.md new file mode 100644 index 0000000..881e7e1 --- /dev/null +++ b/docs/utils/registerPreservation.md @@ -0,0 +1,153 @@ +# Register preservation (utils.clobbers, utils.preserve, utils.restore) + +These utility modules provide a system for efficiently preserving Z80 register states between macro calls, ensuring only the needed registers are preserved. + +1. The internal macros use [utils.clobbers](#utilsclobbers) to declare which registers they clobber (in lieu of push/pop calls). You can utilise them too +2. The caller can utilise [utils.preserve](#utilspreserve) to wrap the calls and declare which registers should not be clobbered +3. The utilities will work together to ensure only the clobbered AND protected registers get preserved + +This provides the following advantages: + +1. 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 + +2. 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 + +3. Allows an optional [auto-preserve](#automatic-registers-preseveration) mode to make this process easier (at the cost of efficiency) + +## Automatic registers preservation + +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. + +```asm +.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. + +## utils.clobbers + +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'. + +```asm +.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. + +```asm +.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 +``` + +### utils.clobbers.withBranching + +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. + +```asm +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. + +#### utils.clobbers.closeBranch + +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. + +```asm +utils.clobbers.withBranching "af" + utils.clobbers.endBranch ; restore registers + jp somewhere ; unconditional jump +utils.clobbers.closeBranch ; close off branching clobber scope without restoring +``` + +## utils.preserve + +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'. + +```asm +.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. diff --git a/examples/01-helloWorld/main.asm b/examples/01-helloWorld/main.asm index 789073f..fe491f4 100644 --- a/examples/01-helloWorld/main.asm +++ b/examples/01-helloWorld/main.asm @@ -6,7 +6,7 @@ ;==== ; Import smslib ;==== -.incdir "../../" ; point to smslib directory +.incdir "../../src" ; point to smslib directory .include "smslib.asm" .incdir "." ; point back to current working directory diff --git a/examples/02-staticSprites/main.asm b/examples/02-staticSprites/main.asm index 8871e92..4af6829 100644 --- a/examples/02-staticSprites/main.asm +++ b/examples/02-staticSprites/main.asm @@ -8,7 +8,7 @@ ;==== ; Import smslib ;==== -.incdir "../../" ; back to smslib directory +.incdir "../../src" ; back to smslib directory .include "smslib.asm" ; base library .incdir "." ; back to current directory @@ -44,7 +44,7 @@ palette.writeRgb 0, 0, 0 ; black ; Load sprite palette - palette.setIndex palette.SPRITE_PALETTE + palette.setIndex palette.SPRITE palette.writeSlice bubblePalette, 6 ; Load pattern data into indices 256+ (used for sprites, by default) diff --git a/examples/03-vblank/main.asm b/examples/03-vblank/main.asm index abc487e..f8ff66e 100644 --- a/examples/03-vblank/main.asm +++ b/examples/03-vblank/main.asm @@ -21,7 +21,7 @@ ; Import smslib .define interrupts.HANDLE_VBLANK 1 ; enable VBlank handling in interrupts.asm -.incdir "../../" ; back to smslib directory +.incdir "../../src" ; back to smslib directory .include "smslib.asm" ; base library .incdir "." ; return to current directory diff --git a/examples/04-input/main.asm b/examples/04-input/main.asm index 274a3c1..8662f68 100644 --- a/examples/04-input/main.asm +++ b/examples/04-input/main.asm @@ -9,7 +9,7 @@ ; Import smslib .define interrupts.HANDLE_VBLANK 1 ; enable VBlank handling in interrupts.asm .define input.ENABLE_PORT_2 ; enable reading port 2 -.incdir "../../" ; point to smslib directory +.incdir "../../src" ; point to smslib directory .include "smslib.asm" ; base library .incdir "." ; return to current directory diff --git a/examples/05-movingSprites/bubble.asm b/examples/05-movingSprites/bubble.asm index 84d89e3..7af3825 100644 --- a/examples/05-movingSprites/bubble.asm +++ b/examples/05-movingSprites/bubble.asm @@ -35,10 +35,10 @@ input.readPort1 ; Set movement vectors based on input - input.loadADirX 2 ; left = -2; right = 2; none = 0 + input.loadDirX, "a", 2 ; left = -2; right = 2; none = 0 ld (ix + Bubble.xVec), a ; update x movement based on input - input.loadADirY 2 ; up = -2; down = 2; none = 0 + input.loadDirY "a", 2 ; up = -2; down = 2; none = 0 ld (ix + Bubble.yVec), a ; update y movement based on input ret diff --git a/examples/05-movingSprites/main.asm b/examples/05-movingSprites/main.asm index 118be91..8c2938f 100644 --- a/examples/05-movingSprites/main.asm +++ b/examples/05-movingSprites/main.asm @@ -15,7 +15,7 @@ .define interrupts.HANDLE_VBLANK 1 ; Import smslib -.incdir "../../" ; back to smslib directory +.incdir "../../src" ; back to smslib directory .include "smslib.asm" ; base library .incdir "." ; return to current directory @@ -39,7 +39,7 @@ palette.writeRgb 0, 0, 0 ; black ; Load sprite palette - palette.setIndex palette.SPRITE_PALETTE + palette.setIndex palette.SPRITE palette.writeBytes bubble.palette, bubble.paletteSize ; Load pattern data to draw sprites. Sprites use indices 256+ by default diff --git a/examples/06-paging/main.asm b/examples/06-paging/main.asm index eead0a1..979e4c7 100644 --- a/examples/06-paging/main.asm +++ b/examples/06-paging/main.asm @@ -13,7 +13,7 @@ .define interrupts.HANDLE_VBLANK 1 ; enable VBlank handling in interrupts.asm .define mapper.ENABLE_CARTRIDGE_RAM 1 -.incdir "../../" ; point to smslib directory +.incdir "../../src" ; point to smslib directory .include "mapper/sega.asm" ; select Sega mapper. Note: if you swap this ; another one this example should still work .include "smslib.asm" ; include rest of smslib lib diff --git a/examples/07-scrolling-tilemap/main.asm b/examples/07-scrolling-tilemap/main.asm index c067125..d7c1e9d 100644 --- a/examples/07-scrolling-tilemap/main.asm +++ b/examples/07-scrolling-tilemap/main.asm @@ -11,7 +11,7 @@ ; Import smslib ;==== .define interrupts.HANDLE_VBLANK 1 ; enable VBlanks (see VBlank example) -.incdir "../../" ; point to smslib directory +.incdir "../../src" ; point to smslib directory .include "smslib.asm" ; Include a scroll handler that uses raw tile data @@ -81,11 +81,11 @@ ; Adjust tilemap based on joypad input direction input.readPort1 ; read the state of joypad 1 - input.loadADirX SCROLL_SPEED; load A with X direction * scroll speed - scroll.tiles.adjustXPixels ; adjust tilemap X by that many pixels + input.loadDirX "a", SCROLL_SPEED ; load A with X direction * scroll speed + scroll.tiles.adjustXPixels ; adjust tilemap X by that many pixels - input.loadADirY SCROLL_SPEED; load A with Y direction * scroll speed - scroll.tiles.adjustYPixels ; adjust tilemap Y by that many pixels + input.loadDirY "a", SCROLL_SPEED ; load A with Y direction * scroll speed + scroll.tiles.adjustYPixels ; adjust tilemap Y by that many pixels ; Buffer changes in RAM (doesn't update VDP/VRAM yet) scroll.tiles.update diff --git a/examples/08-scrolling-metatiles/main.asm b/examples/08-scrolling-metatiles/main.asm index 7e18cb5..14a4983 100644 --- a/examples/08-scrolling-metatiles/main.asm +++ b/examples/08-scrolling-metatiles/main.asm @@ -14,7 +14,7 @@ ;==== ; Import smslib ;==== -.incdir "../../" ; point to smslib directory +.incdir "../../src" ; point to smslib directory .define interrupts.HANDLE_VBLANK 1 ; enable VBlank handling in interrupts.asm .include "smslib.asm" @@ -133,11 +133,11 @@ ;=== input.readPort1 ; read the state of joypad 1 - input.loadADirX SCROLL_SPEED ; load A with X direction * scroll speed - scroll.metatiles.adjustXPixels ; adjust tilemap X by that many pixels + input.loadDirX "a", SCROLL_SPEED ; load A with X direction * scroll speed + scroll.metatiles.adjustXPixels ; adjust tilemap X by that many pixels - input.loadADirY SCROLL_SPEED ; load A with Y direction * scroll speed - scroll.metatiles.adjustYPixels ; adjust tilemap Y by that many pixels + input.loadDirY "a", SCROLL_SPEED ; load A with Y direction * scroll speed + scroll.metatiles.adjustYPixels ; adjust tilemap Y by that many pixels ; Update RAM buffers (doesn't update VDP/VRAM yet) scroll.metatiles.update diff --git a/init.asm b/src/init.asm similarity index 100% rename from init.asm rename to src/init.asm diff --git a/input.asm b/src/input.asm similarity index 63% rename from input.asm rename to src/input.asm index 22f872c..c6e342d 100644 --- a/input.asm +++ b/src/input.asm @@ -19,11 +19,15 @@ ; Dependencies ;==== .ifndef utils.assert - .include "./utils/assert.asm" + .include "utils/assert.asm" +.endif + +.ifndef utils.clobbers + .include "utils/clobbers.asm" .endif .ifndef utils.port - .include "./utils/port.asm" + .include "utils/port.asm" .endif .ifndef utils.ram @@ -31,6 +35,10 @@ utils.ram.assertRamSlot .endif +.ifndef utils.registers + .include "utils/registers.asm" +.endif + ;==== ; Constants ;==== @@ -73,14 +81,16 @@ ;==== .macro "input.init" ; Initialise all buttons to released - xor a - ld (input.ram.activePort.current), a - ld (input.ram.activePort.previous), a + utils.clobbers "af" + xor a + ld (input.ram.activePort.current), a + ld (input.ram.activePort.previous), a - .ifdef input.ENABLE_PORT_2 - ld (input.ram.previous.port1), a - ld (input.ram.previous.port2), a - .endif + .ifdef input.ENABLE_PORT_2 + ld (input.ram.previous.port1), a + ld (input.ram.previous.port2), a + .endif + utils.clobbers.end .endm ;==== @@ -99,25 +109,29 @@ ;==== .macro "input.readPort1" .ifdef input.ENABLE_PORT_2 - ; Copy previous value of port 1 to activePort.previous - ld a, (input.ram.previous.port1) ; load previous.port1 - ld (input.ram.activePort.previous), a ; store in activePort.previous - - ; Load current port 1 input and store in activePort.current - utils.port.read input.PORT_1 ; load input - xor $ff ; invert so 1 = pressed and 0 = released - ld (input.ram.activePort.current), a ; store in activePort.current - ld (input.ram.previous.port1), a ; store in previous.port1 for next time + utils.clobbers "af" + ; Copy previous value of port 1 to activePort.previous + ld a, (input.ram.previous.port1) ; load previous.port1 + ld (input.ram.activePort.previous), a ; store in activePort.previous + + ; Load current port 1 input and store in activePort.current + utils.port.read input.PORT_1 ; load input + xor $ff ; invert so 1 = pressed and 0 = released + ld (input.ram.activePort.current), a ; store in activePort.current + ld (input.ram.previous.port1), a ; store in previous.port1 for next time + utils.clobbers.end .else - ld a, (input.ram.activePort.current) ; load previous input - ld h, a ; store in H as previous value + utils.clobbers "af", "hl" + ld a, (input.ram.activePort.current) ; load previous input + ld h, a ; store in H as previous value - utils.port.read input.PORT_1 ; load input - xor $ff ; invert so 1 = pressed and 0 = released - ld l, a ; set to L + utils.port.read input.PORT_1 ; load input + xor $ff ; invert so 1 = pressed and 0 = released + ld l, a ; set to L - ; Set activePort current to L and previous to H - ld (input.ram.activePort.current), hl + ; Set activePort current to L and previous to H + ld (input.ram.activePort.current), hl + utils.clobbers.end .endif .endm @@ -166,7 +180,9 @@ .fail .endif - call input._readPort2 + utils.clobbers "af" "bc" + call input._readPort2 + utils.clobbers.end .endm ;==== @@ -176,10 +192,12 @@ ; @out a held buttons (--21RLDU) ;==== .macro "input.loadAHeld" - ; Load current input into L and previous into H - ld hl, (input.ram.activePort.current) - ld a, l ; load current into A - and h ; AND with previous + utils.clobbers "hl" + ; Load current input into L and previous into H + ld hl, (input.ram.activePort.current) + ld a, l ; load current into A + and h ; AND with previous + utils.clobbers.end .endm ;==== @@ -188,11 +206,13 @@ ; @out a the just-pressed buttons (--21RLDU) ;==== .macro "input.loadAPressed" - ; Load L with current input value and H with previous - ld hl, (input.ram.activePort.current) - ld a, l ; load current into A - xor h ; XOR with previous. The set bits are now buttons that have changed - and l ; AND with current; Set bits have changed AND are currently pressed + utils.clobbers "hl" + ; Load L with current input value and H with previous + ld hl, (input.ram.activePort.current) + ld a, l ; load current into A + xor h ; XOR with previous. The set bits are now buttons that have changed + and l ; AND with current; Set bits have changed AND are currently pressed + utils.clobbers.end .endm ;==== @@ -201,11 +221,13 @@ ; @out a released buttons (--21RLDU) ;==== .macro "input.loadAReleased" - ; Load L with current input value and H with previous - ld hl, (input.ram.activePort.current) - ld a, l ; load current into A - xor h ; XOR with previous. The set bits are now buttons that have changed - and h ; AND with previous; Set bits have changed AND are not currently pressed + utils.clobbers "hl" + ; Load L with current input value and H with previous + ld hl, (input.ram.activePort.current) + ld a, l ; load current into A + xor h ; XOR with previous. The set bits are now buttons that have changed + and h ; AND with previous; Set bits have changed AND are not currently pressed + utils.clobbers.end .endm ;==== @@ -219,9 +241,11 @@ utils.assert.range \1, input.UP, input.BUTTON_2, "input.asm \.: Invalid button argument" utils.assert.label \2, "input.asm \.: Invalid else argument" - ld a, (input.ram.activePort.current) - and \1 ; check button bit - jp z, \2 ; jp to else if the bit was not set + utils.clobbers.withBranching "af" + ld a, (input.ram.activePort.current) + and \1 ; check button bit + utils.clobbers.end.jpz \2 ; jp to else if the bit was not set + utils.clobbers.end .else ;=== ; Check if multiple buttons are pressed @@ -239,10 +263,12 @@ ; Assert remaining \1 argument is the else label utils.assert.label \1, "input.asm \.: Expected last argument to be a label" - ld a, (input.ram.activePort.current) - and mask\.\@ ; clear other buttons - cp mask\.\@ ; compare result with mask - jp nz, \1 ; jp to else if not all buttons are pressed + utils.clobbers.withBranching "af" + ld a, (input.ram.activePort.current) + and mask\.\@ ; clear other buttons + cp mask\.\@ ; compare result with mask + utils.clobbers.end.jpnz \1 ; jp to else if not all buttons are pressed + utils.clobbers.end .endif .endm @@ -258,9 +284,11 @@ utils.assert.range \1, input.UP, input.BUTTON_2, "input.asm \.: Invalid button argument" utils.assert.label \2, "input.asm \.: Invalid else argument" - input.loadAHeld ; load A with held buttons - and \1 ; check button bit - jp z, \2 ; jp to else if the bit was not set + utils.clobbers.withBranching "af" + input.loadAHeld ; load A with held buttons + and \1 ; check button bit + utils.clobbers.end.jpz \2 ; jp to else if the bit was not set + utils.clobbers.end .else ;=== ; Check if multiple buttons are pressed @@ -278,10 +306,12 @@ ; Assert remaining \1 argument is the else label utils.assert.label \1, "input.asm \.: Expected last argument to be a label" - input.loadAHeld ; load A with held buttons - and mask\.\@ ; clear other buttons - cp mask\.\@ ; compare result with mask - jp nz, \1 ; jp to else if not all buttons are held + utils.clobbers.withBranching "af" + input.loadAHeld ; load A with held buttons + and mask\.\@ ; clear other buttons + cp mask\.\@ ; compare result with mask + utils.clobbers.end.jpnz \1 ; jp to else if not all buttons are held + utils.clobbers.end .endif .endm @@ -298,9 +328,11 @@ utils.assert.label \2, "input.asm \.: Invalid label argument" ; Load input that was released last frame but is now pressed - input.loadAPressed - and \1 ; check button bit - jp z, \2 ; jp to else if the bit was not set + utils.clobbers.withBranching "af" + input.loadAPressed + and \1 ; check button bit + utils.clobbers.end.jpz \2 ; jp to else if the bit was not set + utils.clobbers.end .else ;=== ; Check if all buttons are pressed, and that not all of them were pressed @@ -320,21 +352,25 @@ ; Assert remaining \1 argument is the else label utils.assert.label \1, "input.asm \.: Expected last argument to be a label" - ; Load H with previous and L with current - ld hl, (input.ram.activePort.current) - - ; If all given buttons are currently pressed - ld a, l ; load current into A - ld l, mask\.\@ ; load buttons mask into L - and l ; filter out other buttons - cp l ; check if all given buttons are currently pressed - jp nz, \1 ; jump if all given buttons aren't currently pressed - - ; All given buttons are pressed; Check if they were pressed last frame - ld a, h ; load previous input - and l ; filter out other buttons - cp l ; check if all given buttons are currently pressed - jp z, \1 ; jp to else if all were already pressed last frame + utils.clobbers.withBranching "af" "hl" + ; Load H with previous and L with current + ld hl, (input.ram.activePort.current) + + ; If all given buttons are currently pressed + ld a, l ; load current into A + ld l, mask\.\@ ; load buttons mask into L + and l ; filter out other buttons + cp l ; check if all given buttons are currently pressed + utils.clobbers.end.jpnz \1 ; jp if all given buttons aren't pressed + + ; All given buttons are pressed; Check if they were pressed last frame + ld a, h ; load previous input + and l ; filter out other buttons + cp l ; check if all given buttons are currently pressed + + ; Jump to else if all were already pressed last frame + utils.clobbers.end.jpz \1 + utils.clobbers.end .endif .endm @@ -351,9 +387,11 @@ utils.assert.label \2, "input.asm \.: Invalid label argument" ; Load input that was released last frame but is now pressed - input.loadAReleased - and \1 ; check button bit - jp z, \2 ; jp to else if the bit was not set + utils.clobbers.withBranching "af" + input.loadAReleased + and \1 ; check button bit + utils.clobbers.end.jpz \2 ; jp to else if the bit was not set + utils.clobbers.end .else ; OR button masks together to create a single mask .define mask\.\@ 0 @@ -371,28 +409,36 @@ ; Check if all buttons had been pressed last frame, but not all are now ;=== - ; Load H with previous and L with current - ld hl, (input.ram.activePort.current) + utils.clobbers.withBranching "af" "hl" + ; Load H with previous and L with current + ld hl, (input.ram.activePort.current) - ; If all given buttons were pressed - ld a, h ; load previous into A - ld h, mask\.\@ ; load buttons mask into H - and h ; filter out other buttons - cp h ; check if all given buttons were pressed - jp nz, \1 ; jump if all given buttons weren't pressed last frame - - ; All given buttons were pressed; Check if any are now released - ld a, l ; load current input - and h ; filter out other buttons - cp h ; check if all given buttons are currently pressed - jp z, \1 ; jp to else if all are still pressed + ; If all given buttons were pressed + ld a, h ; load previous into A + ld h, mask\.\@ ; load buttons mask into H + and h ; filter out other buttons + cp h ; check if all given buttons were pressed + + ; Jump if all given buttons weren't pressed last frame + utils.clobbers.end.jpnz \1 + + ; All given buttons were pressed; Check if any are now released + ld a, l ; load current input + and h ; filter out other buttons + cp h ; check if all given buttons are currently pressed + + ; Jump to else if all are still pressed + utils.clobbers.end.jpz \1 + utils.clobbers.end .endif .endm ;==== ; Checks if either direction on an axis is pressed and jumps to the relevant ; label. If the negative direction (left or up) on the axis is pressed, it -; will continue the code flow without jumping +; will continue the code flow without jumping. +; +; Should be called within a utils.clobbers.withBranching scope protecting AF ; ; @in a input value to check (--21RLDU) ; @@ -413,11 +459,11 @@ utils.assert.label positiveLabel "input.asm \.: Invalid 'positiveLabel' argument" utils.assert.label elseLabel "input.asm \.: Invalid 'elseLabel' argument" - and negativeDir | positiveDir ; check if either direction is pressed - jp z, elseLabel ; jump to else label if neither are pressed + and negativeDir | positiveDir ; check if either direction is pressed + utils.clobbers.end.jpz elseLabel ; jp to else label if neither are pressed - and positiveDir ; check positive direction - jp nz, positiveLabel ; jump if positive direction pressed + and positiveDir ; check positive direction + utils.clobbers.end.jpnz positiveLabel ; jp if positive direction pressed ; ...continue to the negativeLabel handler .endm @@ -436,8 +482,10 @@ utils.assert.label else "input.asm \.: Invalid 'else' argument" ; Load currently pressed input and jump to relevant label - ld a, (input.ram.activePort.current) - input._jpIfDirection input.LEFT input.RIGHT left right else + utils.clobbers.withBranching "af" + ld a, (input.ram.activePort.current) + input._jpIfDirection input.LEFT input.RIGHT left right else + utils.clobbers.end .endm ;==== @@ -454,9 +502,11 @@ utils.assert.label right "input.asm \.: Invalid 'right' argument" utils.assert.label else "input.asm \.: Invalid 'else' argument" - ; Load held input and jump to relevant label - input.loadAHeld - input._jpIfDirection input.LEFT input.RIGHT left right else + utils.clobbers.withBranching "af" + ; Load held input and jump to relevant label + input.loadAHeld + input._jpIfDirection input.LEFT input.RIGHT left right else + utils.clobbers.end .endm ;==== @@ -473,11 +523,13 @@ utils.assert.label right "input.asm \.: Invalid 'right' argument" utils.assert.label else "input.asm \.: Invalid 'else' argument" - ; Load input that was released last frame but is now pressed - input.loadAPressed + utils.clobbers.withBranching "af" + ; Load input that was released last frame but is now pressed + input.loadAPressed - ; Jump to the relevant label - input._jpIfDirection input.LEFT input.RIGHT left right else + ; Jump to the relevant label + input._jpIfDirection input.LEFT input.RIGHT left right else + utils.clobbers.end .endm ;==== @@ -494,11 +546,13 @@ utils.assert.label right "input.asm \.: Invalid 'right' argument" utils.assert.label else "input.asm \.: Invalid 'else' argument" - ; Load input that was pressed last frame but has just been released - input.loadAReleased + utils.clobbers.withBranching "af" + ; Load input that was pressed last frame but has just been released + input.loadAReleased - ; Jump to the relevant label - input._jpIfDirection input.LEFT input.RIGHT left right else + ; Jump to the relevant label + input._jpIfDirection input.LEFT input.RIGHT left right else + utils.clobbers.end .endm ;==== @@ -515,8 +569,10 @@ utils.assert.label else "input.asm \.: Invalid 'else' argument" ; Load currently pressed direction and jump to the relevant label - ld a, (input.ram.activePort.current) - input._jpIfDirection input.UP input.DOWN up down else + utils.clobbers.withBranching "af" + ld a, (input.ram.activePort.current) + input._jpIfDirection input.UP input.DOWN up down else + utils.clobbers.end .endm ;==== @@ -534,8 +590,10 @@ utils.assert.label else "input.asm \.: Invalid 'else' argument" ; Load held buttons and jump to the relevant label - input.loadAHeld - input._jpIfDirection input.UP input.DOWN up down else + utils.clobbers.withBranching "af" + input.loadAHeld + input._jpIfDirection input.UP input.DOWN up down else + utils.clobbers.end .endm ;==== @@ -552,11 +610,13 @@ utils.assert.label down "input.asm \.: Invalid 'down' argument" utils.assert.label else "input.asm \.: Invalid 'else' argument" - ; Load input that was released last frame but is now pressed - input.loadAPressed + utils.clobbers.withBranching "af" + ; Load input that was released last frame but is now pressed + input.loadAPressed - ; Jump to the relevant label - input._jpIfDirection input.UP input.DOWN up down else + ; Jump to the relevant label + input._jpIfDirection input.UP input.DOWN up down else + utils.clobbers.end .endm ;==== @@ -573,31 +633,40 @@ utils.assert.label down "input.asm \.: Invalid 'down' argument" utils.assert.label else "input.asm \.: Invalid 'else' argument" - ; Load input that was pressed but has just been released - input.loadAReleased + utils.clobbers.withBranching "af" + ; Load input that was pressed but has just been released + input.loadAReleased - ; Jump to the relevant label - input._jpIfDirection input.UP input.DOWN up down else + ; Jump to the relevant label + input._jpIfDirection input.UP input.DOWN up down else + utils.clobbers.end .endm ;==== -; Load the X direction (left/right) into register A. By default, -1 = left, +; Load the X direction (left/right) into the given register. By default, -1 = left, ; 1 = right, 0 = none. The result is multiplied by the optional multiplier ; at assemble time ; ; Ensure you have called input.readPort1 or input.readPort2 ; +; @in register the register/pair to load the value into (a, b, c, d, e, hl, de etc.) ; @in [multiplier] optional multiplier for the result (default 1) ; @out a -1 = left, 1 = right, 0 = none. This value will be ; multiplied by the multiplier at assemble time ;==== -.macro "input.loadADirX" isolated args multiplier +.macro "input.loadDirX" isolated args register multiplier .ifndef multiplier .redefine multiplier 1 .endif + utils.assert.string register, "input.asm \.: Expected register to be a string" utils.assert.number multiplier, "input.asm \.: Expected multiplier to be a number" + ; If return register isn't A, mark it as clobbered + .if register != "a" + utils.clobbers "af" + .endif + ; Read current input data ld a, (input.ram.activePort.current) @@ -605,7 +674,7 @@ bit input.LEFT_BIT, a jp z, + ; Left is pressed - ld a, -1 * multiplier + utils.registers.load register, -1 * multiplier jp \.\@end +: @@ -613,14 +682,23 @@ bit input.RIGHT_BIT, a jp z, + ; Right is pressed - ld a, 1 * multiplier + utils.registers.load register, 1 * multiplier jp \.\@end +: ; Nothing pressed - xor a ; a = 0 + .if register == "a" + xor a ; a = 0 + .else + utils.registers.load register, 0 + .endif \.\@end: + + ; If return register wasn't A, mark the end of the clobber scope + .if register != "a" + utils.clobbers.end + .endif .endm ;==== @@ -628,19 +706,24 @@ ; 1 = down, 0 = none. The result is multiplied by the optional multiplier ; at assemble time ; -; Ensure you have called input.readPort1 or input.readPort2 -; +; @in register the register/pair to load the value into (a, b, c, d, e, hl, de etc.) ; @in [multiplier] optional multiplier for the result (default 1) -; @out a -1 = up, 1 = down, 0 = none. This will be multiplied -; by the multiplier at assemble time +; @out a -1 = left, 1 = right, 0 = none. This value will be +; multiplied by the multiplier at assemble time ;==== -.macro "input.loadADirY" isolated args multiplier +.macro "input.loadDirY" isolated args register multiplier .ifndef multiplier .redefine multiplier 1 .endif + utils.assert.string register, "input.asm \.: Expected register to be a string" utils.assert.number multiplier, "input.asm \.: Expected multiplier to be a number" + ; If return register isn't A, mark it as clobbered + .if register != "a" + utils.clobbers "af" + .endif + ; Read current input data ld a, (input.ram.activePort.current) @@ -648,7 +731,7 @@ bit input.UP_BIT, a jp z, + ; Up is pressed - ld a, -1 * multiplier + utils.registers.load register, -1 * multiplier jp \.\@end +: @@ -656,12 +739,21 @@ bit input.DOWN_BIT, a jp z, + ; Down is pressed - ld a, 1 * multiplier + utils.registers.load register, 1 * multiplier jp \.\@end +: ; Nothing pressed - xor a ; a = 0 + .if register == "a" + xor a ; a = 0 + .else + utils.registers.load register, 0 + .endif \.\@end: + + ; If return register wasn't A, mark the end of the clobber scope + .if register != "a" + utils.clobbers.end + .endif .endm diff --git a/interrupts.asm b/src/interrupts.asm similarity index 82% rename from interrupts.asm rename to src/interrupts.asm index 3243c39..86a193a 100644 --- a/interrupts.asm +++ b/src/interrupts.asm @@ -18,6 +18,10 @@ utils.ram.assertRamSlot .endif +.ifndef utils.clobbers + .include "utils/clobbers.asm" +.endif + ;==== ; Settings ; @@ -53,6 +57,7 @@ ; Constants ;==== .define interrupts.VDP_PORT $bf ; read = status; write = command +.define interrupts.VDP_VERTICAL_COUNTER_PORT $7e ;==== ; RAM @@ -177,9 +182,11 @@ ; Initialises the interrupt handler in RAM, if necessary ;==== .macro "interrupts.init" - ; Set vBlankFlag to 0 - xor a ; a = 0 - ld (interrupts.ram.vBlankFlag), a + utils.clobbers "af" + ; Set vBlankFlag to 0 + xor a ; a = 0 + ld (interrupts.ram.vBlankFlag), a + utils.clobbers.end .endm ;==== @@ -187,8 +194,10 @@ ; VDP interrupts that will unexpectedly trigger as soon as 'ei' takes effect ;==== .macro "interrupts.enable" - ; Ensure there are no pending interrupts that will trigger unexpectedly - in a, (interrupts.VDP_PORT) ; reset status + utils.clobbers "af" + ; Ensure there are no pending interrupts that will trigger unexpectedly + in a, (interrupts.VDP_PORT) ; reset status + utils.clobbers.end ; Enable interrupts on the Z80 ei @@ -198,16 +207,18 @@ ; Waits until a VBlank next occurs before continuing ;==== .macro "interrupts.waitForVBlank" - ; Poll VBlank flag in RAM - -: - halt ; wait for next interrupt (pause, VBlank, HBlank) - ld a, (interrupts.ram.vBlankFlag) ; load vBlankFlag - or a ; analyze a - jp z, - ; if wasn't VBlank, wait again - - ; VBlank occurred - reset flag then continue - xor a ; set a to 0 - ld (interrupts.ram.vBlankFlag), a + utils.clobbers "af" + ; Poll VBlank flag in RAM + -: + halt ; wait for next interrupt (pause, VBlank, HBlank) + ld a, (interrupts.ram.vBlankFlag) ; load vBlankFlag + or a ; analyze a + jp z, - ; if wasn't VBlank, wait again + + ; VBlank occurred - reset flag then continue + xor a ; set a to 0 + ld (interrupts.ram.vBlankFlag), a + utils.clobbers.end .endm ;==== @@ -217,13 +228,15 @@ ; @in a|value if using a, 0-based. If using value, 1-based ;==== .macro "interrupts.setLineInterval" args value - .ifdef value - ld a, value - 1 - .endif + utils.clobbers "af" + .ifdef value + ld a, value - 1 + .endif - out (interrupts.VDP_PORT), a - ld a, $8a ; register 10 - out (interrupts.VDP_PORT), a + out (interrupts.VDP_PORT), a + ld a, $8a ; register 10 + out (interrupts.VDP_PORT), a + utils.clobbers.end .endm ;==== @@ -232,5 +245,5 @@ ; @out a the line that is being/just been drawn (0-based) ;==== .macro "interrupts.getLine" - in a, ($7e) + in a, (interrupts.VDP_VERTICAL_COUNTER_PORT) .endm diff --git a/mapper/48k.asm b/src/mapper/48k.asm similarity index 100% rename from mapper/48k.asm rename to src/mapper/48k.asm diff --git a/mapper/_mapper.asm b/src/mapper/_mapper.asm similarity index 100% rename from mapper/_mapper.asm rename to src/mapper/_mapper.asm diff --git a/mapper/sega.asm b/src/mapper/sega.asm similarity index 100% rename from mapper/sega.asm rename to src/mapper/sega.asm diff --git a/mapper/waimanu.asm b/src/mapper/waimanu.asm similarity index 100% rename from mapper/waimanu.asm rename to src/mapper/waimanu.asm diff --git a/palette.asm b/src/palette.asm similarity index 90% rename from palette.asm rename to src/palette.asm index d1762d9..9c9b89e 100644 --- a/palette.asm +++ b/src/palette.asm @@ -15,6 +15,10 @@ .include "utils/assert.asm" .endif +.ifndef utils.clobbers + .include "utils/clobbers.asm" +.endif + .ifndef utils.outiBlock .include "utils/outiBlock.asm" .endif @@ -27,7 +31,8 @@ ; Constants ;==== .define palette.ELEMENT_SIZE_BYTES 1 -.define palette.SPRITE_PALETTE 16 +.define palette.BACKGROUND 0 +.define palette.SPRITE 16 .define palette.VRAM_ADDR $c000 ;==== @@ -60,6 +65,9 @@ ; @in red red value ; @in green green value ; @in blue blue value +; +; @in vram the color ram address to write to +; @in c the VDP data port ;==== .macro "palette.writeRgb" args red, green, blue utils.assert.equals NARGS, 3, "palette.asm \. received the wrong number of arguments" @@ -73,8 +81,10 @@ .define \.\@blue = (blue + 42.5) / 85 & $ff ; Convert to --bbggrr - ld a, (\.\@blue * 16) + (\.\@green * 4) + \.\@red - out (c), a ; write color + utils.clobbers "af" + ld a, (\.\@blue * 16) + (\.\@green * 4) + \.\@red + out (c), a ; write color + utils.clobbers.end .endm ;==== @@ -91,8 +101,10 @@ utils.assert.label address, "palette.asm \.: Invalid address argument" utils.assert.number size, "palette.asm \.: Invalid size argument" - ld hl, address - utils.outiBlock.write size + utils.clobbers "hl" + ld hl, address + utils.outiBlock.write size + utils.clobbers.end .endm ;==== diff --git a/patterns.asm b/src/patterns.asm similarity index 94% rename from patterns.asm rename to src/patterns.asm index 6daae51..4895a54 100644 --- a/patterns.asm +++ b/src/patterns.asm @@ -30,6 +30,10 @@ .include "utils/assert.asm" .endif +.ifndef utils.clobbers + .include "utils/clobbers.asm" +.endif + .ifndef utils.outiBlock .include "utils/outiBlock.asm" .endif @@ -75,8 +79,10 @@ utils.assert.label dataAddress, "patterns.asm \.: Invalid dataAddress argument" utils.assert.number size, "patterns.asm \.: Invalid size argument" - ld hl, dataAddress - utils.outiBlock.write size + utils.clobbers "hl" + ld hl, dataAddress + utils.outiBlock.write size + utils.clobbers.end .endm ;==== diff --git a/pause.asm b/src/pause.asm similarity index 78% rename from pause.asm rename to src/pause.asm index 39938ff..6c845cb 100644 --- a/pause.asm +++ b/src/pause.asm @@ -7,13 +7,17 @@ ;==== ; Dependencies ;==== +.ifndef utils.assert + .include "utils/assert.asm" +.endif + .ifndef utils.ram .include "utils/ram.asm" utils.ram.assertRamSlot .endif -.ifndef utils.assert - .include "utils/assert.asm" +.ifndef utils.clobbers + .include "utils/clobbers.asm" .endif ;==== @@ -60,22 +64,26 @@ ; Initialises the pause handler in RAM ;==== .macro "pause.init" - xor a - ld (pause.ram.pauseFlag), a + utils.clobbers "af" + xor a + ld (pause.ram.pauseFlag), a + utils.clobbers.end .endm ;==== ; If pause activated, waits until pause button is pressed again before ; continuing ;==== -.macro "pause.waitIfPaused" - -: - ld a, (pause.ram.pauseFlag) ; read pauseFlag - or a ; analyse flag - jp z, + ; jp if not paused - halt ; wait for an interrupt (pause, vBlank, hBlank) - jp - ; check again - +: +.macro "pause.waitIfPaused" isolated + utils.clobbers "af" + -: + ld a, (pause.ram.pauseFlag) ; read pauseFlag + or a ; analyse flag + jr z, + ; jp if not paused + halt ; wait for an interrupt (pause, vBlank, hBlank) + jr - ; check again + +: + utils.clobbers.end .endm ;==== diff --git a/scroll/metatiles.asm b/src/scroll/metatiles.asm similarity index 99% rename from scroll/metatiles.asm rename to src/scroll/metatiles.asm index 09afcfd..ac9975b 100644 --- a/scroll/metatiles.asm +++ b/src/scroll/metatiles.asm @@ -17,6 +17,10 @@ .fail .endif +.ifndef utils.clobbers + .include "utils/clobbers.asm" +.endif + .ifndef utils.math .include "utils/math.asm" .endif @@ -251,7 +255,9 @@ ; Alias to call scroll.metatiles.init ;==== .macro "scroll.metatiles.init" - call scroll.metatiles.init + utils.clobbers "af", "bc", "de", "hl", "ix", "iy", "i" + call scroll.metatiles.init + utils.clobbers.end .endm ;==== @@ -472,7 +478,9 @@ ; Alias to call scroll.metatiles.update ;==== .macro "scroll.metatiles.update" - call scroll.metatiles.update + utils.clobbers "af", "bc", "de", "hl", "ix", "iy" + call scroll.metatiles.update + utils.clobbers.end .endm ;==== diff --git a/scroll/tiles.asm b/src/scroll/tiles.asm similarity index 98% rename from scroll/tiles.asm rename to src/scroll/tiles.asm index ea7f475..4252a93 100644 --- a/scroll/tiles.asm +++ b/src/scroll/tiles.asm @@ -11,6 +11,10 @@ .fail .endif +.ifndef utils.clobbers + .include "utils/clobbers.asm" +.endif + .ifndef utils.math .include "utils/math.asm" .endif @@ -63,6 +67,8 @@ ; and you should also offset topLeftPointer accordingly ;==== .macro "scroll.tiles.init" args topLeftPointer mapCols mapRows colOffset rowOffset + utils.clobbers "af", "bc", "de", "hl" + .if NARGS > 0 .define \.\@topLeft topLeftPointer @@ -97,6 +103,8 @@ .endif call scroll.tiles.init + + utils.clobbers.end .endm ;==== @@ -153,7 +161,8 @@ ld e, a ; set E to bytesPerRow ; Write rows of tiles into VRAM - jp tilemap.writeRows ; jp to writeRows, which returns + tilemap.writeRows + ret .ends ;==== @@ -182,7 +191,9 @@ ; Alias to call scroll.tiles.update ;==== .macro "scroll.tiles.update" - call scroll.tiles.update + utils.clobbers "af", "bc", "de", "hl", "ix" + call scroll.tiles.update + utils.clobbers.end .endm ;==== diff --git a/tests/smslib-zest.asm b/src/smslib-zest.asm similarity index 100% rename from tests/smslib-zest.asm rename to src/smslib-zest.asm diff --git a/smslib.asm b/src/smslib.asm similarity index 78% rename from smslib.asm rename to src/smslib.asm index ae9aeec..245b864 100644 --- a/smslib.asm +++ b/src/smslib.asm @@ -7,8 +7,8 @@ ; ; Usage: ; -; .incdir "./lib/smslib" ; path to SMSLib directory -; .include "smslib.asm" ; import this file +; .incdir "./lib/smslib/src" ; path to SMSLib directory +; .include "smslib.asm" ; import this file ; ; See README.md for documentation regarding each module ;==== @@ -18,6 +18,12 @@ .include "mapper/48k.asm" .endif +; Intelligent register preservation +.include "utils/clobbers.asm" +.include "utils/preserve.asm" +.include "utils/restore.asm" + +; Modules .include "input.asm" ; handles input .include "interrupts.asm" ; handles line and frame interrupts .include "palette.asm" ; handles colors diff --git a/sprites.asm b/src/sprites.asm similarity index 74% rename from sprites.asm rename to src/sprites.asm index 18ea195..576e77a 100644 --- a/sprites.asm +++ b/src/sprites.asm @@ -20,12 +20,6 @@ .define sprites.VRAM_ADDRESS $3f00 .endif -; The high byte of the RAM address to place the sprite buffer. The low byte is -; always set to $3F to allow for optimisations -.ifndef sprites.HIGH_BUFFER_ADDRESS - .define sprites.HIGH_BUFFER_ADDRESS $C0 -.endif - ;==== ; Dependencies ;==== @@ -33,18 +27,21 @@ .include "utils/ram.asm" .endif -.ifndef utils.vdp - .include "utils/vdp.asm" +.ifndef utils.clobbers + .include "utils/clobbers.asm" .endif .ifndef utils.outiBlock .include "utils/outiBlock.asm" .endif +.ifndef utils.vdp + .include "utils/vdp.asm" +.endif + ;==== ; Constants and variables ;==== -.define sprites.BUFFER_ADDRESS (sprites.HIGH_BUFFER_ADDRESS * 256) + $3F .define sprites.TABLE_OFFSET $40 .define sprites.GROUP_TERMINATOR 255 ; terminates list of sprites.Sprite instances .define sprites.Y_TERMINATOR $D0 @@ -70,11 +67,12 @@ ;==== ;==== -; The offset of $40 is required to make use of performance optimisations. This -; technique is described by user gvx32 in: +; The sprite table buffer in RAM. The y coordinates are aligned to the table +; offset $40 to make use of performance optimisations. This technique is +; described by user gvx32 in: ; https://www.smspower.org/forums/15794-AFewHintsOnCodingAMediumLargeSizedGameUsingWLADX ;==== -.ramsection "sprites.ram.buffer" bank 0 slot utils.ram.SLOT orga sprites.BUFFER_ADDRESS force +.ramsection "sprites.ram.buffer" slot utils.ram.SLOT align 256 offset sprites.TABLE_OFFSET - sprites.Buffer.yPos sprites.ram.buffer: instanceof sprites.Buffer .ends @@ -115,14 +113,16 @@ .endm ;==== -; Loads the next available sprite index (y position) into DE +; (Private) Loads the next available sprite index (y position) into DE ; ; @out de the address of the next available sprite index ;==== -.macro "sprites.getNextIndex" - ld de, sprites.ram.buffer.nextIndex - ld a, (de) - ld e, a +.macro "sprites._getNextIndex" + utils.clobbers "af" + ld de, sprites.ram.buffer.nextIndex + ld a, (de) + ld e, a + utils.clobbers.end .endm ;==== @@ -132,26 +132,30 @@ ;==== .macro "sprites._storeNextIndex" ; Store next index - ld a, e - ld de, sprites.ram.buffer.nextIndex - ld (de), a + utils.clobbers "af" + ld a, e + ld (sprites.ram.buffer.nextIndex), a + utils.clobbers.end .endm ;==== -; Adds a sprite to the buffer +; (Private) Adds a sprite to the buffer. See sprites.add macro for public +; macro ;==== -.section "sprites.add" free +.section "sprites._add" free ;==== ; Add sprite to the next available index ; ; @in a screen yPos ; @in b screen xPos ; @in c pattern number + ; + ; @out de pointer to next available y slot in sprite buffer ;==== - sprites.addToNextIndex: + sprites._addToNextIndex: ; Retrieve next slot ld iyl, a ; preserve yPos - sprites.getNextIndex + sprites._getNextIndex ld a, iyl ; restore yPos ; continue to sprites.add @@ -162,8 +166,10 @@ ; @in a screen yPos ; @in b screen xPos ; @in c pattern number + ; + ; @out de pointer to next available y slot in sprite buffer ;==== - sprites.add: + sprites._add: ; Set ypos ld (de), a @@ -186,19 +192,26 @@ ;==== ; Add a sprite to the buffer ; -; @in a screen yPos -; @in b screen xPos -; @in c pattern number +; @in a screen yPos +; @in b screen xPos +; @in c pattern number ; -; @in de (optional) pointer to next available index (yPos) in sprite buffer -; Only required if a batch is in progress +; @in de (optional) pointer to next available index (yPos) in sprite buffer +; Only required if a batch is in progress +; +; @out de (if batch in progress) pointer to next available index (yPos) in +; sprite buffer ;==== .macro "sprites.add" .if sprites.batchInProgress == 1 - call sprites.add + utils.clobbers "af" + call sprites._add + utils.clobbers.end .else - call sprites.addToNextIndex - sprites._storeNextIndex + utils.clobbers "af", "de", "iy" + call sprites._addToNextIndex + sprites._storeNextIndex + utils.clobbers.end .endif .endm @@ -210,12 +223,13 @@ ;==== .macro "sprites.startBatch" .ifeq sprites.batchInProgress 1 - .print "Warning: sprites.startBatch called but batch already in progress." - .print " Ensure you also call sprites.endBatch\n\n" + .print "\. called but batch already in progress." + .print " Ensure you also call sprites.endBatch\n" + .fail .endif .redefine sprites.batchInProgress 1 - sprites.getNextIndex + sprites._getNextIndex .endm ;==== @@ -223,7 +237,8 @@ ;==== .macro "sprites.endBatch" .ifeq sprites.batchInProgress 0 - .print "Warning: sprites.endBatch called but no batch is in progress" + .print "\. called but no batch is in progress\n" + .fail .else .redefine sprites.batchInProgress 0 sprites._storeNextIndex @@ -234,9 +249,11 @@ ; Initialises a sprite buffer in RAM ;==== .macro "sprites.init" - ; Set nextIndex to index 0 - ld a, <(sprites.ram.buffer) + sprites.Buffer.yPos ; low byte of index 0 - ld (sprites.ram.buffer.nextIndex), a ; store in nextIndex + utils.clobbers "af" + ; Set nextIndex to index 0 + ld a, <(sprites.ram.buffer) + sprites.Buffer.yPos ; low byte of index 0 + ld (sprites.ram.buffer.nextIndex), a ; store in nextIndex + utils.clobbers.end .endm ;==== @@ -247,11 +264,11 @@ .endm ;==== -; Copies the sprite buffer from RAM to VRAM. This should be performed while the +; (Private) Copies the sprite buffer from RAM to VRAM. This should be performed while the ; display is off or during VBlank ;==== -.section "sprites.copyToVram" - sprites.copyToVram: +.section "sprites._copyToVram" + sprites._copyToVram: ; Set VDP write address to y positions utils.vdp.prepWrite sprites.VRAM_ADDRESS @@ -265,7 +282,7 @@ ; Copy y positions to VRAM ld b, ixl ; load size into B inc l ; point to y positions in buffer - call utils.outiBlock.writeUpTo128Bytes ; write data + utils.outiBlock.writeUpTo128Bytes ; write data ; Output sprite terminator at end of y positions ld a, ixl ; load counter into A @@ -282,7 +299,7 @@ ; Copy x positions and patterns from buffer to VRAM ld b, ixl ; restore sprite count rlc b ; double to get xPos + pattern - jp utils.outiBlock.writeUpTo128Bytes ; write bytes, then ret + utils.outiBlock.writeUpTo128BytesThenReturn ; write bytes, then ret ; No sprites in buffer - cap table with sprite terminator then return ; VRAM address must be set @@ -293,22 +310,19 @@ .ends ;==== -; Alias for sprites.copyToVram +; Copies the sprite buffer from RAM to VRAM. This should be performed while the +; display is off or during VBlank ;==== .macro "sprites.copyToVram" - call sprites.copyToVram + utils.clobbers "af", "bc", "hl", "ix", "iy" + call sprites._copyToVram + utils.clobbers.end .endm ;==== -; Adds a group of sprites relative to an anchor position. Sprites within the -; group that fall off-screen are not added. -; -; Limitation: Once the anchorX position falls off screen the entire group will -; disappear, meaning the group cannot move smoothly off the left edge of the -; screen. This can be partially obscured by setting the VDP registers to hide -; the left column +; (Private) See sprites.addGroup ;==== -.section "sprites.addGroup" +.section "sprites._addGroup" free ;==== ; Gets the next free sprite index and adds a sprite group from there onwards ; @@ -319,9 +333,9 @@ ; ; @out de next free sprite index ;==== - sprites.addGroupFromNextIndex: - sprites.getNextIndex - jp sprites.addGroup + sprites._addGroupFromNextIndex: + sprites._getNextIndex + jp sprites._addGroup ;==== ; Add a group of sprites from the currently selected slot @@ -341,14 +355,14 @@ _nextSprite: inc hl ; point to next item - sprites.addGroup: + sprites._addGroup: ld a, (hl) ; load relX cp sprites.GROUP_TERMINATOR ret z ; end of group ; Add relative x add a, b ; relX + anchorX - jp c, _xOffScreen ; off screen; next sprite + jr c, _xOffScreen ; off screen; next sprite ld ixh, a ; store result x for later ; Add relative y @@ -356,7 +370,7 @@ ld a, (hl) ; load relY add a, c ; relY + anchorY cp sprites.MAX_Y_POSITION ; compare with max y allowed - jp nc, _yOffScreen ; off screen; next sprite + jr nc, _yOffScreen ; off screen; next sprite ; Output sprite to sprite buffer ld (de), a ; set sprite y in buffer @@ -373,13 +387,26 @@ .ends ;==== -; Macro alias for sprites.addGroup +; Adds a group of sprites relative to an anchor position. Sprites within the +; group that fall off-screen are not added. +; +; Limitation: Once the anchorX position falls off screen the entire group will +; disappear, meaning the group cannot move smoothly off the left edge of the +; screen. This can be partially obscured by setting the VDP registers to hide +; the left column +; +; @in hl pointer to sprites.Sprite instances terminated by +; sprites.GROUP_TERMINATOR ;==== .macro "sprites.addGroup" .if sprites.batchInProgress == 1 - call sprites.addGroup + utils.clobbers "af", "hl", "ix" + call sprites._addGroup + utils.clobbers.end .else - call sprites.addGroupFromNextIndex - sprites._storeNextIndex + utils.clobbers "af", "de", "hl", "ix" + call sprites._addGroupFromNextIndex + sprites._storeNextIndex + utils.clobbers.end .endif .endm diff --git a/tilemap.asm b/src/tilemap.asm similarity index 85% rename from tilemap.asm rename to src/tilemap.asm index 9f103b7..860b301 100644 --- a/tilemap.asm +++ b/src/tilemap.asm @@ -25,6 +25,10 @@ .include "utils/assert.asm" .endif +.ifndef utils.clobbers + .include "utils/clobbers.asm" +.endif + .ifndef utils.math .include "utils/math.asm" .endif @@ -138,6 +142,8 @@ ; @in index 0 is top left tile ;==== .macro "tilemap.setIndex" args index + utils.assert.range index 0, 895, "\.: Index should be between 0 and 895" + utils.vdp.prepWrite (tilemap.VRAM_ADDRESS + (index * tilemap.TILE_SIZE_BYTES)) .endm @@ -148,6 +154,9 @@ ; @in row row number (y) ;==== .macro "tilemap.setColRow" args colX rowY + utils.assert.range colX, 0, 31, "\.: colX should be between 0 and 31" + utils.assert.range rowY, 0, 27, "\.: rowY should be between 0 and 27" + tilemap.setIndex ((rowY * tilemap.COLS) + colX) .endm @@ -160,14 +169,16 @@ ; @out c the port to output data to (using out, outi etc) ;==== .macro "tilemap.loadHLWriteAddress" - ; Multiply by 2 (2 bytes per tile) - add hl, hl - - ; Low byte of base address is 0, so we just need to manipulate high byte - ; Set A to high byte of base address with the write command set - ld a, >tilemap.VRAM_ADDRESS | utils.vdp.commands.WRITE - or h ; combine bits with high byte of relative address - ld h, a ; set HL to the full address + utils.clobbers "af" + ; Multiply by 2 (2 bytes per tile) + add hl, hl + + ; Low byte of base address is 0, so we just need to manipulate high byte + ; Set A to high byte of base address with the write command set + ld a, >tilemap.VRAM_ADDRESS | utils.vdp.commands.WRITE + or h ; combine bits with high byte of relative address + ld h, a ; set HL to the full address + utils.clobbers.end .endm ;==== @@ -179,7 +190,7 @@ ; is set automatically ;==== .macro "tilemap.tile" args patternIndex attributes - utils.assert.range NARGS, 1, 2, "tilemap.asm \.: Invalid number of arguments" + utils.assert.range NARGS, 1, 2, "\.: Invalid number of arguments" utils.assert.range patternIndex, 0, tilemap.MAX_PATTERN_INDEX, "tilemap.asm \.: Invalid patternIndex argument" .ifndef attributes @@ -202,14 +213,16 @@ ; @in attributes (optional) the tile attributes (see Tile attributes section). ; Note, if patternRef is greater than 255, tilemap.HIGH_BIT ; is set automatically +; +; @in c VDP data port ;==== .macro "tilemap.writeTile" args patternIndex attributes - utils.assert.range patternIndex, 0, tilemap.MAX_PATTERN_INDEX, "tilemap.asm \.: Invalid patternIndex argument" + utils.assert.range patternIndex, 0, tilemap.MAX_PATTERN_INDEX, "\.: Invalid patternIndex argument" .ifndef attributes .define attributes $00 .else - utils.assert.range attributes, 0, 255, "tilemap.asm \.: Invalid attributes argument" + utils.assert.range attributes, 0, 255, "\.: Invalid attributes argument" .endif ; Set high bit attribute if pattern index is above 255 @@ -217,11 +230,27 @@ .redefine attributes attributes | tilemap.HIGH_BIT .endif - ld a, <(patternIndex) ; load A with low-byte of pattern index - out (utils.vdp.DATA_PORT), a ; write pattern ref + utils.clobbers "af" + ; Load A with low-byte of pattern index + .if <(patternIndex) == 0 + xor a ; set to 0 + .else + ld a, <(patternIndex) + .endif - ld a, attributes - out (utils.vdp.DATA_PORT), a ; write tile attributes + out (utils.vdp.DATA_PORT), a ; write pattern index + + ; Load A with attribute byte + .if attributes != <(patternIndex) + .if attributes == 0 + xor a ; set A to 0 + .else + ld a, attributes + .endif + .endif + + out (utils.vdp.DATA_PORT), a ; write tile attributes + utils.clobbers.end .endm ;==== @@ -232,32 +261,11 @@ ; @in VRAM pointer to destination address with write command ;==== .macro "tilemap.writeTiles" args number - utils.assert.range number, 1, tilemap.TILES, "tilemap.asm \.: Invalid number argument" + utils.assert.range number, 1, tilemap.TILES, "\.: Invalid number argument" utils.outiBlock.write tilemap.TILE_SIZE_BYTES * number .endm -;==== -; Reads pattern ref bytes and writes to the tilemap until a terminator byte is -; reached. -; -; @in hl address of the data to write -; @in b tile attributes to use for all the tiles -; @in c the data port to write to -; @in d the terminator byte value -;==== -.section "tilemap.writeBytesUntil" free - tilemap.writeBytesUntil: - ld a, (hl) ; read byte - cp d ; compare value with terminator - ret z ; return if terminator byte found - out (utils.vdp.DATA_PORT), a ; write pattern ref - ld a, b ; load attributes - out (utils.vdp.DATA_PORT), a ; write attributes - inc hl ; next char - jp tilemap.writeBytesUntil ; repeat -.ends - ;==== ; Copies pattern ref bytes to VRAM until a terminator byte is reached ; @@ -267,16 +275,22 @@ ; attribute options at top). Defaults to 0 ;==== .macro "tilemap.writeBytesUntil" args terminator dataAddr attributes - ld d, terminator - ld hl, dataAddr + utils.assert.range terminator 0 255 "\.: terminator should be a byte value" + utils.assert.label dataAddr "\.: dataAddr should be a label" - .ifdef attributes - ld b, attributes - .else - ld b, 0 - .endif + utils.clobbers "af", "bc", "de", "hl" + ld d, terminator + ld hl, dataAddr - call tilemap.writeBytesUntil + .ifdef attributes + utils.assert.range attributes 0 255 "\.: attributes should be a byte value" + ld b, attributes + .else + ld b, 0 + .endif + + call tilemap._writeBytesUntil + utils.clobbers.end .endm ;==== @@ -310,16 +324,21 @@ ; See tile attribute options at top. Defaults to $00 ;==== .macro "tilemap.writeBytes" args address count attributes - ld hl, address - ld b, count + utils.assert.label address "\.: Address should be a label" + utils.assert.range count 0 tilemap.TILES "\.: Count should be between 0 and {tilemap.TILES}" - .ifdef attributes - ld c, attributes - .else - ld c, 0 - .endif + utils.clobbers "af", "hl", "bc" + ld hl, address + ld b, count + + .ifdef attributes + ld c, attributes + .else + ld c, 0 + .endif - call tilemap.writeBytes + call tilemap.writeBytes + utils.clobbers.end .endm ;==== @@ -333,13 +352,6 @@ utils.outiBlock.write tilemap.ROW_SIZE_BYTES .endm -;==== -; Alias for tilemap.writeRows -;==== -.macro "tilemap.writeRows" - call tilemap.writeRows -.endm - ;==== ; Write tile data from an uncompressed map. Each tile is 2-bytes - the first is ; the tileRef and the second is the tile's attributes. @@ -349,12 +361,21 @@ ; columns in the full map * 2 (as each tile is 2-bytes) ; @in hl pointer to the first tile to write ;==== -.section "tilemap.writeRows" +.macro "tilemap.writeRows" + utils.clobbers "af", "bc", "de" + call tilemap._writeRows + utils.clobbers.end +.endm + +;==== +; Private (see macro) +;==== +.section "tilemap._writeRows" _nextRow: ld a, e ; write row width into A utils.math.addHLA ; add 1 row to full tilemap pointer - tilemap.writeRows: + tilemap._writeRows: push hl ; preserve HL tilemap.writeRow ; write a row of data pop hl ; restore HL @@ -365,40 +386,15 @@ .ends ;==== -; Alias to call tilemap.reset +; Initialises the RAM buffers and scroll values to their starting state ;==== .macro "tilemap.reset" - call tilemap.reset -.endm - -;==== -; Initialise the RAM buffers and scroll values to their starting state -;==== -.section "tilemap.reset" free - tilemap.reset: - xor a ; set A to 0 - ld (tilemap.ram.flags), a - ld (tilemap.ram.yScrollBuffer), a - - ld (tilemap.ram.vramRowWrite), a - ld (tilemap.ram.vramRowWrite + 1), a - - ld (tilemap.ram.colWriteCall), a - ld (tilemap.ram.colWriteCall + 1), a - - ; Set the VDP SCROLL_Y_REGISTER to 0 - utils.vdp.setRegister utils.vdp.SCROLL_Y_REGISTER - - ; Set the xScrollBuffer to the starting X_OFFSET value - ld a, tilemap.X_OFFSET - ld (tilemap.ram.xScrollBuffer), a - - ; Write xScrollBuffer to the VDP SCROLL_X_REGISTER (needs to be negated) - ld a, -tilemap.X_OFFSET - utils.vdp.setRegister utils.vdp.SCROLL_X_REGISTER + \@_\.: - ret -.ends + utils.clobbers "af" + call tilemap._reset + utils.clobbers.end +.endm ;==== ; Adjusts the buffered tilemap xScroll value by a given number of pixels. If @@ -413,7 +409,10 @@ ; left (shifting the tiles right) ;==== .macro "tilemap.adjustXPixels" - call tilemap._adjustXPixels + \@_\.: + utils.clobbers "af" "bc" "hl" + call tilemap._adjustXPixels + utils.clobbers.end .endm ;==== @@ -429,7 +428,10 @@ ; up (shifting the tiles down) ;==== .macro "tilemap.adjustYPixels" - call tilemap._adjustYPixels + \@_\.: + utils.clobbers "af" "bc" "hl" + call tilemap._adjustYPixels + utils.clobbers.end .endm ;==== @@ -441,8 +443,15 @@ ; ; Note: This should be called before calling tilemap.calculateScroll ;==== -.section "tilemap.stopUpRowScroll" free - tilemap.stopUpRowScroll: +.macro "tilemap.stopUpRowScroll" + \@_\.: + utils.clobbers "af" + call tilemap._stopUpRowScroll + utils.clobbers.end +.endm + +.section "tilemap._stopUpRowScroll" free + tilemap._stopUpRowScroll: ; Reset UP scroll flag ld a, (tilemap.ram.flags) ; load flags and tilemap.SCROLL_Y_RESET_MASK ; reset Y scroll flags @@ -465,13 +474,6 @@ ret .ends -;==== -; Alias to call tilemap.stopUpRowScroll -;==== -.macro "tilemap.stopUpRowScroll" - call tilemap.stopUpRowScroll -.endm - ;==== ; When tilemap.ifRowScroll indicates a down scroll, but you detect this new row ; will be out of bounds of the tilemap, call this to cap the y pixel scrolling @@ -481,8 +483,15 @@ ; ; Note: This should be called before calling tilemap.calculateScroll ;==== -.section "tilemap.stopDownRowScroll" free - tilemap.stopDownRowScroll: +.macro "tilemap.stopDownRowScroll" + \@_\.: + utils.clobbers "af" + call tilemap._stopDownRowScroll + utils.clobbers.end +.endm + +.section "tilemap._stopDownRowScroll" free + tilemap._stopDownRowScroll: ; Reset Y scroll flags ld a, (tilemap.ram.flags) ; load flags and tilemap.SCROLL_Y_RESET_MASK ; reset Y scroll flags @@ -505,13 +514,6 @@ ret .ends -;==== -; Alias to call tilemap.stopDownRowScroll -;==== -.macro "tilemap.stopDownRowScroll" - call tilemap.stopDownRowScroll -.endm - ;==== ; When tilemap.ifColScroll indicates a left scroll, but you detect this new row ; will be out of bounds of the tilemap, call this to cap the x pixel scrolling @@ -521,8 +523,15 @@ ; ; Note: This should be called before calling tilemap.calculateScroll ;==== -.section "tilemap.stopLeftColScroll" free - tilemap.stopLeftColScroll: +.macro "tilemap.stopLeftColScroll" + \@_\.: + utils.clobbers "af" + call tilemap._stopLeftColScroll + utils.clobbers.end +.endm + +.section "tilemap._stopLeftColScroll" free + tilemap._stopLeftColScroll: ; Reset column scroll flags ld a, (tilemap.ram.flags) ; load flags and tilemap.SCROLL_X_RESET_MASK ; reset x scroll flags @@ -537,13 +546,6 @@ ret .ends -;==== -; Alias to call tilemap.stopLeftColScroll -;==== -.macro "tilemap.stopLeftColScroll" - call tilemap.stopLeftColScroll -.endm - ;==== ; When tilemap.ifColScroll indicates a right scroll, but you detect this new row ; will be out of bounds of the tilemap, call this to cap the x pixel scrolling @@ -553,8 +555,15 @@ ; ; Note: This should be called before calling tilemap.calculateScroll ;==== -.section "tilemap.stopRightColScroll" free - tilemap.stopRightColScroll: +.macro "tilemap.stopRightColScroll" + \@_\.: + utils.clobbers "af" + call tilemap._stopRightColScroll + utils.clobbers.end +.endm + +.section "tilemap._stopRightColScroll" free + tilemap._stopRightColScroll: ; Reset column scroll flags ld a, (tilemap.ram.flags) ; load flags and tilemap.SCROLL_X_RESET_MASK ; reset x scroll flags @@ -569,19 +578,15 @@ ret .ends -;==== -; Alias to call tilemap.stopRightColScroll -;==== -.macro "tilemap.stopRightColScroll" - call tilemap.stopRightColScroll -.endm - ;==== ; Calculates the adjustments made with tilemap.adjustXPixels/adjustYPixels ; and applies them to the RAM variables ;==== .macro "tilemap.calculateScroll" - call tilemap._calculateScroll + \@_\.: + utils.clobbers "af" "bc" + call tilemap._calculateScroll + utils.clobbers.end .endm ;==== @@ -654,22 +659,28 @@ ; @in else jump to this label if no columns need loading ;==== .macro "tilemap.ifColScroll" args left, right, else + \@_\.: + .if NARGS == 1 ; Only one argument passed ('else' label) - ld a, (tilemap.ram.flags) ; load flags - rlca ; set C to 7th bit - jp nc, \1 ; jp to else if no col to scroll + utils.clobbers.withBranching "af" + ld a, (tilemap.ram.flags) ; load flags + rlca ; set carry to 7th bit + utils.clobbers.end.jpnc \1 ; jp to else if no col to scroll + utils.clobbers.end ; ...otherwise continue .elif NARGS == 3 - ; 3 arguments passed (left, right, else) - ld a, (tilemap.ram.flags) ; load flags - rlca ; set C to 7th bit - jp nc, else ; jp to else if no col to scroll (bit 7 was 0) - - ; Check right scroll flag - rlca ; set C to what was 6th bit - jp nc, right ; jp if scrolling right (bit 6 was 0) - ; ...otherwise continue to left label + utils.clobbers.withBranching "af" + ; 3 arguments passed (left, right, else) + ld a, (tilemap.ram.flags) ; load flags + rlca ; set C to 7th bit + utils.clobbers.end.jpnc else ; bit 7 was 0 - no col scroll + + ; Check right scroll flag + rlca ; set C to what was 6th bit + utils.clobbers.end.jpnc right ; bit 6 was 0 - scrolling right + ; ...otherwise continue to left label + utils.clobbers.end .else .print "\ntilemap.ifColScroll requires 1 or 3 arguments (left/right/else, or just else alone)\n\n" .fail @@ -684,19 +695,19 @@ ; @in right if scrolling right, will jump to this label ;==== .macro "tilemap.ifColScrollElseRet" args left, right - .if NARGS != 2 - .print "\ntilemap.ifColScrollElseRet requires 2 arguments (left and right)\n\n" - .fail - .endif + utils.assert.equals NARGS 2 "\. requires 2 arguments (left and right)" - ld a, (tilemap.ram.flags) ; load flags - rlca ; set C to 7th bit - ret nc ; return if no column to scroll (bit 7 was 0) + utils.clobbers.withBranching "af" + ld a, (tilemap.ram.flags) ; load flags + rlca ; set carry to 7th bit + utils.clobbers.end.retnc ; ret if no col to scroll (bit 7 was 0) - ; Check right scroll flag - rlca ; set C to what was 6th bit - jp nc, right ; jp if scrolling right (bit 6 was 0) - ; ...otherwise continue to 'left' label + ; Check right scroll flag + rlca ; set carry to what was 6th bit + utils.clobbers.end.jpnc right ; jp if scrolling right (bit 6 was 0) + jp nc, right + ; ...otherwise continue to 'left' label + utils.clobbers.end .endm ;==== @@ -710,20 +721,24 @@ ;==== .macro "tilemap.ifRowScroll" args up, down, else .if NARGS == 1 - ; Only one argument passed ('else' label) - ld a, (tilemap.ram.flags) ; load flags - rrca ; set C to bit 0 - jp nc, \1 ; jp to else if no row scroll (bit 0 was 0) - ; ...otherwise continue + utils.clobbers.withBranching "af" + ; Only one argument passed ('else' label) + ld a, (tilemap.ram.flags) ; load flags + rrca ; set C to bit 0 + utils.clobbers.end.jpnc \1 ; jp to else if no row scroll (bit 0 was 0) + ; ...otherwise continue + utils.clobbers.end .elif NARGS == 3 - ld a, (tilemap.ram.flags) ; load flags - rrca ; set C to bit 0 - jp nc, else ; no row to scroll (bit 0 was 0) - - ; Check down scroll flag - rrca ; set C to what was bit 1 - jp c, down ; jp if scrolling down (bit 1 was set) - ; ...otherwise continue to 'up' label + utils.clobbers.withBranching "af" + ld a, (tilemap.ram.flags) ; load flags + rrca ; set C to bit 0 + utils.clobbers.end.jpnc else; no row to scroll (bit 0 was 0) + + ; Check down scroll flag + rrca ; set C to what was bit 1 + utils.clobbers.end.jpc down ; jp if scrolling down (bit 1 was set) + ; ...otherwise continue to 'up' label + utils.clobbers.end .else .print "\ntilemap.ifRowScroll requires 1 or 3 arguments (up/down/else, or just else alone)\n\n" .fail @@ -738,19 +753,18 @@ ; @in down if scrolling down, will jump to this label ;==== .macro "tilemap.ifRowScrollElseRet" args up, down - .if NARGS != 2 - .print "\ntilemap.ifRowScrollElseRet requires 2 arguments (up and down)\n\n" - .fail - .endif + utils.assert.equals NARGS 2 "\. requires 2 arguments (up and down)" - ld a, (tilemap.ram.flags) ; load flags - rrca ; set C to bit 0 - ret nc ; return if no row to scroll + utils.clobbers.withBranching "af" + ld a, (tilemap.ram.flags) ; load flags + rrca ; set C to bit 0 + utils.clobbers.end.retnc ; return if no row to scroll - ; Check down scroll flag - rrca ; set C to what was bit 1 - jp c, down ; jp if down scroll (bit 1 was set) - ; ...otherwise continue to 'up' label + ; Check down scroll flag + rrca ; set C to what was bit 1 + utils.clobbers.end.jpc down ; jp if down scroll (bit 1 was set) + ; ...otherwise continue to 'up' label + utils.clobbers.end .endm ;==== @@ -758,12 +772,14 @@ ; when the display is off or during a V or H interrupt ;==== .macro "tilemap.writeScrollRegisters" - ld a, (tilemap.ram.xScrollBuffer) - neg - utils.vdp.setRegister utils.vdp.SCROLL_X_REGISTER + utils.clobbers "af" + ld a, (tilemap.ram.xScrollBuffer) + neg + utils.vdp.setRegister utils.vdp.SCROLL_X_REGISTER - ld a, (tilemap.ram.yScrollBuffer) - utils.vdp.setRegister utils.vdp.SCROLL_Y_REGISTER + ld a, (tilemap.ram.yScrollBuffer) + utils.vdp.setRegister utils.vdp.SCROLL_Y_REGISTER + utils.clobbers.end .endm ;==== @@ -845,7 +861,9 @@ ; This should be called when the display is off or during VBlank ;==== .macro "tilemap.writeScrollBuffers" - call tilemap.writeScrollBuffers + utils.clobbers "af" "bc" "de" "hl" "iy" + call tilemap.writeScrollBuffers + utils.clobbers.end .endm ;==== @@ -891,7 +909,7 @@ ld l, a ; Copy bytes from buffer to VDP - call utils.outiBlock.writeUpTo128Bytes + utils.outiBlock.writeUpTo128Bytes ;=== ; Second write @@ -909,7 +927,7 @@ ; Write bytes then return to caller ld b, a ; set B to bytes to write - jp utils.outiBlock.writeUpTo128Bytes + utils.outiBlock.writeUpTo128BytesThenReturn +: ret @@ -1179,6 +1197,35 @@ ret .ends +;==== +; (Private) Initialises the RAM buffers and scroll values to their starting state +;==== +.section "tilemap._reset" free + tilemap._reset: + xor a ; set A to 0 + ld (tilemap.ram.flags), a + ld (tilemap.ram.yScrollBuffer), a + + ld (tilemap.ram.vramRowWrite), a + ld (tilemap.ram.vramRowWrite + 1), a + + ld (tilemap.ram.colWriteCall), a + ld (tilemap.ram.colWriteCall + 1), a + + ; Set the VDP SCROLL_Y_REGISTER to 0 + utils.vdp.setRegister utils.vdp.SCROLL_Y_REGISTER + + ; Set the xScrollBuffer to the starting X_OFFSET value + ld a, tilemap.X_OFFSET + ld (tilemap.ram.xScrollBuffer), a + + ; Write xScrollBuffer to the VDP SCROLL_X_REGISTER (needs to be negated) + ld a, -tilemap.X_OFFSET + utils.vdp.setRegister utils.vdp.SCROLL_X_REGISTER + + ret +.ends + ;==== ; Unrolled loop of column tile writes. Call one of the addresses stored in the ; tilemap._writeColumnLookup lookup table to start from a given row. The loop @@ -1256,6 +1303,27 @@ .endr .ends +;==== +; Reads pattern ref bytes and writes to the tilemap until a terminator byte is +; reached. +; +; @in hl address of the data to write +; @in b tile attributes to use for all the tiles +; @in c the data port to write to +; @in d the terminator byte value +;==== +.section "tilemap._writeBytesUntil" free + tilemap._writeBytesUntil: + ld a, (hl) ; read byte + cp d ; compare value with terminator + ret z ; return if terminator byte found + out (utils.vdp.DATA_PORT), a ; write pattern ref + ld a, b ; load attributes + out (utils.vdp.DATA_PORT), a ; write attributes + inc hl ; next char + jp tilemap._writeBytesUntil ; repeat +.ends + ;==== ; Calls the address stored in IY ; diff --git a/utils/assert.asm b/src/utils/assert.asm similarity index 100% rename from utils/assert.asm rename to src/utils/assert.asm diff --git a/src/utils/clobbers.asm b/src/utils/clobbers.asm new file mode 100644 index 0000000..45851d7 --- /dev/null +++ b/src/utils/clobbers.asm @@ -0,0 +1,647 @@ +.define utils.clobbers + +;==== +; Dependencies +;==== +.ifndef utils.registers + .include "utils/registers.asm" +.endif + +;==== +; Variables +;==== +.define utils.clobbers.index -1 ; the current clobber scope + +;==== +; (Private) Starts a new clobber scope, specifying which registers are about to +; be clobbed. +; +; @in clobbing the registers being clobbed (utils.registers.XX constants +; ORed together) +; @in type (Optional) if "isolated", the scope will be wrapped in its +; own preserve scope. This is needed for branching clobber +; scopes +;==== +.macro "utils.clobbers._startScope" args clobbing type + utils.assert.range clobbing utils.registers.AF, utils.registers.ALL, "\.: clobbing should be the register.* constants ORed together" + + ; Increment the clobber index + .redefine utils.clobbers.index utils.clobbers.index + 1 + + ; Inform utils.registers (which may start an auto-preserve scope) + .if utils.clobbers.index == 0 ; first scope + utils.registers.onInitialClobberScope + .endif + + ; If this is an isolated scope, start a preserve scope to wrap it + .ifdef type + .if type == "isolated" + ; Get registers protected by existing preserve scopes + utils.registers.getProtected + + ; Start preserve scope to isolate this clobber scope + utils.registers.preserve (clobbing & utils.registers.getProtected.returnValue) + .define utils.clobbers{utils.clobbers.index}.isIsolated + .endif + .endif + + ; Preserve registers that need preserving and are being clobbered + utils.registers.getVulnerable + utils.registers._preserveRegisters (clobbing & utils.registers.getVulnerable.returnValue) +.endm + +;==== +; Starts a clobber scope, in which the specific registers will be clobbered. +; If the current preserve scope needs these values to be preserved they will +; be pushed to the stack +; +; @in ...registers list of registers that will be clobbered +; Valid values: +; "AF", "BC", "DE", "HL", "IX", "IY", "I", +; "af", "bc", "de", "hl", "ix", "iy", "i" +; "AF'", "BC'", "DE'", "HL'" +; "af'", "bc'", "de'", "hl'", +; or one or more utils.registers.xx constants ORed +; together +;==== +.macro "utils.clobbers" + .if nargs == 0 + .print "\.: Expected at least 1 register to be passed\n" + .fail + .endif + + ; Combine (OR) all given registers into a single value + .redefine \.._clobbing 0 + .repeat nargs + ; Parse the register string into a constant + utils.registers.parse \1 + .redefine \.._clobbing (\.._clobbing | utils.registers.parse.returnValue) + .shift ; shift args (\2 => \1) + .endr + + utils.clobbers._startScope (\.._clobbing) +.endm + +;==== +; Starts a clobber scope that behaves the same way as standard scopes but +; supports jumps outside the clobber scope using the utils.clobbers.end.x +; macros. +; +; As these jumps don't know whether the jump location is inside or outside the +; preserve scope, withBranching wraps the clobber scope with its own preserve +; scope to isolate it. The jumps can therefore determine to always restore the +; registers they clobber. +; +; @in ...registers list of registers that will be clobbered +; Valid values: +; "AF", "BC", "DE", "HL", "IX", "IY", "I", +; "af", "bc", "de", "hl", "ix", "iy", "i" +; "AF'", "BC'", "DE'", "HL'" +; "af'", "bc'", "de'", "hl'", +; or one or more utils.registers.xx constants ORed +; together +;==== +.macro "utils.clobbers.withBranching" + .if nargs == 0 + .print "\.: Expected at least 1 register to be passed\n" + .fail + .endif + + ; Combine (OR) all given registers into a single value + .redefine \.._clobbing 0 + .repeat nargs + ; Parse the register string into a constant + utils.registers.parse \1 + .redefine \.._clobbing (\.._clobbing | utils.registers.parse.returnValue) + .shift ; shift args (\2 => \1) + .endr + + ;=== + ; Start a clobber scope wrapped within its own preserve scope so it doesn't + ; affect outer scopes, allowing conditional jumps to pop registers without + ; causing mismatches in some edge cases + ;=== + utils.clobbers._startScope (\.._clobbing) "isolated" +.endm + +;==== +; Marks the end of a clobber scope +;==== +.macro "utils.clobbers.end" + ; Assert there are clobber scopes in progress + .if utils.clobbers.index == -1 + .print "\. was called but no clobber scopes are in progress\n" + .fail + .endif + + ; If this was an isolated scope with its own preserve scope + .ifdef utils.clobbers{utils.clobbers.index}.isIsolated + .undefine utils.clobbers{utils.clobbers.index}.isIsolated + utils.registers.restore + .endif + + ; If this is the last clobber scope in progress + .if utils.clobbers.index == 0 + utils.registers.onFinalClobberScopeEnd + .endif + + .redefine utils.clobbers.index utils.clobbers.index - 1 +.endm + +;==== +; If a utils.clobbers.withBranching is in progress, restores the registers from +; its isolated preserve scope. This should be called before making jumps outside +; of the clobber scope +; +; @fails if no utils.clobbers.withBranching scope is in progress +;==== +.macro "utils.clobbers.endBranch" + ; Ensure current scope is an isolated/branching scope + .ifndef utils.clobbers{utils.clobbers.index}.isIsolated + .print "\. called but no utils.clobbing.withBranching in progress\n" + .fail + .endif + + ; Restore registers (but don't end preserve scope) + utils.registers.restoreRegisters +.endm + +;==== +; Closes the current utils.clobbers.withBranching scope without restoring the +; registers. This can be used if a utils.clobber.endBranch has already been +; called separately +; +; @fails if no utils.clobbers.withBranching scope is in progress +;==== +.macro "utils.clobbers.closeBranch" + ; Ensure current scope is an isolated/branching scope + .ifndef utils.clobbers{utils.clobbers.index}.isIsolated + .print "\. called but no utils.clobbing.withBranching in progress\n" + .fail + .endif + + ; End the branching preserve scope + utils.registers.closePreserveScope + .undefine utils.clobbers{utils.clobbers.index}.isIsolated + utils.clobbers.end +.endm + +;==== +; If carry is set, restore the registers for the active +; utils.clobbers.withBranching scope and jumps to the given label. If there are +; no registers to restore will just generate the jp c instruction +; +; @in label the label to jump to if carry is set +;==== +.macro "utils.clobbers.end.jpc" args label + utils.assert.equals NARGS 1 "\.: Expected a label argument" + utils.assert.label label "\.: Argument should be a label" + + ; Check if there are any registers to restore within this scope + utils.registers.getPreserved + .if utils.registers.getPreserved.returnValue == 0 + ; No registers to restore - just generate jump + jp c, label + .else + jr nc, _\@_\. + ; Restore registers then jump + utils.clobbers.endBranch + jp label + _\@_\.: + .endif +.endm + +;==== +; If carry is reset, restore the registers for the active +; utils.clobbers.withBranching scope and jumps to the given label. If there are +; no registers to restore will just generate the jp nc instruction +; +; @in label the label to jump to if carry is set +;==== +.macro "utils.clobbers.end.jpnc" args label + utils.assert.equals NARGS 1 "\.: Expected a label argument" + utils.assert.label label "\.: Argument should be a label" + + ; Check if there are any registers to restore within this scope + utils.registers.getPreserved + .if utils.registers.getPreserved.returnValue == 0 + ; No registers to restore - just generate jump + jp nc, label + .else + jr c, _\@_\. + ; Restore registers then jump + utils.clobbers.endBranch + jp label + _\@_\.: + .endif +.endm + +;==== +; If carry is set, restore the registers for the active +; utils.clobbers.withBranching scope and relative jumps to the given label. If +; there are no registers to restore will just generate the jr c instruction +; +; @in label the label to jump to if carry is set +;==== +.macro "utils.clobbers.end.jrc" args label + utils.assert.equals NARGS 1 "\.: Expected a label argument" + utils.assert.label label "\.: Argument should be a label" + + ; Check if there are any registers to restore within this scope + utils.registers.getPreserved + .if utils.registers.getPreserved.returnValue == 0 + ; No registers to restore - just generate jump + jr c, label + .else + jr nc, _\@_\. + ; Restore registers then jump + utils.clobbers.endBranch + jr label + _\@_\.: + .endif +.endm + +;==== +; If carry is reset, restore the registers for the active +; utils.clobbers.withBranching scope and relative jumps to the given label. If +; there are no registers to restore will just generate the jr nc instruction +; +; @in label the label to jump to if carry is set +;==== +.macro "utils.clobbers.end.jrnc" args label + utils.assert.equals NARGS 1 "\.: Expected a label argument" + utils.assert.label label "\.: Argument should be a label" + + ; Check if there are any registers to restore within this scope + utils.registers.getPreserved + .if utils.registers.getPreserved.returnValue == 0 + ; No registers to restore - just generate jump + jr nc, label + .else + jr c, _\@_\. + ; Restore registers then jump + utils.clobbers.endBranch + jr label + _\@_\.: + .endif +.endm + +;==== +; If parity/overflow is set, restore the registers for the active +; utils.clobbers.withBranching scope and jumps to the given label. If there are +; no registers to restore will just generate the jp po instruction +; +; @in label the label to jump to if carry is set +;==== +.macro "utils.clobbers.end.jppe" args label + utils.assert.equals NARGS 1 "\.: Expected a label argument" + utils.assert.label label "\.: Argument should be a label" + + ; Check if there are any registers to restore within this scope + utils.registers.getPreserved + .if utils.registers.getPreserved.returnValue == 0 + ; No registers to restore - just generate jump + jp pe, label + .else + jp po, _\@_\. + ; Restore registers then jump + utils.clobbers.endBranch + jp label + _\@_\.: + .endif +.endm + +;==== +; If parity/overflow is reset, restore the registers for the active +; utils.clobbers.withBranching scope and jumps to the given label. If there are +; no registers to restore will just generate the jp po instruction +; +; @in label the label to jump to if carry is set +;==== +.macro "utils.clobbers.end.jppo" args label + utils.assert.equals NARGS 1 "\.: Expected a label argument" + utils.assert.label label "\.: Argument should be a label" + + ; Check if there are any registers to restore within this scope + utils.registers.getPreserved + .if utils.registers.getPreserved.returnValue == 0 + ; No registers to restore - just generate jump + jp po, label + .else + jp pe, _\@_\. + ; Restore registers then jump + utils.clobbers.endBranch + jp label + _\@_\.: + .endif +.endm + +;==== +; If sign is reset, restore the registers for the active +; utils.clobbers.withBranching scope and jumps to the given label. If there are +; no registers to restore will just generate the jp p instruction +; +; @in label the label to jump to if carry is set +;==== +.macro "utils.clobbers.end.jpp" args label + utils.assert.equals NARGS 1 "\.: Expected a label argument" + utils.assert.label label "\.: Argument should be a label" + + ; Check if there are any registers to restore within this scope + utils.registers.getPreserved + .if utils.registers.getPreserved.returnValue == 0 + ; No registers to restore - just generate jump + jp p, label + .else + jp m, _\@_\. + ; Restore registers then jump + utils.clobbers.endBranch + jp label + _\@_\.: + .endif +.endm + +;==== +; If sign is set, restore the registers for the active +; utils.clobbers.withBranching scope and jumps to the given label. If there are +; no registers to restore will just generate the jp m instruction +; +; @in label the label to jump to if carry is set +;==== +.macro "utils.clobbers.end.jpm" args label + utils.assert.equals NARGS 1 "\.: Expected a label argument" + utils.assert.label label "\.: Argument should be a label" + + ; Check if there are any registers to restore within this scope + utils.registers.getPreserved + .if utils.registers.getPreserved.returnValue == 0 + ; No registers to restore - just generate jump + jp m, label + .else + jp p, _\@_\. + ; Restore registers then jump + utils.clobbers.endBranch + jp label + _\@_\.: + .endif +.endm + +;==== +; If Z is set, restore the registers for the active utils.clobbers.withBranching +; scope and jumps to the given label. If there are no registers to restore will +; just generate the jp z instruction +; +; @in label the label to jump to if Z is set +;==== +.macro "utils.clobbers.end.jpz" args label + utils.assert.equals NARGS 1 "\.: Expected a label argument" + utils.assert.label label "\.: Argument should be a label" + + ; Check if there are any registers to restore within this scope + utils.registers.getPreserved + .if utils.registers.getPreserved.returnValue == 0 + ; No registers to restore - just generate jump + jp z, label + .else + jr nz, _\@_\. + ; Restore registers then jump + utils.clobbers.endBranch + jp label + _\@_\.: + .endif +.endm + +;==== +; If NZ is set, restore the registers for the active utils.clobbers.withBranching +; scope and jumps to the given label. If there are no registers to restore will +; just generate the jp nz instruction +; +; @in label the label to jump to if Z is reset +;==== +.macro "utils.clobbers.end.jpnz" args label + utils.assert.equals NARGS 1 "\.: Expected a label argument" + utils.assert.label label "\.: Argument should be a label" + + ; Check if there are any registers to restore within this scope + utils.registers.getPreserved + .if utils.registers.getPreserved.returnValue == 0 + ; No registers to restore - just generate jump + jp nz, label + .else + jr z, _\@_\. + ; Restore registers then jump + utils.clobbers.endBranch + jp label + _\@_\.: + .endif +.endm + +;==== +; If Z is set, restore the registers for the active utils.clobbers.withBranching +; scope and relative jumps to the given label. If there are no registers to +; restore will just generate the jr z instruction +; +; @in label the label to jump to if Z is set +;==== +.macro "utils.clobbers.end.jrz" args label + utils.assert.equals NARGS 1 "\.: Expected a label argument" + utils.assert.label label "\.: Argument should be a label" + + ; Check if there are any registers to restore within this scope + utils.registers.getPreserved + .if utils.registers.getPreserved.returnValue == 0 + ; No registers to restore - just generate jump + jr z, label + .else + jr nz, _\@_\. + ; Restore registers then jump + utils.clobbers.endBranch + jr label + _\@_\.: + .endif +.endm + +;==== +; If NZ is set, restore the registers for the active utils.clobbers.withBranching +; scope and relative jumps to the given label. If there are no registers to +; restore will just generate the jr nz instruction +; +; @in label the label to jump to if Z is reset +;==== +.macro "utils.clobbers.end.jrnz" args label + utils.assert.equals NARGS 1 "\.: Expected a label argument" + utils.assert.label label "\.: Argument should be a label" + + ; Check if there are any registers to restore within this scope + utils.registers.getPreserved + .if utils.registers.getPreserved.returnValue == 0 + ; No registers to restore - just generate jump + jr nz, label + .else + jr z, _\@_\. + ; Restore registers then jump + utils.clobbers.endBranch + jr label + _\@_\.: + .endif +.endm + +;==== +; If carry is set, restore the registers for the active +; utils.clobbers.withBranching scope and return to the caller. +;==== +.macro "utils.clobbers.end.retc" + utils.assert.equals NARGS 0 "\.: Expected no arguments" + + ; Check if there are any registers to restore within this scope + utils.registers.getPreserved + .if utils.registers.getPreserved.returnValue == 0 + ret c ; no registers to restore + .else + jr nc, _\@_\. + ; Condition true - restore registers then ret + utils.clobbers.endBranch + ret + _\@_\.: + .endif +.endm + +;==== +; If carry is reset, restore the registers for the active +; utils.clobbers.withBranching scope and return to the caller. +;==== +.macro "utils.clobbers.end.retnc" + utils.assert.equals NARGS 0 "\.: Expected no arguments" + + ; Check if there are any registers to restore within this scope + utils.registers.getPreserved + .if utils.registers.getPreserved.returnValue == 0 + ret nc ; no registers to restore + .else + jr c, _\@_\. + ; Condition true - restore registers then ret + utils.clobbers.endBranch + ret + _\@_\.: + .endif +.endm + +;==== +; If zero is set, restore the registers for the active +; utils.clobbers.withBranching scope and return to the caller. +;==== +.macro "utils.clobbers.end.retz" + utils.assert.equals NARGS 0 "\.: Expected no arguments" + + ; Check if there are any registers to restore within this scope + utils.registers.getPreserved + .if utils.registers.getPreserved.returnValue == 0 + ret z ; no registers to restore + .else + jr nz, _\@_\. + ; Condition true - restore registers then ret + utils.clobbers.endBranch + ret + _\@_\.: + .endif +.endm + +;==== +; If zero is reset, restore the registers for the active +; utils.clobbers.withBranching scope and return to the caller. +;==== +.macro "utils.clobbers.end.retnz" + utils.assert.equals NARGS 0 "\.: Expected no arguments" + + ; Check if there are any registers to restore within this scope + utils.registers.getPreserved + .if utils.registers.getPreserved.returnValue == 0 + ret nz ; no registers to restore + .else + jr z, _\@_\. + ; Condition true - restore registers then ret + utils.clobbers.endBranch + ret + _\@_\.: + .endif +.endm + +;==== +; If parity/overflow is set, restore the registers for the active +; utils.clobbers.withBranching scope and return to the caller. +;==== +.macro "utils.clobbers.end.retpe" + utils.assert.equals NARGS 0 "\.: Expected no arguments" + + ; Check if there are any registers to restore within this scope + utils.registers.getPreserved + .if utils.registers.getPreserved.returnValue == 0 + ret pe ; no registers to restore + .else + jp po, _\@_\. + ; Condition true - restore registers then ret + utils.clobbers.endBranch + ret + _\@_\.: + .endif +.endm + +;==== +; If parity/overflow is reset, restore the registers for the active +; utils.clobbers.withBranching scope and return to the caller. +;==== +.macro "utils.clobbers.end.retpo" + utils.assert.equals NARGS 0 "\.: Expected no arguments" + + ; Check if there are any registers to restore within this scope + utils.registers.getPreserved + .if utils.registers.getPreserved.returnValue == 0 + ret po ; no registers to restore + .else + jp pe, _\@_\. + ; Condition true - restore registers then ret + utils.clobbers.endBranch + ret + _\@_\.: + .endif +.endm + +;==== +; If sign is set, restore the registers for the active +; utils.clobbers.withBranching scope and return to the caller. +;==== +.macro "utils.clobbers.end.retm" + utils.assert.equals NARGS 0 "\.: Expected no arguments" + + ; Check if there are any registers to restore within this scope + utils.registers.getPreserved + .if utils.registers.getPreserved.returnValue == 0 + ret m ; no registers to restore + .else + jp p, _\@_\. + ; Condition true - restore registers then ret + utils.clobbers.endBranch + ret + _\@_\.: + .endif +.endm + +;==== +; If sign is reset, restore the registers for the active +; utils.clobbers.withBranching scope and return to the caller. +;==== +.macro "utils.clobbers.end.retp" + utils.assert.equals NARGS 0 "\.: Expected no arguments" + + ; Check if there are any registers to restore within this scope + utils.registers.getPreserved + .if utils.registers.getPreserved.returnValue == 0 + ret p ; no registers to restore + .else + jp m, _\@_\. + ; Condition true - restore registers then ret + utils.clobbers.endBranch + ret + _\@_\.: + .endif +.endm diff --git a/utils/math.asm b/src/utils/math.asm similarity index 99% rename from utils/math.asm rename to src/utils/math.asm index 40a75a7..ca07f39 100644 --- a/utils/math.asm +++ b/src/utils/math.asm @@ -332,7 +332,7 @@ ; ; @source https://www.plutiedev.com/z80-add-8bit-to-16bit#add-signed ;==== -.macro "utils.math.addSignedHLA" +.macro "utils.math.addSignedHLA" isolated or a ; evaluate A jp p, + ; jp if A is not signed ; A is signed diff --git a/utils/outiBlock.asm b/src/utils/outiBlock.asm similarity index 80% rename from utils/outiBlock.asm rename to src/utils/outiBlock.asm index 41ed16b..4b035f0 100644 --- a/utils/outiBlock.asm +++ b/src/utils/outiBlock.asm @@ -49,6 +49,10 @@ .include "utils/assert.asm" .endif +.ifndef utils.clobbers + .include "utils/clobbers.asm" +.endif + ;==== ; Constants ;==== @@ -94,6 +98,8 @@ .macro "utils.outiBlock.write" args bytes utils.assert.range bytes 1 16384 "outiBlock.asm \.: Invalid bytes argument" + utils.clobbers "af", "bc", "hl" + ; Transfer chunks if data exceeds outi block size .rept bytes / utils.outiBlock.size call utils.outiBlock.block - utils.outiBlock.SIZE_BYTES @@ -108,17 +114,19 @@ .else call utils.outiBlock.block - (bytes # utils.outiBlock.size) * 2 .endif + + utils.clobbers.end .endm ;==== -; OUTI between 1-128 bytes +; (Private) OUTI between 1-128 bytes ; ; @in b the number of bytes to write. Must be greater than 0 and <= 128 ; @in c the port to output the data to ; @in hl the address of the source data ;==== -.section "utils.outiBlock.writeUpTo128Bytes" free - utils.outiBlock.writeUpTo128Bytes: +.section "utils.outiBlock._writeUpTo128Bytes" free + utils.outiBlock._writeUpTo128Bytes: ; Address of last OUTI instruction ld iyh, >(utils.outiBlock.lastOuti) ; high-byte address of last outi ld a, <(utils.outiBlock.lastOuti) ; load low-byte address of last outi @@ -134,6 +142,29 @@ jp (iy) .ends +;==== +; OUTI between 1-128 bytes +; +; @in b the number of bytes to write. Must be greater than 0 and <= 128 +; @in c the port to output the data to +; @in hl the address of the source data +;==== +.macro "utils.outiBlock.writeUpTo128Bytes" + utils.clobbers "af", "bc", "hl", "iy" + call utils.outiBlock._writeUpTo128Bytes + utils.clobbers.end +.endm + +;==== +; Like utils.outiBlock.writeUpTo128Bytes but 'jp's to the routine then returns +; to the original caller +;==== +.macro "utils.outiBlock.writeUpTo128BytesThenReturn" + utils.clobbers "af", "bc", "hl", "iy" + jp utils.outiBlock._writeUpTo128Bytes + utils.clobbers.end +.endm + ;==== ; Writes elements from an array of data to VRAM using OUTI instructions ; @@ -143,6 +174,8 @@ ; @in offset the first item in the array to copy (0-based) ;==== .macro "utils.outiBlock.writeSlice" args dataAddress elementSize count offset - ld hl, dataAddress + (offset * elementSize) - utils.outiBlock.write (count * elementSize) + utils.clobbers "hl" + ld hl, dataAddress + (offset * elementSize) + utils.outiBlock.write (count * elementSize) + utils.clobbers.end .endm diff --git a/utils/port.asm b/src/utils/port.asm similarity index 100% rename from utils/port.asm rename to src/utils/port.asm diff --git a/src/utils/preserve.asm b/src/utils/preserve.asm new file mode 100644 index 0000000..20d0cfa --- /dev/null +++ b/src/utils/preserve.asm @@ -0,0 +1,54 @@ +;==== +; Alias for utils.registers.preserve +;==== + +.define utils.preserve + +;==== +; Dependencies +;==== +.ifndef utils.registers + .include "utils/registers.asm" +.endif + +;==== +; Alias for utils.registers.preserve +;==== +.macro "utils.preserve" + .if nargs == 0 + utils.registers.preserve + .elif nargs == 1 + utils.registers.preserve \1 + .elif nargs == 2 + utils.registers.preserve \1 \2 + .elif nargs == 3 + utils.registers.preserve \1 \2 \3 + .elif nargs == 4 + utils.registers.preserve \1 \2 \3 \4 + .elif nargs == 5 + utils.registers.preserve \1 \2 \3 \4 \5 + .elif nargs == 6 + utils.registers.preserve \1 \2 \3 \4 \5 \6 + .elif nargs == 7 + utils.registers.preserve \1 \2 \3 \4 \5 \6 \7 + .elif nargs == 8 + utils.registers.preserve \1 \2 \3 \4 \5 \6 \7 \8 + .elif nargs == 9 + utils.registers.preserve \1 \2 \3 \4 \5 \6 \7 \8 \9 + .elif nargs == 10 + utils.registers.preserve \1 \2 \3 \4 \5 \6 \7 \8 \9 \10 + .elif nargs == 11 + utils.registers.preserve \1 \2 \3 \4 \5 \6 \7 \8 \9 \10 \11 + .else + .print "\.: Too many arguments passed\n" + .fail + .endif +.endm + +;==== +; Closes a preserve scope without producing the restore instructions. Sometimes +; needed by branching clobber scopes which restore the registers separately +;==== +.macro "utils.preserve.close" + utils.registers.closePreserveScope +.endm \ No newline at end of file diff --git a/utils/ram.asm b/src/utils/ram.asm similarity index 100% rename from utils/ram.asm rename to src/utils/ram.asm diff --git a/src/utils/registers.asm b/src/utils/registers.asm new file mode 100644 index 0000000..f815e7a --- /dev/null +++ b/src/utils/registers.asm @@ -0,0 +1,559 @@ +;==== +; Utilities to handle registers including efficient register preservation on +; the stack. +; +; Wrap macro calls within a preserve scope (utils.registers.preserve). Code +; that uses utils.clobbers to state which registers it clobbers will ensure +; the protected registers remain unclobbered. +;==== +.define utils.registers + +;==== +; Settings +;==== + +;=== +; If set to 1, clobber scopes will automatically start a preserve scope if one +; isn't already in progress +; (Default = 0) +;=== +.ifndef utils.registers.AUTO_PRESERVE + .define utils.registers.AUTO_PRESERVE 0 +.endif + +;=== +; The maximum size of the i-register preservation stack. Any unused entries +; will be discarded by wla-z80 unless it's invoked with the -k option +; (Default = 8) +;=== +.ifndef utils.registers.I_STACK_MAX_SIZE + .define utils.registers.I_STACK_MAX_SIZE 8 +.endif + +;==== +; Constants +;==== +.define utils.registers.AF %1 +.define utils.registers.BC %10 +.define utils.registers.DE %100 +.define utils.registers.HL %1000 +.define utils.registers.IX %10000 +.define utils.registers.IY %100000 +.define utils.registers.I %1000000 + +.define utils.registers.SHADOW_AF %10000000 +.define utils.registers.SHADOW_BC %100000000 +.define utils.registers.SHADOW_DE %1000000000 +.define utils.registers.SHADOW_HL %10000000000 + +.define utils.registers.ALL $ffff + +;==== +; Dependencies +;==== +.ifndef utils.assert + .include "utils/assert.asm" +.endif + +.ifndef utils.ram + .include "utils/ram.asm" +.endif + +;==== +; RAM +;==== + +;=== +; A stack to preserve the i-register value, as it's inefficient to +; preserve on the main stack without clobbing AF. +; +; Increase the max size by defining the utils.registers.I_STACK_MAX_SIZE setting. +; wla-z80 should discard entries that aren't used unless using the -k option +;=== +.repeat utils.registers.I_STACK_MAX_SIZE index index + .ramsection "utils.registers.iStack.{index}" slot utils.ram.SLOT + utils.registers.iStack.{index}: db + .ends +.endr + +;==== +; Variables +;==== +.define utils.registers.autoPreserveIndex -1 ; the current auto preserve scope +.define utils.registers.preserveIndex -1 ; the current preserve scope +.define utils.registers.iStackIndex -1 ; the current i register stack index + +;=== +; Also set: +; +; utils.registers.doNotClobber{preserveIndex} +; the registers that should be preserved if they are about to be clobbered +; +; utils.registers.unpreserved{preserveIndex} +; the registers that haven't been preserved on the stack yet, in the current +; preserve scope or ancestor scopes +; +; utils.registers.stack{preserveIndex}_size +; the number of registers currently preserved on the stack +; +; utils.registers.stack{preserveIndex}_{stackIndex} +; the register that has been stored at a given stack index +;=== + +;==== +; Called when the given register has been pushed to the stack +; +; @in register the register constant (i.e. utils.registers.AF, utils.registers.BC) +;==== +.macro "utils.registers._registerPushed" args register + ; Keep a record of the register on the variable stack + .define \.\@stackSize (utils.registers.stack{utils.registers.preserveIndex}_size) + .redefine utils.registers.stack{utils.registers.preserveIndex}_{\.\@stackSize} register + .redefine utils.registers.stack{utils.registers.preserveIndex}_size (\.\@stackSize + 1) + + ; Remove the register from the 'unpreserved' list + .define \.\@currentValue utils.registers.unpreserved{utils.registers.preserveIndex} + .redefine utils.registers.unpreserved{utils.registers.preserveIndex} (\.\@currentValue ~ register) +.endm + +;==== +; Adds a register to the 'do not clobber' list. Registers on this list will be +; preserved when a clobber scope states it will clobber that register +;==== +.macro "utils.registers._addDoNotClobber" args registers + .define \.\@newValue (utils.registers.doNotClobber{utils.registers.preserveIndex} | registers) + .redefine utils.registers.doNotClobber{utils.registers.preserveIndex} (\.\@newValue) +.endm + +;==== +; Creates a new preserve scope on top of the preserve scope stack +;==== +.macro "utils.registers._pushPreserveScope" + ; Increment preserve scope index + .redefine utils.registers.preserveIndex utils.registers.preserveIndex + 1 + + ; Default to an empty 'do not clobber' register list + .define utils.registers.doNotClobber{utils.registers.preserveIndex} 0 + + ; Check for existing preserve scopes + .if utils.registers.preserveIndex > 0 + ; Inherit the unpreserved registers from the outer scope(s) + ; Registers the outer scopes need that haven't been preserved yet + .define \.\@previousDoNotClobber (utils.registers.doNotClobber{utils.registers.preserveIndex - 1}) + .define \.\@previousUnpreserved (utils.registers.unpreserved{utils.registers.preserveIndex - 1}) + utils.registers._addDoNotClobber (\.\@previousDoNotClobber & \.\@previousUnpreserved) + .endif + + ; All registers are currently unpreserved in this scope + .define utils.registers.unpreserved{utils.registers.preserveIndex} utils.registers.ALL + + ; Initialise stack size to 0 + .define utils.registers.stack{utils.registers.preserveIndex}_size 0 +.endm + +;==== +; Pops the most recent preserve scope from the preserve scope variable stack +; without producing any restore instructions +;==== +.macro "utils.registers.closePreserveScope" + ; Assert there are preserve scopes in progress + .if utils.registers.preserveIndex == -1 + .print "\.: was called but no preserve scopes are in progress\n" + .fail + .endif + + ; If I was preserved on i-stack + .if (utils.registers.I & (utils.registers.unpreserved{utils.registers.preserveIndex} ~ $ff)) > 0 + ; Decrement i-stack size + .redefine utils.registers.iStackIndex utils.registers.iStackIndex - 1 + .endif + + ; Clear preserve stack registers + .repeat utils.registers.stack{utils.registers.preserveIndex}_size index index + .undefine utils.registers.stack{utils.registers.preserveIndex}_{index} + .endr + + ; Clean up registers + .undefine utils.registers.doNotClobber{utils.registers.preserveIndex} + .undefine utils.registers.unpreserved{utils.registers.preserveIndex} + .undefine utils.registers.stack{utils.registers.preserveIndex}_size + + ; Decrement scope index + .redefine utils.registers.preserveIndex utils.registers.preserveIndex - 1 +.endm + +;==== +; Parse a register identifier into one of the register constants +; +; @in rawValue the register string (i.e. "AF", "af", "HL'", "hl'") +; or one or more register constants ORed together +; +; @out utils.registers.parse.returnValue +; defined with the register constant +; +; @fails if the value cannot be parsed +;==== +.macro "utils.registers.parse" args rawValue + ; Resolve register string to a constant + .if \?1 != ARG_STRING + utils.assert.range \1 0 utils.registers.ALL "\.: Unknown register register value" + .redefine utils.registers.parse.returnValue \1 + .elif rawValue == "AF" || rawValue == "af" + .redefine utils.registers.parse.returnValue utils.registers.AF + .elif rawValue == "BC" || rawValue == "bc" + .redefine utils.registers.parse.returnValue utils.registers.BC + .elif rawValue == "DE" || rawValue == "de" + .redefine utils.registers.parse.returnValue utils.registers.DE + .elif rawValue == "HL" || rawValue == "hl" + .redefine utils.registers.parse.returnValue utils.registers.HL + .elif rawValue == "IX" || rawValue == "ix" + .redefine utils.registers.parse.returnValue utils.registers.IX + .elif rawValue == "IY" || rawValue == "iy" + .redefine utils.registers.parse.returnValue utils.registers.IY + .elif rawValue == "I" || rawValue == "i" + .redefine utils.registers.parse.returnValue utils.registers.I + .elif rawValue == "AF'" || rawValue == "af'" + .redefine utils.registers.parse.returnValue utils.registers.SHADOW_AF + .elif rawValue == "BC'" || rawValue == "bc'" + .redefine utils.registers.parse.returnValue utils.registers.SHADOW_BC + .elif rawValue == "DE'" || rawValue == "de'" + .redefine utils.registers.parse.returnValue utils.registers.SHADOW_DE + .elif rawValue == "HL'" || rawValue == "hl'" + .redefine utils.registers.parse.returnValue utils.registers.SHADOW_HL + .else + .print "\.: Unknown register value: ", string, "\n" + .fail + .endif +.endm + +;==== +; Restores the registers that have been preserved by the current preserve scope +;==== +.macro "utils.registers.restoreRegisters" + ; Assert there are preserve scopes in progress + .if utils.registers.preserveIndex == -1 + .print "\.: was called but no preserve scopes are in progress\n" + .fail + .endif + + ; Check if we need to restore the I register + .if (utils.registers.I & (utils.registers.unpreserved{utils.registers.preserveIndex} ~ $ff)) > 0 + .if utils.registers.iStackIndex == -1 + .print "\.: I stack popped but there's no value on it\n" + .fail + .endif + + ; This will clobber A - if it's needed it will be restored further below + ld a, (utils.registers.iStack.{utils.registers.iStackIndex}) + ld i, a + .endif + + ; Pop each register from the stack + .redefine \.\@stackSize utils.registers.stack{utils.registers.preserveIndex}_size + .repeat (\.\@stackSize) index index + ; Reverse order (start from top of stack) + .redefine index (\.\@stackSize) - index - 1 + + ; Pop register identifier from variable stack + .redefine \.\@register utils.registers.stack{utils.registers.preserveIndex}_{index} + + ; If it's one of the shadow registers + .if \.\@register & (utils.registers.SHADOW_BC | utils.registers.SHADOW_DE | utils.registers.SHADOW_HL) + ; Switch to shadow set if we haven't already + .ifndef \.\@exxUsed + exx + .define \.\@exxUsed + .endif + .elif \.\@register & (utils.registers.BC | utils.registers.DE | utils.registers.HL) + ; Non shadow register - switch to main set if needed + .ifdef \.\@exxUsed + exx ; switch back to non-shadow registers + .undefine \.\@exxUsed + .endif + .endif + + ; Pop relevant register from RAM stack + .if \.\@register == utils.registers.AF + pop af + .elif \.\@register == utils.registers.BC + pop bc + .elif \.\@register == utils.registers.DE + pop de + .elif \.\@register == utils.registers.HL + pop hl + .elif \.\@register == utils.registers.IX + pop ix + .elif \.\@register == utils.registers.IY + pop iy + .elif \.\@register == utils.registers.SHADOW_AF + ex af, af' + pop af' + ex af, af' + .elif \.\@register == utils.registers.SHADOW_BC + pop bc' ; switched to shadow set above + .elif \.\@register == utils.registers.SHADOW_DE + pop de' ; switched to shadow set above + .elif \.\@register == utils.registers.SHADOW_HL + pop hl' ; switched to shadow set above + .endif + .endr + + ; Switch back to non-shadow registers if we've used exx + .ifdef \.\@exxUsed + exx + .undefine \.\@exxUsed + .endif +.endm + +;==== +; Adds the given registers to the preserve list. When a register.clobberStart is +; called with any of these registers, they will be preserved. +; +; @in ...registers (optional) strings of one or more register pair to +; preserve +; Valid values: +; "AF", "BC", "DE", "HL", "IX", "IY", "I" +; "af", "bc", "de", "hl", "ix", "iy", "i" +; "AF'", "BC'", "DE'", "HL'" +; "af'", "bc'", "de'", "hl'", +; or one or more utils.registers.xx constants ORed +; together +; Defaults to all registers +;==== +.macro "utils.registers.preserve" + ; Create a new preserve scope + utils.registers._pushPreserveScope + + ; Add the given registers to the doNotClobber list + .if nargs == 0 + ; If no args, preserve all the main registers by default + utils.registers._addDoNotClobber utils.registers.ALL + .else + ; Combine (OR) all given args into doNotClobber value + .repeat nargs + ; Parse the register string into a constant + utils.registers.parse \1 + .redefine \.\@register utils.registers.parse.returnValue + + ; Set given bit in doNotClobber + utils.registers._addDoNotClobber (\.\@register) + .shift ; shift args + .endr + .endif +.endm + +;==== +; Marks the end of a preserve scope +;==== +.macro "utils.registers.restore" + ; Assert there are preserve scopes in progress + .if utils.registers.preserveIndex == -1 + .print "\.: was called but no preserve scopes are in progress\n" + .fail + .endif + + ; Restore the registers + utils.registers.restoreRegisters + + ; Remove the preserve scope + utils.registers.closePreserveScope +.endm + +;==== +; Preserves the given list of registers +; +; @in registers the list of register constants (i.e. register.AF) ORed +; together into one value +;==== +.macro "utils.registers._preserveRegisters" args registers + .if registers & utils.registers.AF + push af + utils.registers._registerPushed utils.registers.AF + .endif + + .if registers & utils.registers.BC + push bc + utils.registers._registerPushed utils.registers.BC + .endif + + .if registers & utils.registers.DE + push de + utils.registers._registerPushed utils.registers.DE + .endif + + .if registers & utils.registers.HL + push hl + utils.registers._registerPushed utils.registers.HL + .endif + + .if registers & utils.registers.IX + push ix + utils.registers._registerPushed utils.registers.IX + .endif + + .if registers & utils.registers.IY + push iy + utils.registers._registerPushed utils.registers.IY + .endif + + .if registers & utils.registers.I + .redefine utils.registers.iStackIndex utils.registers.iStackIndex + 1 + + .if utils.registers.iStackIndex >= utils.registers.I_STACK_MAX_SIZE + .print "\.: The I stack has exceeded its max size. Consider " + .print "increasing the utils.registers.I_STACK_MAX_SIZE value\n" + .fail + .endif + + push af + ld a, i + ld (utils.registers.iStack.{utils.registers.iStackIndex}), a + pop af + + utils.registers._registerPushed utils.registers.I + .endif + + .if registers & utils.registers.SHADOW_AF + ex af, af' + push af + utils.registers._registerPushed utils.registers.SHADOW_AF + ex af, af' + .endif + + ; 16-bit shadow registers + .if registers & (utils.registers.SHADOW_BC|utils.registers.SHADOW_DE|utils.registers.SHADOW_HL) + exx ; switch to shadow registers + .if registers & utils.registers.SHADOW_BC + push bc + utils.registers._registerPushed utils.registers.SHADOW_BC + .endif + + .if registers & utils.registers.SHADOW_DE + push de + utils.registers._registerPushed utils.registers.SHADOW_DE + .endif + + .if registers & utils.registers.SHADOW_HL + push hl + utils.registers._registerPushed utils.registers.SHADOW_HL + .endif + exx ; switch back to non-shadow registers + .endif +.endm + +;==== +; Returns the protected registers that have been preserved within the current +; preserve scope +; +; @out utils.registers.getPreserved.returnValue +; the registers (i.e. utils.registers.AF) ORed into a single value +;==== +.macro "utils.registers.getPreserved" + .if utils.registers.preserveIndex == -1 + .redefine utils.registers.getPreserved.returnValue 0 + .else + .redefine \.doNotClobber (utils.registers.doNotClobber{utils.registers.preserveIndex}) + .redefine \.preserved (utils.registers.unpreserved{utils.registers.preserveIndex}) ~ $ff + .redefine utils.registers.getPreserved.returnValue (\.doNotClobber & \.preserved) + .endif +.endm + +;==== +; Returns the registers that are protected within the current preserve scope +; or its ancestors +; +; @out utils.registers.getProtected.returnValue +; the registers (i.e. utils.registers.AF) ORed into a single value +;==== +.macro "utils.registers.getProtected" + .if utils.registers.preserveIndex == -1 + .redefine utils.registers.getProtected.returnValue 0 + .else + .redefine \.doNotClobber (utils.registers.doNotClobber{utils.registers.preserveIndex}) + .redefine utils.registers.getProtected.returnValue (\.doNotClobber) + .endif +.endm + +;==== +; Returns the registers that should be protected but haven't been preserved yet +; +; @out utils.registers.getVulnerable.returnValue +; the registers (i.e. utils.registers.AF) ORed into a single value +;==== +.macro "utils.registers.getVulnerable" + .if utils.registers.preserveIndex == -1 + .redefine utils.registers.getVulnerable.returnValue 0 + .else + .redefine \.doNotClobber (utils.registers.doNotClobber{utils.registers.preserveIndex}) + .redefine \.unpreserved (utils.registers.unpreserved{utils.registers.preserveIndex}) + .redefine utils.registers.getVulnerable.returnValue (\.doNotClobber & \.unpreserved) + .endif +.endm + +;==== +; Should be called when the initial/outer-most clobber scope has been started +;==== +.macro "utils.registers.onInitialClobberScope" + ; If auto preserve is enabled and there are no existing preserve scopes + .if utils.registers.AUTO_PRESERVE == 1 && utils.registers.preserveIndex == -1 + ; Preserve all registers including shadow registers + utils.registers.preserve "af", "bc", "de", "hl", "ix", "iy", "i", "af'", "bc'", "de'", "hl'" + .redefine utils.registers.autoPreserveIndex utils.registers.preserveIndex + .endif +.endm + +;==== +; Should be called when the last/outer-most scope has been closed +;==== +.macro "utils.registers.onFinalClobberScopeEnd" + ; If this is the auto preserve scope + .if utils.registers.preserveIndex > -1 && utils.registers.preserveIndex == utils.registers.autoPreserveIndex + utils.registers.restore + .redefine utils.registers.autoPreserveIndex -1 + .endif +.endm + +;==== +; Loads the given value into the given register +; +; @in register one of 'a', 'b', 'c', 'd', 'e', 'h', 'l', 'ixh', +; 'ixl', 'bc', 'de', 'hl', 'ix', 'iy' +; @in value the value to load into the register +;==== +.macro "utils.registers.load" args register value + .if register == "a" + ld a, value + .elif register == "b" + ld b, value + .elif register == "c" + ld c, value + .elif register == "d" + ld d, value + .elif register == "e" + ld e, value + .elif register == "h" + ld h, value + .elif register == "l" + ld l, value + .elif register == "ixh" + ld ixh, value + .elif register == "ixl" + ld ixl, value + .elif register == "iyh" + ld iyh, value + .elif register == "iyl" + ld iyl, value + .elif register == "bc" + ld bc, value + .elif register == "de" + ld de, value + .elif register == "hl" + ld hl, value + .elif register == "ix" + ld ix, value + .elif register == "iy" + ld iy, value + .else + utils.assert.fail "\.: Unsupported register", register, " (one of) 'a', 'b', 'c', 'd', 'e', 'h', 'l', 'ixh', 'ixl', 'bc', 'de', 'hl', 'ix', 'iy'" + .endif +.endm diff --git a/src/utils/restore.asm b/src/utils/restore.asm new file mode 100644 index 0000000..8f04e1a --- /dev/null +++ b/src/utils/restore.asm @@ -0,0 +1,19 @@ +;==== +; Alias for utils.registers.restore +;==== + +.define utils.restore + +;==== +; Dependencies +;==== +.ifndef utils.registers + .include "utils/registers.asm" +.endif + +;==== +; Alias for utils.registers.restore +;==== +.macro "utils.restore" + utils.registers.restore +.endm diff --git a/utils/vdp.asm b/src/utils/vdp.asm similarity index 87% rename from utils/vdp.asm rename to src/utils/vdp.asm index 133f846..0879bd9 100644 --- a/utils/vdp.asm +++ b/src/utils/vdp.asm @@ -11,6 +11,10 @@ .include "utils/assert.asm" .endif +.ifndef utils.clobbers + .include "utils/clobbers.asm" +.endif + .ifndef utils.outiBlock .include "utils/outiBlock.asm" .endif @@ -42,27 +46,33 @@ ; cycles) ;==== .macro "utils.vdp.prepWrite" args address setPort - ; Output low byte to VDP - .ifeq
address | utils.vdp.commands.WRITE - out (utils.vdp.COMMAND_PORT), a - - ; Port to write to + ; Default setPort to 1 .ifndef setPort .redefine setPort 1 .endif - .if setPort == 1 - ld c, utils.vdp.DATA_PORT - .endif + utils.clobbers "af" + ; Output low byte to VDP + .ifeq
address | utils.vdp.commands.WRITE + out (utils.vdp.COMMAND_PORT), a + + .if setPort == 1 + ; Port to write to + ld c, utils.vdp.DATA_PORT + .endif + utils.clobbers.end .endm ;==== diff --git a/vdp.asm b/src/vdp.asm similarity index 100% rename from vdp.asm rename to src/vdp.asm diff --git a/tests/build.sh b/tests/build.sh index 256a63b..988d7fd 100755 --- a/tests/build.sh +++ b/tests/build.sh @@ -20,7 +20,7 @@ assemble() { # Create ROM from object files cd $build_dir - wlalink -v -d -S -A $link_file $name.sms + wlalink -d -S -A $link_file $name.sms # Delete temp files rm -f $build_dir/$name.o $link_file diff --git a/tests/input/if.test.asm b/tests/input/if.test.asm index 672b5c0..8807adc 100644 --- a/tests/input/if.test.asm +++ b/tests/input/if.test.asm @@ -1,3 +1,5 @@ +.redefine utils.registers.AUTO_PRESERVE 1 + .repeat 2 index controller describe { "input.if should run the code block if the given button is pressed (controller {controller + 1})" } .repeat 6 index buttonNumber @@ -6,9 +8,11 @@ test { "{BUTTON_NAME} button" } test.input.mockController controller, FAKE_INPUT + zest.initRegisters + input.if TEST_INPUT, + - ; Pass test - jp ++ + expect.all.toBeUnclobbered + jp ++ ; pass test +: ; Otherwise, fail @@ -24,20 +28,26 @@ test { "{BUTTON_NAME} button" } test.input.mockController controller, ALL_BUTTONS_EXCEPT_CURRENT + zest.initRegisters + input.if TEST_INPUT, + ; Fail test zest.fail +: + + expect.all.toBeUnclobbered .endr describe { "input.if when multiple buttons are given (controller {controller + 1})" } it { "should run the code block when all buttons are pressed" } test.input.mockController controller, $ff ; all buttons pressed + zest.initRegisters + ; Check if all buttons are pressed input.if input.UP, input.DOWN, input.LEFT, input.RIGHT, input.BUTTON_1, input.BUTTON_2, + - ; Pass test - jp ++ + expect.all.toBeUnclobbered + jp ++ ; pass test +: ; Otherwise, fail @@ -51,9 +61,15 @@ test.input.defineButtonData buttonNumber ; set constants (see helpers) test.input.mockController controller, ALL_BUTTONS_EXCEPT_CURRENT + zest.initRegisters + ; Check if all buttons are pressed input.if input.UP, input.DOWN, input.LEFT, input.RIGHT, input.BUTTON_1, input.BUTTON_2, + zest.fail ; fail test +: + + expect.all.toBeUnclobbered .endr .endr + +.redefine utils.registers.AUTO_PRESERVE 0 \ No newline at end of file diff --git a/tests/input/ifHeld.test.asm b/tests/input/ifHeld.test.asm index 0529257..c195dc0 100644 --- a/tests/input/ifHeld.test.asm +++ b/tests/input/ifHeld.test.asm @@ -1,3 +1,5 @@ +.redefine utils.registers.AUTO_PRESERVE 1 + ; Each controller .repeat 2 index controller describe { "input.ifHeld (controller {controller + 1}) should run the code block when the given button has been pressed for two frames" } @@ -7,7 +9,10 @@ test { "{BUTTON_NAME} button" } test.input.mockController controller, FAKE_INPUT, 2 + zest.initRegisters + input.ifHeld TEST_INPUT, + + expect.all.toBeUnclobbered jp ++ ; pass test +: @@ -24,9 +29,13 @@ test { "{BUTTON_NAME} button" } test.input.mockController controller, ALL_BUTTONS_EXCEPT_CURRENT, 2 + zest.initRegisters + input.ifHeld TEST_INPUT, + zest.fail ; this shouldn't run +: + + expect.all.toBeUnclobbered .endr describe { "input.ifHeld should jump over the code block if the button is not pressed this frame (controller {controller + 1})" } @@ -37,9 +46,13 @@ test.input.mockController controller, FAKE_INPUT ; last frame test.input.mockController controller, ALL_BUTTONS_EXCEPT_CURRENT ; this frame + zest.initRegisters + input.ifHeld TEST_INPUT, + zest.fail +: + + expect.all.toBeUnclobbered .endr describe { "input.ifHeld should jump over the code block if the button is was not pressed last frame (controller {controller + 1})" } @@ -50,9 +63,13 @@ test.input.mockController controller, ALL_BUTTONS_EXCEPT_CURRENT ; last frame test.input.mockController controller, FAKE_INPUT ; this frame + zest.initRegisters + input.ifHeld TEST_INPUT, + zest.fail ; this shouldn't run +: + + expect.all.toBeUnclobbered .endr ;==== @@ -63,7 +80,10 @@ test { "should run the code block when all the given buttons have been pressed for two frames" } test.input.mockController controller, ALL_BUTTONS, 2 + zest.initRegisters + input.ifHeld input.UP, input.DOWN, input.LEFT, input.RIGHT, input.BUTTON_1, input.BUTTON_2, + + expect.all.toBeUnclobbered jp ++ ; pass test +: @@ -80,9 +100,13 @@ test.input.mockController controller, ALL_BUTTONS_EXCEPT_CURRENT ; last frame test.input.mockController controller, ALL_BUTTONS ; this frame + zest.initRegisters + input.ifHeld input.UP, input.DOWN, input.LEFT, input.RIGHT, input.BUTTON_1, input.BUTTON_2, + zest.fail ; this shouldn't run +: + + expect.all.toBeUnclobbered .endr describe { "input.ifHeld should jump over the code block if not all given buttons are pressed this frame (controller {controller + 1})" } @@ -93,9 +117,13 @@ test.input.mockController controller, ALL_BUTTONS ; last frame test.input.mockController controller, ALL_BUTTONS_EXCEPT_CURRENT ; this frame + zest.initRegisters + input.ifHeld input.UP, input.DOWN, input.LEFT, input.RIGHT, input.BUTTON_1, input.BUTTON_2, + zest.fail ; this shouldn't run +: + + expect.all.toBeUnclobbered .endr describe { "input.ifHeld should jump over the code block if not all given buttons are pressed this frame or last frame (controller {controller + 1})" } @@ -105,7 +133,13 @@ test { "{BUTTON_NAME} button" } test.input.mockController controller, ALL_BUTTONS_EXCEPT_CURRENT, 2 + zest.initRegisters + input.ifHeld input.UP, input.DOWN, input.LEFT, input.RIGHT, input.BUTTON_1, input.BUTTON_2, + zest.fail ; this shouldn't run +: + + expect.all.toBeUnclobbered .endr + +.redefine utils.registers.AUTO_PRESERVE 0 diff --git a/tests/input/ifPressed.test.asm b/tests/input/ifPressed.test.asm index f9568d2..bf93c99 100644 --- a/tests/input/ifPressed.test.asm +++ b/tests/input/ifPressed.test.asm @@ -1,3 +1,5 @@ +.redefine utils.registers.AUTO_PRESERVE 1 + .repeat 2 index controller describe { "input.ifPressed should run the code block if the given button wasn't pressed last frame but is now (controller {controller + 1})" } .repeat 6 index buttonNumber @@ -7,9 +9,11 @@ test.input.mockController controller, ALL_BUTTONS_EXCEPT_CURRENT ; last frame test.input.mockController controller, FAKE_INPUT ; this frame + zest.initRegisters + input.ifPressed TEST_INPUT, + - ; Pass test - jp ++ + expect.all.toBeUnclobbered + jp ++ ; pass test +: ; Otherwise, fail @@ -25,10 +29,14 @@ test { "{BUTTON_NAME} button" } test.input.mockController controller, ALL_BUTTONS_EXCEPT_CURRENT + zest.initRegisters + input.ifPressed TEST_INPUT, + ; Fail test zest.fail +: + + expect.all.toBeUnclobbered .endr describe { "input.ifPressed should jump over the code block if the given button was pressed last frame but not this frame (controller {controller + 1})" } @@ -39,10 +47,14 @@ test.input.mockController controller, FAKE_INPUT ; was pressed test.input.mockController controller, ALL_BUTTONS_EXCEPT_CURRENT ; isn't now + zest.initRegisters + input.ifPressed TEST_INPUT, + ; Fail test zest.fail +: + + expect.all.toBeUnclobbered .endr ;==== @@ -57,9 +69,11 @@ test.input.mockController controller, ALL_BUTTONS_EXCEPT_CURRENT ; last frame test.input.mockController controller, ALL_BUTTONS ; this frame + zest.initRegisters + input.ifPressed input.UP, input.DOWN, input.LEFT, input.RIGHT, input.BUTTON_1, input.BUTTON_2, + - ; Pass test - jp ++ + expect.all.toBeUnclobbered + jp ++ ; pass test +: ; Otherwise, fail @@ -75,18 +89,28 @@ test { "{BUTTON_NAME} button" } test.input.mockController controller, ALL_BUTTONS_EXCEPT_CURRENT + zest.initRegisters + ; Check if all buttons are pressed input.ifPressed input.UP, input.DOWN, input.LEFT, input.RIGHT, input.BUTTON_1, input.BUTTON_2, + zest.fail ; fail test +: + + expect.all.toBeUnclobbered .endr describe { "input.ifPressed when multiple buttons are given (controller {controller + 1})" } test { "should jp over the code block if all given buttons were pressed but were already pressed last frame" } test.input.mockController controller, ALL_BUTTONS, 2 + zest.initRegisters + ; Check if all buttons are pressed input.ifPressed input.UP, input.DOWN, input.LEFT, input.RIGHT, input.BUTTON_1, input.BUTTON_2, + zest.fail ; fail test +: + + expect.all.toBeUnclobbered .endr + +.redefine utils.registers.AUTO_PRESERVE 0 \ No newline at end of file diff --git a/tests/input/ifReleased.test.asm b/tests/input/ifReleased.test.asm index 6710a93..e3622d6 100644 --- a/tests/input/ifReleased.test.asm +++ b/tests/input/ifReleased.test.asm @@ -1,3 +1,5 @@ +.redefine utils.registers.AUTO_PRESERVE 1 + .repeat 2 index controller describe { "input.ifReleased should run the code block if the given button was pressed last frame but isn't now (controller {controller + 1})" } .repeat 6 index buttonNumber @@ -7,9 +9,11 @@ test.input.mockController controller, FAKE_INPUT ; was pressed test.input.mockController controller, ALL_BUTTONS_EXCEPT_CURRENT ; isn't now + zest.initRegisters + input.ifReleased TEST_INPUT, + - ; Pass test - jp ++ + expect.all.toBeUnclobbered + jp ++ ; pass test +: ; Otherwise, fail @@ -25,10 +29,13 @@ test { "{BUTTON_NAME} button" } test.input.mockController controller, FAKE_INPUT + zest.initRegisters + input.ifReleased TEST_INPUT, + - ; Fail test zest.fail +: + + expect.all.toBeUnclobbered .endr describe { "input.ifReleased should jump over the code block if the given button wasn't pressed last frame (controller {controller + 1})" } @@ -38,10 +45,13 @@ test { "{BUTTON_NAME} button" } test.input.mockController controller, ALL_BUTTONS_EXCEPT_CURRENT, 2 + zest.initRegisters + input.ifReleased TEST_INPUT, + - ; Fail test zest.fail +: + + expect.all.toBeUnclobbered .endr ;==== @@ -56,9 +66,11 @@ test.input.mockController controller, ALL_BUTTONS ; this frame test.input.mockController controller, ALL_BUTTONS_EXCEPT_CURRENT ; last frame + zest.initRegisters + input.ifReleased input.UP, input.DOWN, input.LEFT, input.RIGHT, input.BUTTON_1, input.BUTTON_2, + - ; Pass test - jp ++ + expect.all.toBeUnclobbered + jp ++ ; pass test +: ; Otherwise, fail @@ -75,9 +87,15 @@ test.input.mockController controller, ALL_BUTTONS_EXCEPT_CURRENT ; last frame test.input.mockController controller, NO_BUTTONS ; this frame + zest.initRegisters + ; Check if all buttons are pressed input.ifReleased input.UP, input.DOWN, input.LEFT, input.RIGHT, input.BUTTON_1, input.BUTTON_2, + zest.fail ; fail test +: + + expect.all.toBeUnclobbered .endr .endr + +.redefine utils.registers.AUTO_PRESERVE 0 diff --git a/tests/input/ifXDir.test.asm b/tests/input/ifXDir.test.asm new file mode 100644 index 0000000..98935e5 --- /dev/null +++ b/tests/input/ifXDir.test.asm @@ -0,0 +1,52 @@ +.repeat 2 index controller + describe { "input.ifXDir (pad {controller + 1})" } + test "jps when neither left nor right are pressed" + test.input.mockController controller, zest.NO_INPUT + + zest.initRegisters + + utils.preserve + input.ifXDir +, ++, +++ + utils.restore + +: + zest.fail "Left called" + ++: + zest.fail "Right called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to left when left is pressed" + test.input.mockController controller, zest.LEFT + + zest.initRegisters + + utils.preserve + input.ifXDir +, ++, +++ + utils.restore + +: + expect.all.toBeUnclobbered + jp ++++ ; pass + ++: + zest.fail "Right called" + +++: + zest.fail "Else called" + ++++: + + test "jps to right when right is pressed" + test.input.mockController controller, zest.RIGHT + + zest.initRegisters + + utils.preserve + input.ifXDir +, ++, +++ + utils.restore + +: + zest.fail "Left called" + ++: + expect.all.toBeUnclobbered + jp ++++ ; pass + +++: + zest.fail "Else called" + ++++: +.endr diff --git a/tests/input/ifXDirHeld.test.asm b/tests/input/ifXDirHeld.test.asm new file mode 100644 index 0000000..bac682b --- /dev/null +++ b/tests/input/ifXDirHeld.test.asm @@ -0,0 +1,120 @@ +.repeat 2 index controller + describe { "input.ifXDirHeld (pad {controller + 1})" } + test "jps to else when neither left nor right are pressed" + test.input.mockController controller, zest.NO_INPUT 2 + + zest.initRegisters + + utils.preserve + input.ifXDirHeld +, ++, +++ + utils.restore + +: + zest.fail "Left called" + ++: + zest.fail "Right called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to else when left was pressed last frame but not this frame" + test.input.mockController controller, zest.LEFT + test.input.mockController controller, zest.NO_INPUT + + zest.initRegisters + + utils.preserve + input.ifXDirHeld +, ++, +++ + utils.restore + +: + zest.fail "Left called" + ++: + zest.fail "Right called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to else when right was pressed last frame but not this frame" + test.input.mockController controller, zest.RIGHT + test.input.mockController controller, zest.NO_INPUT + + zest.initRegisters + + utils.preserve + input.ifXDirHeld +, ++, +++ + utils.restore + +: + zest.fail "Left called" + ++: + zest.fail "Right called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to else when left was pressed this frame but not last frame" + test.input.mockController controller, zest.NO_INPUT + test.input.mockController controller, zest.LEFT + + zest.initRegisters + + utils.preserve + input.ifXDirHeld +, ++, +++ + utils.restore + +: + zest.fail "Left called" + ++: + zest.fail "Right called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to else when right was pressed this frame but not last frame" + test.input.mockController controller, zest.NO_INPUT + test.input.mockController controller, zest.RIGHT + + zest.initRegisters + + utils.preserve + input.ifXDirHeld +, ++, +++ + utils.restore + +: + zest.fail "Left called" + ++: + zest.fail "Right called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to left when left is held" + test.input.mockController controller, zest.LEFT 2 + + zest.initRegisters + + utils.preserve + input.ifXDirHeld +, ++, +++ + utils.restore + +: + expect.all.toBeUnclobbered + jp ++++ ; pass + ++: + zest.fail "Right called" + +++: + zest.fail "Else called" + ++++: + + test "jps to right when right is pressed" + test.input.mockController controller, zest.RIGHT 2 + + zest.initRegisters + + utils.preserve + input.ifXDirHeld +, ++, +++ + utils.restore + +: + zest.fail "Left called" + ++: + expect.all.toBeUnclobbered + jp ++++ ; pass + +++: + zest.fail "Else called" + ++++: +.endr diff --git a/tests/input/ifXDirPressed.test.asm b/tests/input/ifXDirPressed.test.asm new file mode 100644 index 0000000..219d991 --- /dev/null +++ b/tests/input/ifXDirPressed.test.asm @@ -0,0 +1,120 @@ +.repeat 2 index controller + describe { "input.ifXDirPressed (pad {controller + 1})" } + test "jps to else when neither left nor right are pressed" + test.input.mockController controller, zest.NO_INPUT + + zest.initRegisters + + utils.preserve + input.ifXDirPressed +, ++, +++ + utils.restore + +: + zest.fail "Left called" + ++: + zest.fail "Right called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to else when left was pressed last frame and this frame" + test.input.mockController controller, zest.LEFT, 2 + + zest.initRegisters + + utils.preserve + input.ifXDirPressed +, ++, +++ + utils.restore + +: + zest.fail "Left called" + ++: + zest.fail "Right called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to else when right was pressed last frame and this frame" + test.input.mockController controller, zest.RIGHT, 2 + + zest.initRegisters + + utils.preserve + input.ifXDirPressed +, ++, +++ + utils.restore + +: + zest.fail "Left called" + ++: + zest.fail "Right called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to else when left was pressed last frame but not this frame" + test.input.mockController controller, zest.LEFT + test.input.mockController controller, zest.NO_INPUT + + zest.initRegisters + + utils.preserve + input.ifXDirPressed +, ++, +++ + utils.restore + +: + zest.fail "Left called" + ++: + zest.fail "Right called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to else when right was pressed last frame but not this frame" + test.input.mockController controller, zest.RIGHT + test.input.mockController controller, zest.NO_INPUT + + zest.initRegisters + + utils.preserve + input.ifXDirPressed +, ++, +++ + utils.restore + +: + zest.fail "Left called" + ++: + zest.fail "Right called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to left when left was not pressed last frame but is this frame" + test.input.mockController controller, zest.NO_INPUT + test.input.mockController controller, zest.LEFT + + zest.initRegisters + + utils.preserve + input.ifXDirPressed +, ++, +++ + utils.restore + +: + expect.all.toBeUnclobbered + jp ++++ ; pass + ++: + zest.fail "Right called" + +++: + zest.fail "Else called" + ++++: + + test "jps to right when right was not pressed last frame but is this frame" + test.input.mockController controller, zest.NO_INPUT + test.input.mockController controller, zest.RIGHT + + zest.initRegisters + + utils.preserve + input.ifXDirPressed +, ++, +++ + utils.restore + +: + zest.fail "Left called" + ++: + expect.all.toBeUnclobbered + jp ++++ ; pass + +++: + zest.fail "Else called" + ++++: +.endr diff --git a/tests/input/ifXDirReleased.test.asm b/tests/input/ifXDirReleased.test.asm new file mode 100644 index 0000000..d96ceed --- /dev/null +++ b/tests/input/ifXDirReleased.test.asm @@ -0,0 +1,120 @@ +.repeat 2 index controller + describe { "input.ifXDirReleased (pad {controller + 1})" } + test "jps to else when neither left nor right were pressed this frame or last frame" + test.input.mockController controller, zest.NO_INPUT, 2 + + zest.initRegisters + + utils.preserve + input.ifXDirReleased +, ++, +++ + utils.restore + +: + zest.fail "Left called" + ++: + zest.fail "Right called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to else when left was pressed last frame and this frame" + test.input.mockController controller, zest.LEFT, 2 + + zest.initRegisters + + utils.preserve + input.ifXDirReleased +, ++, +++ + utils.restore + +: + zest.fail "Left called" + ++: + zest.fail "Right called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to else when right was pressed last frame and this frame" + test.input.mockController controller, zest.RIGHT, 2 + + zest.initRegisters + + utils.preserve + input.ifXDirReleased +, ++, +++ + utils.restore + +: + zest.fail "Left called" + ++: + zest.fail "Right called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to else when left is pressed this frame but not last frame" + test.input.mockController controller, zest.NO_INPUT + test.input.mockController controller, zest.LEFT + + zest.initRegisters + + utils.preserve + input.ifXDirReleased +, ++, +++ + utils.restore + +: + zest.fail "Left called" + ++: + zest.fail "Right called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to else when right was pressed this frame but not last frame" + test.input.mockController controller, zest.NO_INPUT + test.input.mockController controller, zest.RIGHT + + zest.initRegisters + + utils.preserve + input.ifXDirReleased +, ++, +++ + utils.restore + +: + zest.fail "Left called" + ++: + zest.fail "Right called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to left when left was pressed last frame but isn't this frame" + test.input.mockController controller, zest.LEFT + test.input.mockController controller, zest.NO_INPUT + + zest.initRegisters + + utils.preserve + input.ifXDirReleased +, ++, +++ + utils.restore + +: + expect.all.toBeUnclobbered + jp ++++ ; pass + ++: + zest.fail "Right called" + +++: + zest.fail "Else called" + ++++: + + test "jps to right when right was pressed last frame but isn't this frame" + test.input.mockController controller, zest.RIGHT + test.input.mockController controller, zest.NO_INPUT + + zest.initRegisters + + utils.preserve + input.ifXDirReleased +, ++, +++ + utils.restore + +: + zest.fail "Left called" + ++: + expect.all.toBeUnclobbered + jp ++++ ; pass + +++: + zest.fail "Else called" + ++++: +.endr diff --git a/tests/input/ifYDir.test.asm b/tests/input/ifYDir.test.asm new file mode 100644 index 0000000..d3ea919 --- /dev/null +++ b/tests/input/ifYDir.test.asm @@ -0,0 +1,52 @@ +.repeat 2 index controller + describe { "input.ifYDir (pad {controller + 1})" } + test "jps to else when neither up nor down are pressed" + test.input.mockController controller, zest.NO_INPUT + + zest.initRegisters + + utils.preserve + input.ifYDir +, ++, +++ + utils.restore + +: + zest.fail "Up called" + ++: + zest.fail "Down called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to up when up is pressed" + test.input.mockController controller, zest.UP + + zest.initRegisters + + utils.preserve + input.ifYDir +, ++, +++ + utils.restore + +: + expect.all.toBeUnclobbered + jp ++++ ; pass + ++: + zest.fail "Down called" + +++: + zest.fail "Else called" + ++++: + + test "jps to down when down is pressed" + test.input.mockController controller, zest.DOWN + + zest.initRegisters + + utils.preserve + input.ifYDir +, ++, +++ + utils.restore + +: + zest.fail "Up called" + ++: + expect.all.toBeUnclobbered + jp ++++ ; pass + +++: + zest.fail "Else called" + ++++: +.endr diff --git a/tests/input/ifYDirHeld.test.asm b/tests/input/ifYDirHeld.test.asm new file mode 100644 index 0000000..da6b297 --- /dev/null +++ b/tests/input/ifYDirHeld.test.asm @@ -0,0 +1,120 @@ +.repeat 2 index controller + describe { "input.ifYDirHeld (pad {controller + 1})" } + test "jps to else when neither up nor down are pressed" + test.input.mockController controller, zest.NO_INPUT 2 + + zest.initRegisters + + utils.preserve + input.ifYDirHeld +, ++, +++ + utils.restore + +: + zest.fail "Up called" + ++: + zest.fail "Down called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to else when up was pressed last frame but not this frame" + test.input.mockController controller, zest.UP + test.input.mockController controller, zest.NO_INPUT + + zest.initRegisters + + utils.preserve + input.ifYDirHeld +, ++, +++ + utils.restore + +: + zest.fail "Up called" + ++: + zest.fail "Down called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to else when down was pressed last frame but not this frame" + test.input.mockController controller, zest.DOWN + test.input.mockController controller, zest.NO_INPUT + + zest.initRegisters + + utils.preserve + input.ifYDirHeld +, ++, +++ + utils.restore + +: + zest.fail "Up called" + ++: + zest.fail "Down called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to else when up was pressed this frame but not last frame" + test.input.mockController controller, zest.NO_INPUT + test.input.mockController controller, zest.UP + + zest.initRegisters + + utils.preserve + input.ifYDirHeld +, ++, +++ + utils.restore + +: + zest.fail "Up called" + ++: + zest.fail "Down called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to else when down was pressed this frame but not last frame" + test.input.mockController controller, zest.NO_INPUT + test.input.mockController controller, zest.DOWN + + zest.initRegisters + + utils.preserve + input.ifYDirHeld +, ++, +++ + utils.restore + +: + zest.fail "Up called" + ++: + zest.fail "Down called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to up when up is held" + test.input.mockController controller, zest.UP 2 + + zest.initRegisters + + utils.preserve + input.ifYDirHeld +, ++, +++ + utils.restore + +: + expect.all.toBeUnclobbered + jp ++++ ; pass + ++: + zest.fail "Down called" + +++: + zest.fail "Else called" + ++++: + + test "jps to down when down is pressed" + test.input.mockController controller, zest.DOWN 2 + + zest.initRegisters + + utils.preserve + input.ifYDirHeld +, ++, +++ + utils.restore + +: + zest.fail "Up called" + ++: + expect.all.toBeUnclobbered + jp ++++ ; pass + +++: + zest.fail "Else called" + ++++: +.endr diff --git a/tests/input/ifYDirPressed.test.asm b/tests/input/ifYDirPressed.test.asm new file mode 100644 index 0000000..247330c --- /dev/null +++ b/tests/input/ifYDirPressed.test.asm @@ -0,0 +1,120 @@ +.repeat 2 index controller + describe { "input.ifYDirPressed (pad {controller + 1})" } + test "jps to else when neither up nor down are pressed" + test.input.mockController controller, zest.NO_INPUT + + zest.initRegisters + + utils.preserve + input.ifYDirPressed +, ++, +++ + utils.restore + +: + zest.fail "Up called" + ++: + zest.fail "Down called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to else when up was pressed last frame and this frame" + test.input.mockController controller, zest.UP, 2 + + zest.initRegisters + + utils.preserve + input.ifYDirPressed +, ++, +++ + utils.restore + +: + zest.fail "Up called" + ++: + zest.fail "Down called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to else when down was pressed last frame and this frame" + test.input.mockController controller, zest.DOWN, 2 + + zest.initRegisters + + utils.preserve + input.ifYDirPressed +, ++, +++ + utils.restore + +: + zest.fail "Up called" + ++: + zest.fail "Down called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to else when up was pressed last frame but not this frame" + test.input.mockController controller, zest.UP + test.input.mockController controller, zest.NO_INPUT + + zest.initRegisters + + utils.preserve + input.ifYDirPressed +, ++, +++ + utils.restore + +: + zest.fail "Up called" + ++: + zest.fail "Down called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to else when down was pressed last frame but not this frame" + test.input.mockController controller, zest.DOWN + test.input.mockController controller, zest.NO_INPUT + + zest.initRegisters + + utils.preserve + input.ifYDirPressed +, ++, +++ + utils.restore + +: + zest.fail "Up called" + ++: + zest.fail "Down called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to up when up was not pressed last frame but is this frame" + test.input.mockController controller, zest.NO_INPUT + test.input.mockController controller, zest.UP + + zest.initRegisters + + utils.preserve + input.ifYDirPressed +, ++, +++ + utils.restore + +: + expect.all.toBeUnclobbered + jp ++++ ; pass + ++: + zest.fail "Down called" + +++: + zest.fail "Else called" + ++++: + + test "jps to down when down was not pressed last frame but is this frame" + test.input.mockController controller, zest.NO_INPUT + test.input.mockController controller, zest.DOWN + + zest.initRegisters + + utils.preserve + input.ifYDirPressed +, ++, +++ + utils.restore + +: + zest.fail "Up called" + ++: + expect.all.toBeUnclobbered + jp ++++ ; pass + +++: + zest.fail "Else called" + ++++: +.endr diff --git a/tests/input/ifYDirReleased.test.asm b/tests/input/ifYDirReleased.test.asm new file mode 100644 index 0000000..5cabbe3 --- /dev/null +++ b/tests/input/ifYDirReleased.test.asm @@ -0,0 +1,120 @@ +.repeat 2 index controller + describe { "input.ifYDirReleased (pad {controller + 1})" } + test "jps to else when neither up nor down were pressed this frame or last frame" + test.input.mockController controller, zest.NO_INPUT, 2 + + zest.initRegisters + + utils.preserve + input.ifYDirReleased +, ++, +++ + utils.restore + +: + zest.fail "Up called" + ++: + zest.fail "Down called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to else when up was pressed last frame and this frame" + test.input.mockController controller, zest.UP, 2 + + zest.initRegisters + + utils.preserve + input.ifYDirReleased +, ++, +++ + utils.restore + +: + zest.fail "Up called" + ++: + zest.fail "Down called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to else when down was pressed last frame and this frame" + test.input.mockController controller, zest.DOWN, 2 + + zest.initRegisters + + utils.preserve + input.ifYDirReleased +, ++, +++ + utils.restore + +: + zest.fail "Up called" + ++: + zest.fail "Down called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to else when up is pressed this frame but not last frame" + test.input.mockController controller, zest.NO_INPUT + test.input.mockController controller, zest.UP + + zest.initRegisters + + utils.preserve + input.ifYDirReleased +, ++, +++ + utils.restore + +: + zest.fail "Up called" + ++: + zest.fail "Down called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to else when down was pressed this frame but not last frame" + test.input.mockController controller, zest.NO_INPUT + test.input.mockController controller, zest.DOWN + + zest.initRegisters + + utils.preserve + input.ifYDirReleased +, ++, +++ + utils.restore + +: + zest.fail "Up called" + ++: + zest.fail "Down called" + +++: + expect.all.toBeUnclobbered + ++++: + + test "jps to up when up was pressed last frame but isn't this frame" + test.input.mockController controller, zest.UP + test.input.mockController controller, zest.NO_INPUT + + zest.initRegisters + + utils.preserve + input.ifYDirReleased +, ++, +++ + utils.restore + +: + expect.all.toBeUnclobbered + jp ++++ ; pass + ++: + zest.fail "Down called" + +++: + zest.fail "Else called" + ++++: + + test "jps to down when down was pressed last frame but isn't this frame" + test.input.mockController controller, zest.DOWN + test.input.mockController controller, zest.NO_INPUT + + zest.initRegisters + + utils.preserve + input.ifYDirReleased +, ++, +++ + utils.restore + +: + zest.fail "Up called" + ++: + expect.all.toBeUnclobbered + jp ++++ ; pass + +++: + zest.fail "Else called" + ++++: +.endr diff --git a/tests/input/init.test.asm b/tests/input/init.test.asm new file mode 100644 index 0000000..f63bca1 --- /dev/null +++ b/tests/input/init.test.asm @@ -0,0 +1,23 @@ +describe "input.init (with port 2 disabled)" + .undefine input.ENABLE_PORT_2 + + test "preserves registers" + zest.initRegisters + + utils.preserve + input.init + utils.restore + + expect.all.toBeUnclobbered + +describe "input.init (with port 2 enabled)" + .define input.ENABLE_PORT_2 + + test "preserves registers" + zest.initRegisters + + utils.preserve + input.init + utils.restore + + expect.all.toBeUnclobbered diff --git a/tests/input/loadDirX.test.asm b/tests/input/loadDirX.test.asm new file mode 100644 index 0000000..794fa4a --- /dev/null +++ b/tests/input/loadDirX.test.asm @@ -0,0 +1,60 @@ +.redefine utils.registers.AUTO_PRESERVE 1 + +.repeat 2 index controller + describe { "input.loadDirX (pad {controller + 1}) with A register" } + test "returns 0 if left or right aren't pressed" + test.input.mockController controller, zest.NO_INPUT + zest.initRegisters + + input.loadDirX "a" + + expect.all.toBeUnclobberedExcept "af" + expect.a.toBe 0 + + test "returns a negative value if left is pressed" + test.input.mockController controller, zest.LEFT + zest.initRegisters + + input.loadDirX "a", 5 + + expect.all.toBeUnclobberedExcept "af" + expect.a.toBe -5 + + test "returns a positive value if right is pressed" + test.input.mockController controller, zest.RIGHT + zest.initRegisters + + input.loadDirX "a", 8 + + expect.all.toBeUnclobberedExcept "af" + expect.a.toBe 8 + + describe { "input.loadDirX (pad {controller + 1}) with other registers" } + test "loads the register with 0 if left or right aren't pressed" + test.input.mockController controller, zest.NO_INPUT + zest.initRegisters + + input.loadDirX "b" + + expect.all.toBeUnclobberedExcept "b" + expect.b.toBe 0 + + test "returns a negative value if left is pressed" + test.input.mockController controller, zest.LEFT + zest.initRegisters + + input.loadDirX "de" + + expect.all.toBeUnclobberedExcept "de" + expect.de.toBe -1 + + test "returns a positive value if right is pressed" + test.input.mockController controller, zest.RIGHT + zest.initRegisters + + input.loadDirX "hl" + + expect.all.toBeUnclobberedExcept "hl" + expect.hl.toBe 1 + +.redefine utils.registers.AUTO_PRESERVE 0 diff --git a/tests/input/loadDirY.test.asm b/tests/input/loadDirY.test.asm new file mode 100644 index 0000000..21d940a --- /dev/null +++ b/tests/input/loadDirY.test.asm @@ -0,0 +1,60 @@ +.redefine utils.registers.AUTO_PRESERVE 1 + +.repeat 2 index controller + describe { "input.loadDirY (pad {controller + 1}) with A register" } + test "returns 0 if up or down aren't pressed" + test.input.mockController controller, zest.NO_INPUT + zest.initRegisters + + input.loadDirY "a" + + expect.all.toBeUnclobberedExcept "af" + expect.a.toBe 0 + + test "returns a negative value if up is pressed" + test.input.mockController controller, zest.UP + zest.initRegisters + + input.loadDirY "a", 5 + + expect.all.toBeUnclobberedExcept "af" + expect.a.toBe -5 + + test "returns a positive value if down is pressed" + test.input.mockController controller, zest.DOWN + zest.initRegisters + + input.loadDirY "a", 8 + + expect.all.toBeUnclobberedExcept "af" + expect.a.toBe 8 + + describe { "input.loadDirY (pad {controller + 1}) with other registers" } + test "loads the register with 0 if up or down aren't pressed" + test.input.mockController controller, zest.NO_INPUT + zest.initRegisters + + input.loadDirY "b" + + expect.all.toBeUnclobberedExcept "b" + expect.b.toBe 0 + + test "returns a negative value if up is pressed" + test.input.mockController controller, zest.UP + zest.initRegisters + + input.loadDirY "de" + + expect.all.toBeUnclobberedExcept "de" + expect.de.toBe -1 + + test "returns a positive value if down is pressed" + test.input.mockController controller, zest.DOWN + zest.initRegisters + + input.loadDirY "hl" + + expect.all.toBeUnclobberedExcept "hl" + expect.hl.toBe 1 + +.redefine utils.registers.AUTO_PRESERVE 0 diff --git a/tests/input/readPort1.test.asm b/tests/input/readPort1.test.asm new file mode 100644 index 0000000..67a87dd --- /dev/null +++ b/tests/input/readPort1.test.asm @@ -0,0 +1,23 @@ +describe "input.readPort1 (with port 2 disabled)" + .undefine input.ENABLE_PORT_2 + + test "preserves registers" + zest.initRegisters + + utils.preserve + input.readPort1 + utils.restore + + expect.all.toBeUnclobbered + +describe "input.readPort1 (with port 2 enabled)" + .define input.ENABLE_PORT_2 + + test "preserves registers" + zest.initRegisters + + utils.preserve + input.readPort1 + utils.restore + + expect.all.toBeUnclobbered diff --git a/tests/input/readPort2.test.asm b/tests/input/readPort2.test.asm new file mode 100644 index 0000000..b92fd17 --- /dev/null +++ b/tests/input/readPort2.test.asm @@ -0,0 +1,9 @@ +describe "input.readPort2" + test "preserves registers" + zest.initRegisters + + utils.preserve + input.readPort2 + utils.restore + + expect.all.toBeUnclobbered diff --git a/tests/interrupts/enable.test.asm b/tests/interrupts/enable.test.asm new file mode 100644 index 0000000..bbb4596 --- /dev/null +++ b/tests/interrupts/enable.test.asm @@ -0,0 +1,9 @@ +describe "interrupts.enable" + test "does not clobber any registers" + zest.initRegisters + + utils.preserve + interrupts.enable + utils.restore + + expect.all.toBeUnclobbered \ No newline at end of file diff --git a/tests/interrupts/init.test.asm b/tests/interrupts/init.test.asm new file mode 100644 index 0000000..4642ab9 --- /dev/null +++ b/tests/interrupts/init.test.asm @@ -0,0 +1,9 @@ +describe "interrupts.init" + test "does not clobber any registers" + zest.initRegisters + + utils.preserve + interrupts.init + utils.restore + + expect.all.toBeUnclobbered \ No newline at end of file diff --git a/tests/interrupts/setLineInterval.test.asm b/tests/interrupts/setLineInterval.test.asm new file mode 100644 index 0000000..e3172f1 --- /dev/null +++ b/tests/interrupts/setLineInterval.test.asm @@ -0,0 +1,9 @@ +describe "interrupts.setLineInterval" + test "does not clobber any registers" + zest.initRegisters + + utils.preserve + interrupts.setLineInterval + utils.restore + + expect.all.toBeUnclobbered \ No newline at end of file diff --git a/tests/interrupts/waitForVBlank.test.asm b/tests/interrupts/waitForVBlank.test.asm new file mode 100644 index 0000000..0d9671f --- /dev/null +++ b/tests/interrupts/waitForVBlank.test.asm @@ -0,0 +1,13 @@ +describe "interrupts.waitForVBlank" + test "does not clobber any registers" + ; Set VBlank flag + ld a, 1 + ld (interrupts.ram.vBlankFlag), a + + zest.initRegisters + + utils.preserve + interrupts.waitForVBlank + utils.restore + + expect.all.toBeUnclobbered \ No newline at end of file diff --git a/tests/lib/zest b/tests/lib/zest new file mode 160000 index 0000000..98738ae --- /dev/null +++ b/tests/lib/zest @@ -0,0 +1 @@ +Subproject commit 98738ae65433269222cd26250aaae1b6b2599ed0 diff --git a/tests/palette/setIndex.test.asm b/tests/palette/setIndex.test.asm new file mode 100644 index 0000000..5670195 --- /dev/null +++ b/tests/palette/setIndex.test.asm @@ -0,0 +1,14 @@ +describe "palette.setIndex" + test "sets C to the port but does not clobber other registers" + zest.initRegisters + + utils.preserve + palette.setIndex 1 + utils.restore + + expect.all.toBeUnclobberedExcept "c" + expect.c.toBe $be ; vdp data port + + test "allows the index to be set from 0 to 31" + palette.setIndex 0 + palette.setIndex 31 diff --git a/tests/palette/writeBytes.test.asm b/tests/palette/writeBytes.test.asm new file mode 100644 index 0000000..5054c07 --- /dev/null +++ b/tests/palette/writeBytes.test.asm @@ -0,0 +1,18 @@ +describe "palette.writeBytes" + jr + + _palette.writeBytes.data: + .db 0 + .db 1 + .db 3 + +: + + test "does not clobber registers" + zest.initRegisters + + utils.preserve + palette.setIndex 0 + palette.writeBytes _palette.writeBytes.data 1 + utils.restore + + expect.all.toBeUnclobberedExcept "c" + expect.c.toBe $be ; vdp data port diff --git a/tests/palette/writeRgb.test.asm b/tests/palette/writeRgb.test.asm new file mode 100644 index 0000000..f27d826 --- /dev/null +++ b/tests/palette/writeRgb.test.asm @@ -0,0 +1,11 @@ +describe "palette.writeRgb" + test "does not clobber the registers" + zest.initRegisters + + utils.preserve + palette.setIndex 0 + palette.writeRgb 100 150 200 + utils.restore + + expect.all.toBeUnclobberedExcept "c" + expect.c.toBe $be ; vdp data port diff --git a/tests/palette/writeSlice.test.asm b/tests/palette/writeSlice.test.asm new file mode 100644 index 0000000..b132891 --- /dev/null +++ b/tests/palette/writeSlice.test.asm @@ -0,0 +1,30 @@ +describe "palette.writeSlice" + jr + + _palette.writeSlice.data: + .db 0 + .db 1 + .db 3 + +: + + test "does not clobber registers" + zest.initRegisters + + utils.preserve + palette.setIndex 0 + palette.writeSlice _palette.writeSlice.data 1 + utils.restore + + expect.all.toBeUnclobberedExcept "c" + expect.c.toBe $be ; vdp data port + +describe "palette.writeSlice with offset" + test "does not clobber registers" + zest.initRegisters + + utils.preserve + palette.setIndex 0 + palette.writeSlice _palette.writeSlice.data 1 1 + utils.restore + + expect.all.toBeUnclobberedExcept "c" + expect.c.toBe $be ; vdp data port diff --git a/tests/patterns/setIndex.test.asm b/tests/patterns/setIndex.test.asm new file mode 100644 index 0000000..b70755a --- /dev/null +++ b/tests/patterns/setIndex.test.asm @@ -0,0 +1,14 @@ +describe "patterns.setIndex" + test "sets C to the VDP data port but does not clobber other registers" + zest.initRegisters + + utils.preserve + patterns.setIndex 1 + utils.restore + + expect.all.toBeUnclobberedExcept "c" + expect.c.toBe $be ; vdp data port + + test "allows the index to be set from 0 to 511" + patterns.setIndex 0 + patterns.setIndex 511 \ No newline at end of file diff --git a/tests/patterns/writeBytes.test.asm b/tests/patterns/writeBytes.test.asm new file mode 100644 index 0000000..374242d --- /dev/null +++ b/tests/patterns/writeBytes.test.asm @@ -0,0 +1,18 @@ +describe "patterns.writeBytes" + jr + + _patterns.writeBytes.data: + .db 0 + .db 1 + .db 3 + +: + + test "does not clobber registers" + zest.initRegisters + + utils.preserve + patterns.setIndex 0 + patterns.writeBytes _patterns.writeBytes.data 1 + utils.restore + + expect.all.toBeUnclobberedExcept "c" + expect.c.toBe $be ; vdp data port diff --git a/tests/patterns/writeSlice.test.asm b/tests/patterns/writeSlice.test.asm new file mode 100644 index 0000000..32be034 --- /dev/null +++ b/tests/patterns/writeSlice.test.asm @@ -0,0 +1,30 @@ +describe "patterns.writeSlice" + jr + + _patterns.writeSlice.data: + .db 0 + .db 1 + .db 3 + +: + + test "does not clobber registers" + zest.initRegisters + + utils.preserve + patterns.setIndex 0 + patterns.writeSlice _patterns.writeSlice.data 1 + utils.restore + + expect.all.toBeUnclobberedExcept "c" + expect.c.toBe $be ; vdp data port + +describe "patterns.writeSlice with offset" + test "does not clobber registers" + zest.initRegisters + + utils.preserve + patterns.setIndex 0 + patterns.writeSlice _patterns.writeSlice.data 1 1 + utils.restore + + expect.all.toBeUnclobberedExcept "c" + expect.c.toBe $be ; vdp data port diff --git a/tests/pause/callIfPaused.test.asm b/tests/pause/callIfPaused.test.asm new file mode 100644 index 0000000..876f293 --- /dev/null +++ b/tests/pause/callIfPaused.test.asm @@ -0,0 +1,26 @@ +describe "pause.callIfPaused" + test "calls the given routine if the pause flag is set" + ; Set pause flag + ld a, 1 + ld (pause.ram.pauseFlag), a + + jr + + -: + jp ++ ; routine called; jp to pass + +: + + pause.callIfPaused - + zest.fail "Did not call" + ++: + + test "does not call the routine if the pause flag is reset" + ; Reset pause flag + xor a + ld (pause.ram.pauseFlag), a + + jr + + -: + zest.fail "Unexpected call" + +: + + pause.callIfPaused - diff --git a/tests/pause/init.test.asm b/tests/pause/init.test.asm new file mode 100644 index 0000000..1170d4e --- /dev/null +++ b/tests/pause/init.test.asm @@ -0,0 +1,9 @@ +describe "pause.init" + test "does not clobber any registers" + zest.initRegisters + + utils.preserve + pause.init + utils.restore + + expect.all.toBeUnclobbered \ No newline at end of file diff --git a/tests/pause/jpIfPaused.test.asm b/tests/pause/jpIfPaused.test.asm new file mode 100644 index 0000000..f8ae36f --- /dev/null +++ b/tests/pause/jpIfPaused.test.asm @@ -0,0 +1,22 @@ +describe "pause.jpIfPaused" + test "jumps if the pause flag is set" + ; Set pause flag + ld a, 1 + ld (pause.ram.pauseFlag), a + + pause.jpIfPaused + + zest.fail "Did not jump" + +: + + test "does not jump if the pause flag is reset" + ; Reset pause flag + xor a + ld (pause.ram.pauseFlag), a + + pause.jpIfPaused + + jp ++ ; did not jump; jump to pass + + +: + zest.fail "Unexpected jump" + + ++: ; pass diff --git a/tests/pause/waitIfPaused.test.asm b/tests/pause/waitIfPaused.test.asm new file mode 100644 index 0000000..8f07c8a --- /dev/null +++ b/tests/pause/waitIfPaused.test.asm @@ -0,0 +1,13 @@ +describe "pause.waitIfPaused" + test "does not clobber any registers" + ; Reset pause flag + xor a + ld (pause.ram.pauseFlag), a + + zest.initRegisters + + utils.preserve + pause.waitIfPaused + utils.restore + + expect.all.toBeUnclobbered \ No newline at end of file diff --git a/tests/scroll/metatiles/_helpers.asm b/tests/scroll/metatiles/_helpers.asm new file mode 100644 index 0000000..01cb072 --- /dev/null +++ b/tests/scroll/metatiles/_helpers.asm @@ -0,0 +1,10 @@ +.section "tests.scroll.metatiles.init" free + tests.scroll.metatiles.init: + ld a, 64 ; the map's width in metatiles + ld b, 0 ; the column offset in metatiles + ld c, 0 ; the row offset in metatiles + ld d, 64 ; the map's height in metatiles + scroll.metatiles.init + + ret +.ends diff --git a/tests/scroll/metatiles/init.test.asm b/tests/scroll/metatiles/init.test.asm new file mode 100644 index 0000000..25f923b --- /dev/null +++ b/tests/scroll/metatiles/init.test.asm @@ -0,0 +1,18 @@ +describe "scroll/metatiles init" + +test "does not clobber registers" + zest.initRegisters + + utils.preserve + ld a, 64 ; the map's width in metatiles + ld b, 0 ; the column offset in metatiles + ld c, 0 ; the row offset in metatiles + ld d, 64 ; the map's height in metatiles + scroll.metatiles.init + utils.restore + + expect.all.toBeUnclobberedExcept "a", "b", "c", "d" + expect.a.toBe 64 + expect.b.toBe 0 + expect.c.toBe 0 + expect.d.toBe 64 diff --git a/tests/scroll/metatiles/render.test.asm b/tests/scroll/metatiles/render.test.asm new file mode 100644 index 0000000..06e0159 --- /dev/null +++ b/tests/scroll/metatiles/render.test.asm @@ -0,0 +1,52 @@ +describe "scroll.metatiles.render does not clobber registers when:" + +test "no row or col scroll needed" + ; Setup + call tests.scroll.metatiles.init + xor a + scroll.metatiles.adjustXPixels + xor a + scroll.metatiles.adjustYPixels + scroll.metatiles.update + + zest.initRegisters + + utils.preserve + scroll.metatiles.render + utils.restore + + expect.all.toBeUnclobbered + +test "row scroll needed" + ; Setup + call tests.scroll.metatiles.init + xor a + scroll.metatiles.adjustXPixels + ld a, 8 + scroll.metatiles.adjustYPixels + scroll.metatiles.update + + zest.initRegisters + + utils.preserve + scroll.metatiles.render + utils.restore + + expect.all.toBeUnclobbered + +test "col scroll needed" + ; Setup + call tests.scroll.metatiles.init + ld a, 8 + scroll.metatiles.adjustXPixels + xor a + scroll.metatiles.adjustYPixels + scroll.metatiles.update + + zest.initRegisters + + utils.preserve + scroll.metatiles.render + utils.restore + + expect.all.toBeUnclobbered diff --git a/tests/scroll/metatiles/update.test.asm b/tests/scroll/metatiles/update.test.asm new file mode 100644 index 0000000..72f0772 --- /dev/null +++ b/tests/scroll/metatiles/update.test.asm @@ -0,0 +1,49 @@ +describe "scroll.metatiles.update does not clobber registers when:" + +test "no row or col scroll needed" + ; Setup + call tests.scroll.metatiles.init + xor a + scroll.metatiles.adjustXPixels + xor a + scroll.metatiles.adjustYPixels + + zest.initRegisters + + utils.preserve + scroll.metatiles.update + utils.restore + + expect.all.toBeUnclobbered + +test "row scroll needed" + ; Setup + call tests.scroll.metatiles.init + xor a + scroll.metatiles.adjustXPixels + ld a, 8 + scroll.metatiles.adjustYPixels + + zest.initRegisters + + utils.preserve + scroll.metatiles.update + utils.restore + + expect.all.toBeUnclobbered + +test "col scroll needed" + ; Setup + call tests.scroll.metatiles.init + ld a, 8 + scroll.metatiles.adjustXPixels + xor a + scroll.metatiles.adjustYPixels + + zest.initRegisters + + utils.preserve + scroll.metatiles.update + utils.restore + + expect.all.toBeUnclobbered diff --git a/tests/scroll/tiles/init.test.asm b/tests/scroll/tiles/init.test.asm new file mode 100644 index 0000000..18dcf71 --- /dev/null +++ b/tests/scroll/tiles/init.test.asm @@ -0,0 +1,29 @@ +describe "scroll/tiles init" + +test "does not clobber registers when no macro args given" + zest.initRegisters + + utils.preserve + ld a, 0 ; mapCols + ld b, 0 ; mapRows + ld d, 0 ; colOffset + ld e, 0 ; rowOffset + ld hl, 0 ; topLeftPointer + scroll.tiles.init + utils.restore + + expect.all.toBeUnclobberedExcept "a", "b", "d", "e", "h", "l" + expect.a.toBe 0 + expect.b.toBe 0 + expect.de.toBe 0 + expect.hl.toBe 0 + + +test "does not clobber registers when macro args are given" + zest.initRegisters + + utils.preserve + scroll.tiles.init 0 64 64 0 0 + utils.restore + + expect.all.toBeUnclobbered diff --git a/tests/scroll/tiles/render.test.asm b/tests/scroll/tiles/render.test.asm new file mode 100644 index 0000000..1dab080 --- /dev/null +++ b/tests/scroll/tiles/render.test.asm @@ -0,0 +1,52 @@ +describe "scroll.tiles.render does not clobber registers when:" + +test "no row or col scroll needed" + ; Setup + scroll.tiles.init 0 64 64 0 0 + xor a + scroll.tiles.adjustXPixels + xor a + scroll.tiles.adjustYPixels + scroll.tiles.update + + zest.initRegisters + + utils.preserve + scroll.tiles.render + utils.restore + + expect.all.toBeUnclobbered + +test "row scroll needed" + ; Setup + scroll.tiles.init 0 64 64 0 0 + xor a + scroll.tiles.adjustXPixels + ld a, 8 + scroll.tiles.adjustYPixels + scroll.tiles.update + + zest.initRegisters + + utils.preserve + scroll.tiles.render + utils.restore + + expect.all.toBeUnclobbered + +test "col scroll needed" + ; Setup + scroll.tiles.init 0 64 64 0 0 + ld a, 8 + scroll.tiles.adjustXPixels + xor a + scroll.tiles.adjustYPixels + scroll.tiles.update + + zest.initRegisters + + utils.preserve + scroll.tiles.render + utils.restore + + expect.all.toBeUnclobbered diff --git a/tests/scroll/tiles/update.test.asm b/tests/scroll/tiles/update.test.asm new file mode 100644 index 0000000..59df222 --- /dev/null +++ b/tests/scroll/tiles/update.test.asm @@ -0,0 +1,49 @@ +describe "scroll.tiles.update does not clobber registers when:" + +test "no row or col scroll needed" + ; Setup + scroll.tiles.init 0 64 64 0 0 + xor a + scroll.tiles.adjustXPixels + xor a + scroll.tiles.adjustYPixels + + zest.initRegisters + + utils.preserve + scroll.tiles.update + utils.restore + + expect.all.toBeUnclobbered + +test "row scroll needed" + ; Setup + scroll.tiles.init 0 64 64 0 0 + xor a + scroll.tiles.adjustXPixels + ld a, 8 + scroll.tiles.adjustYPixels + + zest.initRegisters + + utils.preserve + scroll.tiles.update + utils.restore + + expect.all.toBeUnclobbered + +test "col scroll needed" + ; Setup + scroll.tiles.init 0 64 64 0 0 + ld a, 8 + scroll.tiles.adjustXPixels + xor a + scroll.tiles.adjustYPixels + + zest.initRegisters + + utils.preserve + scroll.tiles.update + utils.restore + + expect.all.toBeUnclobbered diff --git a/tests/sprites/add.test.asm b/tests/sprites/add.test.asm new file mode 100644 index 0000000..f62eeb3 --- /dev/null +++ b/tests/sprites/add.test.asm @@ -0,0 +1,22 @@ +describe "sprites.add" + sprites.init + + test "should not clobber any registers" + zest.initRegisters + + utils.preserve + sprites.add + utils.restore + + expect.all.toBeUnclobbered + + test "when in a batch should not clobber any registers apart from DE" + zest.initRegisters + + utils.preserve + sprites.startBatch + sprites.add + sprites.endBatch + utils.restore + + expect.all.toBeUnclobberedExcept "de" diff --git a/tests/sprites/addGroup.test.asm b/tests/sprites/addGroup.test.asm new file mode 100644 index 0000000..d7b5d19 --- /dev/null +++ b/tests/sprites/addGroup.test.asm @@ -0,0 +1,36 @@ +describe "sprites.addGroup" + sprites.init + + jr + + testSpriteGroup: + sprites.startGroup + sprites.sprite 1, 0, 0 + sprites.sprite 2, 8, 0 + sprites.endGroup + +: + + test "should not clobber any registers" + sprites.reset + + zest.initRegisters + + utils.preserve + ld hl, testSpriteGroup + sprites.addGroup + utils.restore + + expect.all.toBeUnclobberedExcept "hl" + expect.hl.toBe testSpriteGroup + + test "when in a batch should not clobber any registers apart from DE" + zest.initRegisters + + utils.preserve + sprites.startBatch + ld hl, testSpriteGroup + sprites.addGroup + sprites.endBatch + utils.restore + + expect.all.toBeUnclobberedExcept "hl" "de" + expect.hl.toBe testSpriteGroup diff --git a/tests/sprites/copyToVram.test.asm b/tests/sprites/copyToVram.test.asm new file mode 100644 index 0000000..1f49b5c --- /dev/null +++ b/tests/sprites/copyToVram.test.asm @@ -0,0 +1,24 @@ +describe "sprites.copyToVram" + sprites.init + + test "should not clobber any registers when there are no sprites" + sprites.reset + zest.initRegisters + + utils.preserve + sprites.copyToVram + utils.restore + + expect.all.toBeUnclobbered + + test "should not clobber any registers when there are sprites" + sprites.reset + sprites.add + + zest.initRegisters + + utils.preserve + sprites.copyToVram + utils.restore + + expect.all.toBeUnclobbered diff --git a/tests/sprites/init.test.asm b/tests/sprites/init.test.asm new file mode 100644 index 0000000..89ff9a5 --- /dev/null +++ b/tests/sprites/init.test.asm @@ -0,0 +1,18 @@ +describe "sprites.init" + test "sets the nextIndex to $40" + ld a, $ff + ld (sprites.ram.buffer.nextIndex), a + + sprites.init + + ld a, (sprites.ram.buffer.nextIndex) + expect.a.toBe $40 + + test "does not clobber any registers" + zest.initRegisters + + utils.preserve + sprites.init + utils.restore + + expect.all.toBeUnclobbered \ No newline at end of file diff --git a/tests/suite.asm b/tests/suite.asm index 153ddfe..f08b31e 100644 --- a/tests/suite.asm +++ b/tests/suite.asm @@ -1,11 +1,16 @@ ; Include Zest library -.incdir "./zest" ; point to zest directory +.define zest.SUITE_BANKS 5 +.incdir "./lib/zest" ; point to zest directory .include "zest.asm" ; include the zest.asm library ; Import smslib (via smslib-zest helper) -.incdir ".." +.incdir "../src" .define input.ENABLE_PORT_2 - .include "tests/smslib-zest.asm" + .include "smslib-zest.asm" + + .define scroll.metatiles.ENFORCE_BOUNDS + .include "scroll/metatiles.asm" + .include "scroll/tiles.asm" .incdir "." ; Test helpers @@ -15,6 +20,156 @@ .section "input tests (bank 1)" appendto zest.suite .include "input/if.test.asm" .include "input/ifHeld.test.asm" + + .include "input/ifXDir.test.asm" + .include "input/ifXDirHeld.test.asm" + .include "input/ifXDirPressed.test.asm" + .include "input/ifXDirReleased.test.asm" +.ends + +.section "input tests (bank 2)" appendto zest.suiteBank2 .include "input/ifPressed.test.asm" .include "input/ifReleased.test.asm" + + .include "input/ifYDir.test.asm" + .include "input/ifYDirHeld.test.asm" +.ends + +.section "input tests (bank 3)" appendto zest.suiteBank3 + .include "input/ifYDirPressed.test.asm" + .include "input/ifYDirReleased.test.asm" + + .include "input/loadDirX.test.asm" + .include "input/loadDirY.test.asm" + + .include "input/init.test.asm" + .include "input/readPort1.test.asm" + .include "input/readPort2.test.asm" +.ends + +; Register preservation +.include "utils/registers/_helpers.asm" + +.section "utils/clobbers.asm tests" appendto zest.suiteBank4 + .include "utils/clobbers/clobbers.withBranching.test.asm" + .include "utils/clobbers/clobbers.endBranch.test.asm" + + .include "utils/clobbers/clobbers.end.jpc.test.asm" + .include "utils/clobbers/clobbers.end.jpnc.test.asm" + + .include "utils/clobbers/clobbers.end.jrc.test.asm" + .include "utils/clobbers/clobbers.end.jrnc.test.asm" + + .include "utils/clobbers/clobbers.end.jppo.test.asm" + .include "utils/clobbers/clobbers.end.jppe.test.asm" + + .include "utils/clobbers/clobbers.end.jpp.test.asm" + .include "utils/clobbers/clobbers.end.jpm.test.asm" + + .include "utils/clobbers/clobbers.end.jpz.test.asm" + .include "utils/clobbers/clobbers.end.jpnz.test.asm" + + .include "utils/clobbers/clobbers.end.jrz.test.asm" + .include "utils/clobbers/clobbers.end.jrnz.test.asm" + + .include "utils/clobbers/clobbers.end.retz.test.asm" + .include "utils/clobbers/clobbers.end.retnz.test.asm" + + .include "utils/clobbers/clobbers.end.retc.test.asm" + .include "utils/clobbers/clobbers.end.retnc.test.asm" + + .include "utils/clobbers/clobbers.end.retpe.test.asm" + .include "utils/clobbers/clobbers.end.retpo.test.asm" + + .include "utils/clobbers/clobbers.end.retm.test.asm" + .include "utils/clobbers/clobbers.end.retp.test.asm" +.ends + +.section "utils/registers.asm tests" appendto zest.suiteBank5 + .include "utils/registers/autoPreserve.test.asm" + .include "utils/registers/iRegister.test.asm" + .include "utils/registers/nestedPreserveScopes.test.asm" + .include "utils/registers/registers.test.asm" +.ends + +; Interrupts +.section "interrupts.asm tests" appendto zest.suiteBank5 + .include "interrupts/enable.test.asm" + .include "interrupts/init.test.asm" + .include "interrupts/setLineInterval.test.asm" + .include "interrupts/waitForVBlank.test.asm" +.ends + +; Palette +.section "palette.asm tests" appendto zest.suiteBank5 + .include "palette/setIndex.test.asm" + .include "palette/writeBytes.test.asm" + .include "palette/writeRgb.test.asm" + .include "palette/writeSlice.test.asm" +.ends + +; Patterns +.section "patterns.asm tests" appendto zest.suiteBank5 + .include "patterns/setIndex.test.asm" + .include "patterns/writeBytes.test.asm" + .include "patterns/writeSlice.test.asm" +.ends + +; Pause +.section "pause.asm tests" appendto zest.suiteBank5 + .include "pause/callIfPaused.test.asm" + .include "pause/init.test.asm" + .include "pause/jpIfPaused.test.asm" + .include "pause/waitIfPaused.test.asm" +.ends + +; scroll/metatiles.asm +.include "scroll/metatiles/_helpers.asm" + +.section "scroll/metatiles tests" appendto zest.suiteBank5 + .include "scroll/metatiles/init.test.asm" + .include "scroll/metatiles/render.test.asm" + .include "scroll/metatiles/update.test.asm" +.ends + +; scroll/tiles.asm +.section "scroll/tiles tests" appendto zest.suiteBank5 + .include "scroll/tiles/init.test.asm" + .include "scroll/tiles/render.test.asm" + .include "scroll/tiles/update.test.asm" +.ends + +; Sprites +.section "sprite.asm tests" appendto zest.suiteBank5 + .include "sprites/add.test.asm" + .include "sprites/addGroup.test.asm" + .include "sprites/copyToVram.test.asm" + .include "sprites/init.test.asm" +.ends + +; Tilemap +.section "tilemap.asm tests" appendto zest.suiteBank5 + .include "tilemap/adjustXPixels.test.asm" + .include "tilemap/adjustYPixels.test.asm" + .include "tilemap/calculateScroll.test.asm" + .include "tilemap/ifColScroll.test.asm" + .include "tilemap/ifColScrollElseRet.test.asm" + .include "tilemap/ifRowScroll.test.asm" + .include "tilemap/ifRowScrollElseRet.test.asm" + .include "tilemap/loadHLWriteAddress.test.asm" + .include "tilemap/reset.test.asm" + .include "tilemap/setColRow.test.asm" + .include "tilemap/setIndex.test.asm" + .include "tilemap/stopDownRowScroll.test.asm" + .include "tilemap/stopLeftColScroll.test.asm" + .include "tilemap/stopRightColScroll.test.asm" + .include "tilemap/stopUpRowScroll.test.asm" + .include "tilemap/writeBytes.test.asm" + .include "tilemap/writeBytesUntil.test.asm" + .include "tilemap/writeRow.test.asm" + .include "tilemap/writeRows.test.asm" + .include "tilemap/writeScrollBuffers.test.asm" + .include "tilemap/writeScrollRegisters.test.asm" + .include "tilemap/writeTile.test.asm" + .include "tilemap/writeTiles.test.asm" .ends diff --git a/tests/tilemap/adjustXPixels.test.asm b/tests/tilemap/adjustXPixels.test.asm new file mode 100644 index 0000000..74caecc --- /dev/null +++ b/tests/tilemap/adjustXPixels.test.asm @@ -0,0 +1,13 @@ +describe "tilemap.adjustXPixels" + tilemap.reset + + test "does not clobber registers" + zest.initRegisters + + utils.preserve + ld a, 1 + tilemap.adjustXPixels + utils.restore + + expect.all.toBeUnclobberedExcept "a" + expect.a.toBe 1 diff --git a/tests/tilemap/adjustYPixels.test.asm b/tests/tilemap/adjustYPixels.test.asm new file mode 100644 index 0000000..9dcf110 --- /dev/null +++ b/tests/tilemap/adjustYPixels.test.asm @@ -0,0 +1,13 @@ +describe "tilemap.adjustYPixels" + tilemap.reset + + test "does not clobber registers" + zest.initRegisters + + utils.preserve + ld a, 1 + tilemap.adjustYPixels + utils.restore + + expect.all.toBeUnclobberedExcept "a" + expect.a.toBe 1 diff --git a/tests/tilemap/calculateScroll.test.asm b/tests/tilemap/calculateScroll.test.asm new file mode 100644 index 0000000..d64190a --- /dev/null +++ b/tests/tilemap/calculateScroll.test.asm @@ -0,0 +1,11 @@ +describe "tilemap.calculateScroll" + tilemap.reset + + test "does not clobber registers" + zest.initRegisters + + utils.preserve + tilemap.calculateScroll + utils.restore + + expect.all.toBeUnclobbered diff --git a/tests/tilemap/ifColScroll.test.asm b/tests/tilemap/ifColScroll.test.asm new file mode 100644 index 0000000..57557b1 --- /dev/null +++ b/tests/tilemap/ifColScroll.test.asm @@ -0,0 +1,99 @@ +describe "tilemap.ifColScroll with 1 arg (else label)" + .redefine utils.registers.AUTO_PRESERVE 1 + + test "jumps to the label if no scroll is needed" + tilemap.reset + + zest.initRegisters + + tilemap.ifColScroll + + zest.fail "ifColScroll was true" + +: + + expect.all.toBeUnclobbered + + test "does not jump to the given label if a left scroll is needed" + tilemap.reset + ld a, -8 + tilemap.adjustXPixels + tilemap.calculateScroll + + zest.initRegisters + + tilemap.ifColScroll + + expect.all.toBeUnclobbered + jr ++ ; pass + +: + + zest.fail "ifColScroll was false" + + ++: + + test "does not jump to the given label if a right scroll is needed" + tilemap.reset + ld a, 8 + tilemap.adjustXPixels + tilemap.calculateScroll + + zest.initRegisters + + tilemap.ifColScroll + + expect.all.toBeUnclobbered + jr ++ ; pass + +: + + zest.fail "ifColScroll was false" + + ++: + +describe "tilemap.ifColScroll with left, right, else args" + test "jumps to the left label when left col scroll needed" + tilemap.reset + ld a, -8 + tilemap.adjustXPixels + tilemap.calculateScroll + + zest.initRegisters + + tilemap.ifColScroll, +, ++, +++ + +: ; Left + expect.all.toBeUnclobbered + jp ++++ ; pass + ++: + zest.fail "Right called" + +++: + zest.fail "Else called" + ++++: ; pass + + test "jumps to the right label when right scroll needed" + tilemap.reset + ld a, 8 + tilemap.adjustXPixels + tilemap.calculateScroll + + zest.initRegisters + + tilemap.ifColScroll, +, ++, +++ + +: + zest.fail "Left called" + ++: ; Right + expect.all.toBeUnclobbered + jp ++++ ; pass + +++: + zest.fail "Else called" + ++++: ; pass + + test "jumps to else label when no col scroll needed" + tilemap.reset + zest.initRegisters + + tilemap.ifColScroll, +, ++, +++ + +: + zest.fail "Left called" + ++: + zest.fail "Right called" + +++: + + expect.all.toBeUnclobbered + +.redefine utils.registers.AUTO_PRESERVE 0 diff --git a/tests/tilemap/ifColScrollElseRet.test.asm b/tests/tilemap/ifColScrollElseRet.test.asm new file mode 100644 index 0000000..7056c56 --- /dev/null +++ b/tests/tilemap/ifColScrollElseRet.test.asm @@ -0,0 +1,65 @@ +describe "tilemap.ifColScrollElseRet" + .redefine utils.registers.AUTO_PRESERVE 1 + + test "jumps to the left label when left col scroll needed" + tilemap.reset + ld a, -8 + tilemap.adjustXPixels + tilemap.calculateScroll + + zest.initRegisters + + call + + zest.fail "Unexpected return" + + +: + + tilemap.ifColScrollElseRet, ++, +++ + ++: + ; Left + expect.all.toBeUnclobbered + jp ++++ ; pass + +++: + zest.fail "Right called" + ++++: ; pass + + test "jumps to the right label when right col scroll needed" + tilemap.reset + ld a, 8 + tilemap.adjustXPixels + tilemap.calculateScroll + + zest.initRegisters + + call + + zest.fail "Unexpected return" + + +: + + tilemap.ifColScrollElseRet, ++, +++ + ++: + ; Left + zest.fail "Left called" + +++: + ; Right + expect.all.toBeUnclobbered + + test "returns when no col scroll needed" + tilemap.reset + zest.initRegisters + + call + + expect.all.toBeUnclobbered + jp +++ ; pass + + +: + tilemap.ifColScrollElseRet, +, ++ + +: + ; Left + zest.fail "Left called" + ++: + ; Right + zest.fail "Right called" + +++: ; pass + +.redefine utils.registers.AUTO_PRESERVE 0 diff --git a/tests/tilemap/ifRowScroll.test.asm b/tests/tilemap/ifRowScroll.test.asm new file mode 100644 index 0000000..a681305 --- /dev/null +++ b/tests/tilemap/ifRowScroll.test.asm @@ -0,0 +1,99 @@ +describe "tilemap.ifRowScroll with 1 arg (else label)" + .redefine utils.registers.AUTO_PRESERVE 1 + + test "jumps to the label if no scroll is needed" + tilemap.reset + + zest.initRegisters + + tilemap.ifRowScroll + + zest.fail "ifRowScroll was true" + +: + + expect.all.toBeUnclobbered + + test "does not jump to the given label if an up row scroll is needed" + tilemap.reset + ld a, -8 + tilemap.adjustYPixels + tilemap.calculateScroll + + zest.initRegisters + + tilemap.ifRowScroll + + expect.all.toBeUnclobbered + jr ++ ; pass + +: + + zest.fail "ifRowScroll was false" + + ++: + + test "does not jump to the given label if a down scroll is needed" + tilemap.reset + ld a, 8 + tilemap.adjustYPixels + tilemap.calculateScroll + + zest.initRegisters + + tilemap.ifRowScroll + + expect.all.toBeUnclobbered + jr ++ ; pass + +: + + zest.fail "ifRowScroll was false" + + ++: + +describe "tilemap.ifRowScroll with up, down, else args" + test "jumps to the up label when up col scroll needed" + tilemap.reset + ld a, -8 + tilemap.adjustYPixels + tilemap.calculateScroll + + zest.initRegisters + + tilemap.ifRowScroll, +, ++, +++ + +: ; up + expect.all.toBeUnclobbered + jp ++++ ; pass + ++: + zest.fail "Down called" + +++: + zest.fail "Else called" + ++++: ; pass + + test "jumps to the down label when down scroll needed" + tilemap.reset + ld a, 8 + tilemap.adjustYPixels + tilemap.calculateScroll + + zest.initRegisters + + tilemap.ifRowScroll, +, ++, +++ + +: + zest.fail "Up called" + ++: ; down + expect.all.toBeUnclobbered + jp ++++ ; pass + +++: + zest.fail "Else called" + ++++: ; pass + + test "jumps to else label when no col scroll needed" + tilemap.reset + zest.initRegisters + + tilemap.ifRowScroll, +, ++, +++ + +: + zest.fail "Up called" + ++: + zest.fail "Down called" + +++: + + expect.all.toBeUnclobbered + +.redefine utils.registers.AUTO_PRESERVE 0 diff --git a/tests/tilemap/ifRowScrollElseRet.test.asm b/tests/tilemap/ifRowScrollElseRet.test.asm new file mode 100644 index 0000000..8271937 --- /dev/null +++ b/tests/tilemap/ifRowScrollElseRet.test.asm @@ -0,0 +1,64 @@ +describe "tilemap.ifRowScrollElseRet" + .redefine utils.registers.AUTO_PRESERVE 1 + + test "jumps to the up label when up row scroll needed" + tilemap.reset + ld a, -8 + tilemap.adjustYPixels + tilemap.calculateScroll + + zest.initRegisters + + call + + zest.fail "Unexpected return" + + +: + + tilemap.ifRowScrollElseRet, ++, +++ + ++: + ; Up + expect.all.toBeUnclobbered + jp ++++ ; pass + +++: + zest.fail "Down called" + ++++: + + test "jumps to the down label when down row scroll needed" + tilemap.reset + ld a, 8 + tilemap.adjustYPixels + tilemap.calculateScroll + + zest.initRegisters + + call + + zest.fail "Unexpected return" + + +: + + tilemap.ifRowScrollElseRet, ++, +++ + ++: + zest.fail "Up called" + +++: + ; Down + expect.all.toBeUnclobbered + + test "returns when no row scroll needed" + tilemap.reset + zest.initRegisters + + call + + expect.all.toBeUnclobbered + jp +++ ; pass + + +: + tilemap.ifRowScrollElseRet, +, ++ + +: + ; Up + zest.fail "Up called" + ++: + ; Down + zest.fail "Down called" + +++: + +.redefine utils.registers.AUTO_PRESERVE 0 diff --git a/tests/tilemap/loadHLWriteAddress.test.asm b/tests/tilemap/loadHLWriteAddress.test.asm new file mode 100644 index 0000000..423c1c7 --- /dev/null +++ b/tests/tilemap/loadHLWriteAddress.test.asm @@ -0,0 +1,16 @@ +describe "tilemap.loadHLWriteAddress" + test "returns HL but doesn't clobber other registers" + zest.initRegisters + + utils.preserve + tilemap.loadHLWriteAddress + utils.restore + + expect.all.toBeUnclobberedExcept "hl" + + test "sets the high bits to %01 for the VDP write command" + ld hl, 12 + (27 * 32) ; col 12, row 27 + tilemap.loadHLWriteAddress + ld a, h + and %11000000 + expect.a.toBe %01000000 diff --git a/tests/tilemap/reset.test.asm b/tests/tilemap/reset.test.asm new file mode 100644 index 0000000..74964ad --- /dev/null +++ b/tests/tilemap/reset.test.asm @@ -0,0 +1,9 @@ +describe "tilemap.reset" + test "does not clobber registers" + zest.initRegisters + + utils.preserve + tilemap.reset + utils.restore + + expect.all.toBeUnclobbered diff --git a/tests/tilemap/setColRow.test.asm b/tests/tilemap/setColRow.test.asm new file mode 100644 index 0000000..f059f60 --- /dev/null +++ b/tests/tilemap/setColRow.test.asm @@ -0,0 +1,18 @@ +describe "tilemap.setColRow" + test "sets C to the VDP data port but does not clobber other registers" + zest.initRegisters + + utils.preserve + tilemap.setColRow 0 0 + utils.restore + + expect.all.toBeUnclobberedExcept "c" + expect.c.toBe $be ; vdp data port + + test "allows the column to be set from 0-31" + tilemap.setColRow 0 0 + tilemap.setColRow 31 0 + + test "allows the row to be set from 0-27" + tilemap.setColRow 0 0 + tilemap.setColRow 0 27 \ No newline at end of file diff --git a/tests/tilemap/setIndex.test.asm b/tests/tilemap/setIndex.test.asm new file mode 100644 index 0000000..f47da9c --- /dev/null +++ b/tests/tilemap/setIndex.test.asm @@ -0,0 +1,14 @@ +describe "tilemap.setIndex" + test "sets C to the VDP data port but does not clobber other registers" + zest.initRegisters + + utils.preserve + tilemap.setIndex 1 + utils.restore + + expect.all.toBeUnclobberedExcept "c" + expect.c.toBe $be ; vdp data port + + test "allows the index to be set from 0 to 895" + tilemap.setIndex 0 + tilemap.setIndex 895 \ No newline at end of file diff --git a/tests/tilemap/stopDownRowScroll.test.asm b/tests/tilemap/stopDownRowScroll.test.asm new file mode 100644 index 0000000..fafc2c2 --- /dev/null +++ b/tests/tilemap/stopDownRowScroll.test.asm @@ -0,0 +1,11 @@ +describe "tilemap.stopDownRowScroll" + tilemap.reset + + test "does not clobber registers" + zest.initRegisters + + utils.preserve + tilemap.stopDownRowScroll + utils.restore + + expect.all.toBeUnclobbered diff --git a/tests/tilemap/stopLeftColScroll.test.asm b/tests/tilemap/stopLeftColScroll.test.asm new file mode 100644 index 0000000..bfa80fb --- /dev/null +++ b/tests/tilemap/stopLeftColScroll.test.asm @@ -0,0 +1,11 @@ +describe "tilemap.stopLeftColScroll" + tilemap.reset + + test "does not clobber registers" + zest.initRegisters + + utils.preserve + tilemap.stopLeftColScroll + utils.restore + + expect.all.toBeUnclobbered diff --git a/tests/tilemap/stopRightColScroll.test.asm b/tests/tilemap/stopRightColScroll.test.asm new file mode 100644 index 0000000..d7e801e --- /dev/null +++ b/tests/tilemap/stopRightColScroll.test.asm @@ -0,0 +1,11 @@ +describe "tilemap.stopRightColScroll" + tilemap.reset + + test "does not clobber registers" + zest.initRegisters + + utils.preserve + tilemap.stopRightColScroll + utils.restore + + expect.all.toBeUnclobbered diff --git a/tests/tilemap/stopUpRowScroll.test.asm b/tests/tilemap/stopUpRowScroll.test.asm new file mode 100644 index 0000000..8310273 --- /dev/null +++ b/tests/tilemap/stopUpRowScroll.test.asm @@ -0,0 +1,11 @@ +describe "tilemap.stopUpRowScroll" + tilemap.reset + + test "does not clobber registers" + zest.initRegisters + + utils.preserve + tilemap.stopUpRowScroll + utils.restore + + expect.all.toBeUnclobbered diff --git a/tests/tilemap/writeBytes.test.asm b/tests/tilemap/writeBytes.test.asm new file mode 100644 index 0000000..cfaf8d0 --- /dev/null +++ b/tests/tilemap/writeBytes.test.asm @@ -0,0 +1,17 @@ +describe "tilemap.writeBytes" + jr + + _writeBytesData: + .db 0 + .dw 1 + +: + + test "does not clobber registers" + zest.initRegisters + + utils.preserve + tilemap.setColRow 0 0 + tilemap.writeBytes _writeBytesData 2 0 + utils.restore + + expect.all.toBeUnclobberedExcept "c" + expect.c.toBe $be diff --git a/tests/tilemap/writeBytesUntil.test.asm b/tests/tilemap/writeBytesUntil.test.asm new file mode 100644 index 0000000..81fd65f --- /dev/null +++ b/tests/tilemap/writeBytesUntil.test.asm @@ -0,0 +1,18 @@ +describe "tilemap.writeBytesUntil" + jr + + _writeBytesUntilData: + .db 0 + .dw 1 + .db $ff + +: + + test "does not clobber registers" + zest.initRegisters + + utils.preserve + tilemap.setColRow 0 0 + tilemap.writeBytesUntil $ff _writeBytesUntilData tilemap.FLIP_XY + utils.restore + + expect.all.toBeUnclobberedExcept "c" + expect.c.toBe $be ; vdp data port diff --git a/tests/tilemap/writeRow.test.asm b/tests/tilemap/writeRow.test.asm new file mode 100644 index 0000000..23dd505 --- /dev/null +++ b/tests/tilemap/writeRow.test.asm @@ -0,0 +1,13 @@ +describe "tilemap.writeRow" + test "does not clobber registers" + zest.initRegisters + + utils.preserve + tilemap.setColRow 0, 0 + ld hl, 0 + tilemap.writeRow + utils.restore + + expect.all.toBeUnclobberedExcept "c" "hl" + expect.c.toBe $be ; vdp data port + expect.hl.toBe 0 diff --git a/tests/tilemap/writeRows.test.asm b/tests/tilemap/writeRows.test.asm new file mode 100644 index 0000000..ce30b1a --- /dev/null +++ b/tests/tilemap/writeRows.test.asm @@ -0,0 +1,17 @@ +describe "tilemap.writeRows" + test "does not clobber registers" + zest.initRegisters + + utils.preserve + tilemap.setColRow 0, 0 + ld d, 1 + ld e, 2 + ld hl, 0 + tilemap.writeRows + utils.restore + + expect.all.toBeUnclobberedExcept "c" "de", "hl" + expect.c.toBe $be ; vdp data port + expect.d.toBe 1 + expect.e.toBe 2 + expect.hl.toBe 0 diff --git a/tests/tilemap/writeScrollBuffers.test.asm b/tests/tilemap/writeScrollBuffers.test.asm new file mode 100644 index 0000000..b0247f8 --- /dev/null +++ b/tests/tilemap/writeScrollBuffers.test.asm @@ -0,0 +1,85 @@ +describe "tilemap.writeScrollBuffers" + tilemap.reset + + ; Dummy tile data + jp + + -: + .dsb 31 * 2, $00 + +: + + ; Initialise column buffer + tilemap.loadDEColBuffer + tilemap.loadBCColBytes + ld hl, - ; point to data + ldir ; write data + + ; Initialise row buffer + tilemap.loadDERowBuffer + tilemap.loadBCRowBytes + ld hl, - ; point to data + ldir ; write data + + it "preserves the registers when no scroll needed" + zest.initRegisters + + utils.preserve + tilemap.writeScrollBuffers + utils.restore + + expect.all.toBeUnclobbered + + it "preserves the registers when left scroll needed" + tilemap.reset + ld a, -8 + tilemap.adjustXPixels + tilemap.calculateScroll + + zest.initRegisters + + utils.preserve + tilemap.writeScrollBuffers + utils.restore + + expect.all.toBeUnclobbered + + it "preserves the registers when right scroll needed" + tilemap.reset + ld a, 8 + tilemap.adjustXPixels + tilemap.calculateScroll + + zest.initRegisters + + utils.preserve + tilemap.writeScrollBuffers + utils.restore + + expect.all.toBeUnclobbered + + it "preserves the registers when up scroll needed" + tilemap.reset + ld a, -8 + tilemap.adjustYPixels + tilemap.calculateScroll + + zest.initRegisters + + utils.preserve + tilemap.writeScrollBuffers + utils.restore + + expect.all.toBeUnclobbered + + it "preserves the registers when down scroll needed" + tilemap.reset + ld a, 8 + tilemap.adjustYPixels + tilemap.calculateScroll + + zest.initRegisters + + utils.preserve + tilemap.writeScrollBuffers + utils.restore + + expect.all.toBeUnclobbered diff --git a/tests/tilemap/writeScrollRegisters.test.asm b/tests/tilemap/writeScrollRegisters.test.asm new file mode 100644 index 0000000..fa65657 --- /dev/null +++ b/tests/tilemap/writeScrollRegisters.test.asm @@ -0,0 +1,11 @@ +describe "tilemap.writeScrollRegisters" + tilemap.reset + + it "preserves the registers" + zest.initRegisters + + utils.preserve + tilemap.writeScrollRegisters + utils.restore + + expect.all.toBeUnclobbered \ No newline at end of file diff --git a/tests/tilemap/writeTile.test.asm b/tests/tilemap/writeTile.test.asm new file mode 100644 index 0000000..dd6358d --- /dev/null +++ b/tests/tilemap/writeTile.test.asm @@ -0,0 +1,17 @@ +describe "tilemap.writeTile" + test "does not clobber registers" + zest.initRegisters + + utils.preserve + tilemap.setColRow 0, 0 + tilemap.writeTile 1 + tilemap.writeTile 2 tilemap.FLIP_X + utils.restore + + expect.all.toBeUnclobberedExcept "c" + expect.c.toBe $be ; vdp data port + + test "allows the pattern to be 0 to 511" + tilemap.setColRow 0, 0 + tilemap.writeTile 0 + tilemap.writeTile 511 diff --git a/tests/tilemap/writeTiles.test.asm b/tests/tilemap/writeTiles.test.asm new file mode 100644 index 0000000..e4c787a --- /dev/null +++ b/tests/tilemap/writeTiles.test.asm @@ -0,0 +1,13 @@ +describe "tilemap.writeTiles" + test "does not clobber registers" + zest.initRegisters + + utils.preserve + tilemap.setColRow 0, 0 + ld hl, 0 + tilemap.writeTiles 2 + utils.restore + + expect.all.toBeUnclobberedExcept "c", "hl" + expect.hl.toBe 0 + expect.c.toBe $be ; vdp data port diff --git a/tests/utils/clobbers/clobbers.end.jpc.test.asm b/tests/utils/clobbers/clobbers.end.jpc.test.asm new file mode 100644 index 0000000..8063e3f --- /dev/null +++ b/tests/utils/clobbers/clobbers.end.jpc.test.asm @@ -0,0 +1,76 @@ +describe "utils.clobbers.end.jpc" + test "does not jump or restore when carry is reset" + ; Set all registers to $FF + ld a, $ff + call suite.registers.setAllToA + + utils.preserve "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + ; Set all registers to zero + call suite.registers.setAllToZero + call suite.registers.resetAllFlags ; reset C + + ; This shouldn't jump + utils.clobbers.end.jpc suite.registers.unexpectedJump + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + utils.restore + + test "restores and jumps when carry is set" + zest.initRegisters + + utils.preserve "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + ; Zero registers but set C flag + call suite.registers.setAllToZero + call suite.registers.setAllFlags + + ; Expect this to jump + utils.clobbers.end.jpc + + zest.fail "Did not jump" + utils.clobbers.end + + +: + expect.all.toBeUnclobbered + utils.restore + +describe "utils.clobbers.end.jpc with nothing to restore" + test "does not jump or restore when carry is reset" + zest.initRegisters + + utils.preserve "hl" + ; Doesn't clobber scope doesn't affect HL - nothing to preserve + utils.clobbers.withBranching "af" + ; Set all registers to zero + call suite.registers.setAllToZero + call suite.registers.resetAllFlags + + ; This shouldn't jump + utils.clobbers.end.jpc suite.registers.unexpectedJump + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + utils.restore + + test "jumps but doesn't restore when carry is set" + zest.initRegisters + + utils.preserve "hl" + ; Doesn't clobber scope doesn't affect HL - nothing to preserve + utils.clobbers.withBranching "af" + ; Set all registers to zero, but set Z flag + call suite.registers.setAllToZero + call suite.registers.setAllFlags + + ; Expect this to jump + utils.clobbers.end.jpc + + zest.fail "Did not jump" + utils.clobbers.end + + +: + ; Expect values to still be zero + call suite.registers.expectAllToBeZero + utils.restore diff --git a/tests/utils/clobbers/clobbers.end.jpm.test.asm b/tests/utils/clobbers/clobbers.end.jpm.test.asm new file mode 100644 index 0000000..b498227 --- /dev/null +++ b/tests/utils/clobbers/clobbers.end.jpm.test.asm @@ -0,0 +1,80 @@ +describe "utils.clobbers.end.jpm" + test "does not jump or restore when sign is reset" + ; Set all registers to $FF + ld a, $ff + call suite.registers.setAllToA + + utils.preserve "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + ; Set all registers to zero + call suite.registers.setAllToZero + call suite.registers.resetAllFlags ; reset sign + + ; This shouldn't jump + utils.clobbers.end.jpm suite.registers.unexpectedJump + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + utils.restore + + test "restores and jumps when sign is set" + zest.initRegisters + + utils.preserve "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + ; Zero registers + call suite.registers.setAllToZero + call suite.registers.setAllFlags ; set sign flag + + ; Expect this to jump + utils.clobbers.end.jpm + + zest.fail "Did not jump" + utils.clobbers.end + + +: + expect.all.toBeUnclobbered + expect.stack.size.toBe 0 + utils.restore + +describe "utils.clobbers.end.jpm with nothing to restore" + test "does not jump or restore when sign is reset" + zest.initRegisters + + utils.preserve "hl" + ; Doesn't clobber scope doesn't affect HL - nothing to preserve + utils.clobbers.withBranching "af" + ; Set all registers to zero + call suite.registers.setAllToZero + call suite.registers.resetAllFlags ; set sign + + ; This shouldn't jump + utils.clobbers.end.jpm suite.registers.unexpectedJump + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + + expect.stack.size.toBe 0 + utils.restore + + test "jumps but doesn't restore when sign is set" + zest.initRegisters + + utils.preserve "hl" + ; Doesn't clobber scope doesn't affect HL - nothing to preserve + utils.clobbers.withBranching "af" + ; Set all registers to zero + call suite.registers.setAllToZero + call suite.registers.setAllFlags + + ; Expect this to jump + utils.clobbers.end.jpm + + zest.fail "Did not jump" + utils.clobbers.end + + +: + ; Expect values to still be zero + call suite.registers.expectAllToBeZero + expect.stack.size.toBe 0 + utils.restore diff --git a/tests/utils/clobbers/clobbers.end.jpnc.test.asm b/tests/utils/clobbers/clobbers.end.jpnc.test.asm new file mode 100644 index 0000000..0d6fbd7 --- /dev/null +++ b/tests/utils/clobbers/clobbers.end.jpnc.test.asm @@ -0,0 +1,80 @@ +describe "utils.clobbers.end.jpnc" + test "does not jump or restore when carry is set" + ; Set all registers to $FF + ld a, $ff + call suite.registers.setAllToA + + utils.preserve "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + ; Set all registers to zero + call suite.registers.setAllToZero + scf ; set carry + + ; This shouldn't jump + utils.clobbers.end.jpnc suite.registers.unexpectedJump + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + utils.restore + + test "restores and jumps when carry is reset" + zest.initRegisters + + utils.preserve "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + ; Zero registers + call suite.registers.setAllToZero + call suite.registers.resetAllFlags ; reset carry flag + + ; Expect this to jump + utils.clobbers.end.jpnc + + zest.fail "Did not jump" + utils.clobbers.end + + +: + expect.all.toBeUnclobbered + expect.stack.size.toBe 0 + utils.restore + +describe "utils.clobbers.end.jpnc with nothing to restore" + test "does not jump or restore when carry is set" + zest.initRegisters + + utils.preserve "hl" + ; Doesn't clobber scope doesn't affect HL - nothing to preserve + utils.clobbers.withBranching "af" + ; Set all registers to zero + call suite.registers.setAllToZero + scf ; set carry + + ; This shouldn't jump + utils.clobbers.end.jpnc suite.registers.unexpectedJump + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + + expect.stack.size.toBe 0 + utils.restore + + test "jumps but doesn't restore when carry is reset" + zest.initRegisters + + utils.preserve "hl" + ; Doesn't clobber scope doesn't affect HL - nothing to preserve + utils.clobbers.withBranching "af" + ; Set all registers to zero, but set Z flag + call suite.registers.setAllToZero + call suite.registers.resetAllFlags + + ; Expect this to jump + utils.clobbers.end.jpnc + + zest.fail "Did not jump" + utils.clobbers.end + + +: + ; Expect values to still be zero + call suite.registers.expectAllToBeZero + expect.stack.size.toBe 0 + utils.restore diff --git a/tests/utils/clobbers/clobbers.end.jpnz.test.asm b/tests/utils/clobbers/clobbers.end.jpnz.test.asm new file mode 100644 index 0000000..7be9958 --- /dev/null +++ b/tests/utils/clobbers/clobbers.end.jpnz.test.asm @@ -0,0 +1,76 @@ +describe "utils.clobbers.end.jpnz" + test "when Z - does not jump or restore" + ; Set all registers to $FF + ld a, $ff + call suite.registers.setAllToA + + utils.preserve "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + ; Set all registers to zero + call suite.registers.setAllToZero + call suite.registers.setAllFlags ; set Z + + ; This shouldn't jump + utils.clobbers.end.jpnz suite.registers.unexpectedJump + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + utils.restore + + test "when NZ - restores and jumps" + zest.initRegisters + + utils.preserve "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + ; Zero registers but reset Z flag + call suite.registers.setAllToZero + call suite.registers.resetAllFlags + + ; Expect this to jump + utils.clobbers.end.jpnz + + zest.fail "Did not jump" + utils.clobbers.end + + +: + expect.all.toBeUnclobbered + utils.restore + +describe "utils.clobbers.end.jpnz with nothing to restore" + test "when Z - does not jump or restore" + zest.initRegisters + + utils.preserve "hl" + ; Doesn't clobber scope doesn't affect HL - nothing to preserve + utils.clobbers.withBranching "af" + ; Set all registers to zero + call suite.registers.setAllToZero + call suite.registers.setAllFlags + + ; This shouldn't jump + utils.clobbers.end.jpnz suite.registers.unexpectedJump + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + utils.restore + + test "when NZ - jumps but doesn't restore" + zest.initRegisters + + utils.preserve "hl" + ; Doesn't clobber scope doesn't affect HL - nothing to preserve + utils.clobbers.withBranching "af" + ; Set all registers to zero, but set Z flag + call suite.registers.setAllToZero + call suite.registers.resetAllFlags + + ; Expect this to jump + utils.clobbers.end.jpnz + + zest.fail "Did not jump" + utils.clobbers.end + + +: + ; Expect values to still be zero + call suite.registers.expectAllToBeZero + utils.restore diff --git a/tests/utils/clobbers/clobbers.end.jpp.test.asm b/tests/utils/clobbers/clobbers.end.jpp.test.asm new file mode 100644 index 0000000..a5f7fb7 --- /dev/null +++ b/tests/utils/clobbers/clobbers.end.jpp.test.asm @@ -0,0 +1,76 @@ +describe "utils.clobbers.end.jpp" + test "does not jump or restore when sign is set" + ; Set all registers to $FF + ld a, $ff + call suite.registers.setAllToA + + utils.preserve "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + ; Set all registers to zero + call suite.registers.setAllToZero + call suite.registers.setAllFlags ; set sign + + ; This shouldn't jump + utils.clobbers.end.jpp suite.registers.unexpectedJump + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + utils.restore + + test "restores and jumps when sign is reset" + zest.initRegisters + + utils.preserve "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + ; Zero registers + call suite.registers.setAllToZero + call suite.registers.resetAllFlags ; reset parity + + ; Expect this to jump + utils.clobbers.end.jpp + + zest.fail "Did not jump" + utils.clobbers.end + + +: + expect.all.toBeUnclobbered + utils.restore + +describe "utils.clobbers.end.jpp with nothing to restore" + test "does not jump or restore when sign is set" + zest.initRegisters + + utils.preserve "hl" + ; Doesn't clobber scope doesn't affect HL - nothing to preserve + utils.clobbers.withBranching "af" + ; Set all registers to zero + call suite.registers.setAllToZero + call suite.registers.setAllFlags + + ; This shouldn't jump + utils.clobbers.end.jpp suite.registers.unexpectedJump + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + utils.restore + + test "jumps but doesn't restore when sign is reset" + zest.initRegisters + + utils.preserve "hl" + ; Doesn't clobber scope doesn't affect HL - nothing to preserve + utils.clobbers.withBranching "af" + ; Set all registers to zero + call suite.registers.setAllToZero + call suite.registers.resetAllFlags ; reset parity + + ; Expect this to jump + utils.clobbers.end.jpp + + zest.fail "Did not jump" + utils.clobbers.end + + +: + ; Expect values to still be zero + call suite.registers.expectAllToBeZero + utils.restore diff --git a/tests/utils/clobbers/clobbers.end.jppe.test.asm b/tests/utils/clobbers/clobbers.end.jppe.test.asm new file mode 100644 index 0000000..7b42839 --- /dev/null +++ b/tests/utils/clobbers/clobbers.end.jppe.test.asm @@ -0,0 +1,80 @@ +describe "utils.clobbers.end.jppe" + test "does not jump or restore when parity/overflow is reset" + ; Set all registers to $FF + ld a, $ff + call suite.registers.setAllToA + + utils.preserve "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + ; Set all registers to zero + call suite.registers.setAllToZero + call suite.registers.resetAllFlags ; reset parity/overflow + + ; This shouldn't jump + utils.clobbers.end.jppe suite.registers.unexpectedJump + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + utils.restore + + test "restores and jumps when parity/overflow is set" + zest.initRegisters + + utils.preserve "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + ; Zero registers + call suite.registers.setAllToZero + call suite.registers.setAllFlags ; set parity/overflow flag + + ; Expect this to jump + utils.clobbers.end.jppe + + zest.fail "Did not jump" + utils.clobbers.end + + +: + expect.all.toBeUnclobbered + expect.stack.size.toBe 0 + utils.restore + +describe "utils.clobbers.end.jppe with nothing to restore" + test "does not jump or restore when parity/overflow is reset" + zest.initRegisters + + utils.preserve "hl" + ; Doesn't clobber scope doesn't affect HL - nothing to preserve + utils.clobbers.withBranching "af" + ; Set all registers to zero + call suite.registers.setAllToZero + call suite.registers.resetAllFlags ; set parity/overflow + + ; This shouldn't jump + utils.clobbers.end.jppe suite.registers.unexpectedJump + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + + expect.stack.size.toBe 0 + utils.restore + + test "jumps but doesn't restore when parity/overflow is set" + zest.initRegisters + + utils.preserve "hl" + ; Doesn't clobber scope doesn't affect HL - nothing to preserve + utils.clobbers.withBranching "af" + ; Set all registers to zero, but set parity flag + call suite.registers.setAllToZero + call suite.registers.setAllFlags + + ; Expect this to jump + utils.clobbers.end.jppe + + zest.fail "Did not jump" + utils.clobbers.end + + +: + ; Expect values to still be zero + call suite.registers.expectAllToBeZero + expect.stack.size.toBe 0 + utils.restore diff --git a/tests/utils/clobbers/clobbers.end.jppo.test.asm b/tests/utils/clobbers/clobbers.end.jppo.test.asm new file mode 100644 index 0000000..bcb35a1 --- /dev/null +++ b/tests/utils/clobbers/clobbers.end.jppo.test.asm @@ -0,0 +1,76 @@ +describe "utils.clobbers.end.jppo" + test "does not jump or restore when parity/overflow is set" + ; Set all registers to $FF + ld a, $ff + call suite.registers.setAllToA + + utils.preserve "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + ; Set all registers to zero + call suite.registers.setAllToZero + call suite.registers.setAllFlags ; set parity/overflow + + ; This shouldn't jump + utils.clobbers.end.jppo suite.registers.unexpectedJump + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + utils.restore + + test "restores and jumps when parity/overflow is reset" + zest.initRegisters + + utils.preserve "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + ; Zero registers + call suite.registers.setAllToZero + call suite.registers.resetAllFlags ; reset parity + + ; Expect this to jump + utils.clobbers.end.jppo + + zest.fail "Did not jump" + utils.clobbers.end + + +: + expect.all.toBeUnclobbered + utils.restore + +describe "utils.clobbers.end.jppo with nothing to restore" + test "does not jump or restore when parity/overflow is set" + zest.initRegisters + + utils.preserve "hl" + ; Doesn't clobber scope doesn't affect HL - nothing to preserve + utils.clobbers.withBranching "af" + ; Set all registers to zero + call suite.registers.setAllToZero + call suite.registers.setAllFlags + + ; This shouldn't jump + utils.clobbers.end.jppo suite.registers.unexpectedJump + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + utils.restore + + test "jumps but doesn't restore when parity/overflow is reset" + zest.initRegisters + + utils.preserve "hl" + ; Doesn't clobber scope doesn't affect HL - nothing to preserve + utils.clobbers.withBranching "af" + ; Set all registers to zero + call suite.registers.setAllToZero + call suite.registers.resetAllFlags ; reset parity + + ; Expect this to jump + utils.clobbers.end.jppo + + zest.fail "Did not jump" + utils.clobbers.end + + +: + ; Expect values to still be zero + call suite.registers.expectAllToBeZero + utils.restore diff --git a/tests/utils/clobbers/clobbers.end.jpz.test.asm b/tests/utils/clobbers/clobbers.end.jpz.test.asm new file mode 100644 index 0000000..4e5a127 --- /dev/null +++ b/tests/utils/clobbers/clobbers.end.jpz.test.asm @@ -0,0 +1,76 @@ +describe "utils.clobbers.end.jpz" + test "when NZ - does not jump or restore" + ; Set all registers to $FF + ld a, $ff + call suite.registers.setAllToA + + utils.preserve "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + ; Set all registers to zero + call suite.registers.setAllToZero + call suite.registers.resetAllFlags ; set NZ + + ; This shouldn't jump + utils.clobbers.end.jpz suite.registers.unexpectedJump + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + utils.restore + + test "when Z - restores and jumps" + zest.initRegisters + + utils.preserve "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + ; Zero registers but set Z flag + call suite.registers.setAllToZero + call suite.registers.setAllFlags + + ; Expect this to jump + utils.clobbers.end.jpz + + zest.fail "Did not jump" + utils.clobbers.end + + +: + expect.all.toBeUnclobbered + utils.restore + +describe "utils.clobbers.end.jpz with nothing to restore" + test "when NZ - does not jump or restore" + zest.initRegisters + + utils.preserve "hl" + ; Doesn't clobber scope doesn't affect HL - nothing to preserve + utils.clobbers.withBranching "af" + ; Set all registers to zero + call suite.registers.setAllToZero + call suite.registers.resetAllFlags + + ; This shouldn't jump + utils.clobbers.end.jpz suite.registers.unexpectedJump + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + utils.restore + + test "when Z - jumps but doesn't restore" + zest.initRegisters + + utils.preserve "hl" + ; Doesn't clobber scope doesn't affect HL - nothing to preserve + utils.clobbers.withBranching "af" + ; Set all registers to zero, but set Z flag + call suite.registers.setAllToZero + call suite.registers.setAllFlags + + ; Expect this to jump + utils.clobbers.end.jpz + + zest.fail "Did not jump" + utils.clobbers.end + + +: + ; Expect values to still be zero + call suite.registers.expectAllToBeZero + utils.restore diff --git a/tests/utils/clobbers/clobbers.end.jrc.test.asm b/tests/utils/clobbers/clobbers.end.jrc.test.asm new file mode 100644 index 0000000..58f1c42 --- /dev/null +++ b/tests/utils/clobbers/clobbers.end.jrc.test.asm @@ -0,0 +1,86 @@ +describe "utils.clobbers.end.jrc" + test "does not jump or restore when carry is reset" + jr + + -: + zest.fail "Unexpected jump" + +: + + ; Set all registers to $FF + ld a, $ff + call suite.registers.setAllToA + + utils.preserve "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + ; Set all registers to zero + call suite.registers.setAllToZero + call suite.registers.resetAllFlags ; reset C + + ; This shouldn't jump + utils.clobbers.end.jrc - + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + utils.restore + + test "restores and jumps when carry is set" + zest.initRegisters + + utils.preserve "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + ; Zero registers but set C flag + call suite.registers.setAllToZero + call suite.registers.setAllFlags + + ; Expect this to jump + utils.clobbers.end.jrc + + zest.fail "Did not jump" + utils.clobbers.end + + +: + expect.all.toBeUnclobbered + utils.restore + +describe "utils.clobbers.end.jrc with nothing to restore" + test "does not jump or restore when carry is reset" + jr + + -: + zest.fail "Unexpected jump" + +: + + zest.initRegisters + + utils.preserve "hl" + ; Doesn't clobber scope doesn't affect HL - nothing to preserve + utils.clobbers.withBranching "af" + ; Set all registers to zero + call suite.registers.setAllToZero + call suite.registers.resetAllFlags + + ; This shouldn't jump + utils.clobbers.end.jrc - + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + utils.restore + + test "jumps but doesn't restore when carry is set" + zest.initRegisters + + utils.preserve "hl" + ; Doesn't clobber scope doesn't affect HL - nothing to preserve + utils.clobbers.withBranching "af" + ; Set all registers to zero, but set Z flag + call suite.registers.setAllToZero + call suite.registers.setAllFlags + + ; Expect this to jump + utils.clobbers.end.jrc + + zest.fail "Did not jump" + utils.clobbers.end + + +: + ; Expect values to still be zero + call suite.registers.expectAllToBeZero + utils.restore diff --git a/tests/utils/clobbers/clobbers.end.jrnc.test.asm b/tests/utils/clobbers/clobbers.end.jrnc.test.asm new file mode 100644 index 0000000..83627ef --- /dev/null +++ b/tests/utils/clobbers/clobbers.end.jrnc.test.asm @@ -0,0 +1,86 @@ +describe "utils.clobbers.end.jrnc" + test "does not jump or restore when carry is set" + jr + + -: + zest.fail "Unexpected jump" + +: + + ; Set all registers to $FF + ld a, $ff + call suite.registers.setAllToA + + utils.preserve "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + ; Set all registers to zero + call suite.registers.setAllToZero + call suite.registers.setAllFlags ; set carry + + ; This shouldn't jump + utils.clobbers.end.jrnc - + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + utils.restore + + test "restores and jumps when carry is reset" + zest.initRegisters + + utils.preserve "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + ; Zero registers + call suite.registers.setAllToZero + call suite.registers.resetAllFlags ; reset carry flag + + ; Expect this to jump + utils.clobbers.end.jrnc + + zest.fail "Did not jump" + utils.clobbers.end + + +: + expect.all.toBeUnclobbered + utils.restore + +describe "utils.clobbers.end.jrnc with nothing to restore" + test "does not jump or restore when carry is set" + jr + + -: + zest.fail "Unexpected jump" + +: + + zest.initRegisters + + utils.preserve "hl" + ; Doesn't clobber scope doesn't affect HL - nothing to preserve + utils.clobbers.withBranching "af" + ; Set all registers to zero + call suite.registers.setAllToZero + call suite.registers.setAllFlags + + ; This shouldn't jump + utils.clobbers.end.jrnc - + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + utils.restore + + test "jumps but doesn't restore when carry is reset" + zest.initRegisters + + utils.preserve "hl" + ; Doesn't clobber scope doesn't affect HL - nothing to preserve + utils.clobbers.withBranching "af" + ; Set all registers to zero, but set Z flag + call suite.registers.setAllToZero + call suite.registers.resetAllFlags + + ; Expect this to jump + utils.clobbers.end.jrnc + + zest.fail "Did not jump" + utils.clobbers.end + + +: + ; Expect values to still be zero + call suite.registers.expectAllToBeZero + utils.restore diff --git a/tests/utils/clobbers/clobbers.end.jrnz.test.asm b/tests/utils/clobbers/clobbers.end.jrnz.test.asm new file mode 100644 index 0000000..35a4ae1 --- /dev/null +++ b/tests/utils/clobbers/clobbers.end.jrnz.test.asm @@ -0,0 +1,86 @@ +describe "utils.clobbers.end.jrnz" + test "when Z - does not jump or restore" + jr + + -: + zest.fail "Unexpected jump" + +: + + ; Set all registers to $FF + ld a, $ff + call suite.registers.setAllToA + + utils.preserve "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + ; Set all registers to zero + call suite.registers.setAllToZero + call suite.registers.setAllFlags ; set Z + + ; This shouldn't jump + utils.clobbers.end.jrnz - + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + utils.restore + + test "when NZ - restores and jumps" + zest.initRegisters + + utils.preserve "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + ; Zero registers but reset Z flag + call suite.registers.setAllToZero + call suite.registers.resetAllFlags + + ; Expect this to jump + utils.clobbers.end.jrnz + + zest.fail "Did not jump" + utils.clobbers.end + + +: + expect.all.toBeUnclobbered + utils.restore + +describe "utils.clobbers.end.jrnz with nothing to restore" + test "when Z - does not jump or restore" + jr + + -: + zest.fail "Unexpected jump" + +: + + zest.initRegisters + + utils.preserve "hl" + ; Doesn't clobber scope doesn't affect HL - nothing to preserve + utils.clobbers.withBranching "af" + ; Set all registers to zero + call suite.registers.setAllToZero + call suite.registers.setAllFlags + + ; This shouldn't jump + utils.clobbers.end.jrnz - + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + utils.restore + + test "when NZ - jumps but doesn't restore" + zest.initRegisters + + utils.preserve "hl" + ; Doesn't clobber scope doesn't affect HL - nothing to preserve + utils.clobbers.withBranching "af" + ; Set all registers to zero, but set Z flag + call suite.registers.setAllToZero + call suite.registers.resetAllFlags + + ; Expect this to jump + utils.clobbers.end.jrnz + + zest.fail "Did not jump" + utils.clobbers.end + + +: + ; Expect values to still be zero + call suite.registers.expectAllToBeZero + utils.restore diff --git a/tests/utils/clobbers/clobbers.end.jrz.test.asm b/tests/utils/clobbers/clobbers.end.jrz.test.asm new file mode 100644 index 0000000..c87d79a --- /dev/null +++ b/tests/utils/clobbers/clobbers.end.jrz.test.asm @@ -0,0 +1,86 @@ +describe "utils.clobbers.end.jrz" + test "when NZ - does not jump or restore" + jr + + -: + zest.fail "Unexpected jump" + +: + + ; Set all registers to $FF + ld a, $ff + call suite.registers.setAllToA + + utils.preserve "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + ; Set all registers to zero + call suite.registers.setAllToZero + call suite.registers.resetAllFlags ; set NZ + + ; This shouldn't jump + utils.clobbers.end.jrz - + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + utils.restore + + test "when Z - restores and jumps" + zest.initRegisters + + utils.preserve "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + ; Zero registers but set Z flag + call suite.registers.setAllToZero + call suite.registers.setAllFlags + + ; Expect this to jump + utils.clobbers.end.jrz + + zest.fail "Did not jump" + utils.clobbers.end + + +: + expect.all.toBeUnclobbered + utils.restore + +describe "utils.clobbers.end.jrz with nothing to restore" + test "when NZ - does not jump or restore" + jr + + -: + zest.fail "Unexpected jump" + +: + + zest.initRegisters + + utils.preserve "hl" + ; Doesn't clobber scope doesn't affect HL - nothing to preserve + utils.clobbers.withBranching "af" + ; Set all registers to zero + call suite.registers.setAllToZero + call suite.registers.resetAllFlags + + ; This shouldn't jump + utils.clobbers.end.jrz - + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + utils.restore + + test "when Z - jumps but doesn't restore" + zest.initRegisters + + utils.preserve "hl" + ; Doesn't clobber scope doesn't affect HL - nothing to preserve + utils.clobbers.withBranching "af" + ; Set all registers to zero, but set Z flag + call suite.registers.setAllToZero + call suite.registers.setAllFlags + + ; Expect this to jump + utils.clobbers.end.jrz + + zest.fail "Did not jump" + utils.clobbers.end + + +: + ; Expect values to still be zero + call suite.registers.expectAllToBeZero + utils.restore diff --git a/tests/utils/clobbers/clobbers.end.retc.test.asm b/tests/utils/clobbers/clobbers.end.retc.test.asm new file mode 100644 index 0000000..543cd12 --- /dev/null +++ b/tests/utils/clobbers/clobbers.end.retc.test.asm @@ -0,0 +1,83 @@ +describe "utils.clobbers.end.retc" + .redefine utils.registers.AUTO_PRESERVE 1 + + test "does not restore or return when carry is reset" + zest.initRegisters + call + + zest.fail "Routine returned" + + +: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0 + call suite.registers.setAllToZero + or a ; reset carry + + ; Call macro + utils.clobbers.end.retc + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + + test "restores and returns when carry is set" + zest.initRegisters + + utils.preserve utils.registers.ALL + jp + + -: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0; Reset flags + call suite.registers.setAllToZero + scf ; set carry + + ; Call macro + utils.clobbers.end.retc + zest.fail "Routine did not return" + utils.clobbers.end + +: + + call - + expect.all.toBeUnclobbered + utils.restore + +describe "utils.clobbers.end.retc with nothing to restore" + .redefine utils.registers.AUTO_PRESERVE 0 + + test "does not restore or return when carry is reset" + zest.initRegisters + call + + zest.fail "Routine returned" + + +: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0 + call suite.registers.setAllToZero + or a ; reset carry + + ; Call macro + utils.clobbers.end.retc + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + + test "restores and returns when carry is set" + zest.initRegisters + + utils.preserve utils.registers.ALL + jp + + -: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0; Reset flags + call suite.registers.setAllToZero + scf ; set carry + + ; Call macro + utils.clobbers.end.retc + zest.fail "Routine did not return" + utils.clobbers.end + +: + + call - + expect.all.toBeUnclobbered + utils.restore diff --git a/tests/utils/clobbers/clobbers.end.retm.test.asm b/tests/utils/clobbers/clobbers.end.retm.test.asm new file mode 100644 index 0000000..743bc53 --- /dev/null +++ b/tests/utils/clobbers/clobbers.end.retm.test.asm @@ -0,0 +1,83 @@ +describe "utils.clobbers.end.retm" + .redefine utils.registers.AUTO_PRESERVE 1 + + test "does not restore or return when sign is reset" + zest.initRegisters + call + + zest.fail "Routine returned" + + +: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0 (including flags) + call suite.registers.setAllToZero + + ; Call macro + utils.clobbers.end.retm + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + + test "restores and returns when sign is set" + zest.initRegisters + + utils.preserve utils.registers.ALL + jp + + -: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0; Reset flags + call suite.registers.setAllToZero + dec a + or a ; set sign + + ; Call macro + utils.clobbers.end.retm + zest.fail "Routine did not return" + utils.clobbers.end + +: + + call - + expect.all.toBeUnclobbered + utils.restore + +describe "utils.clobbers.end.retm with nothing to restore" + .redefine utils.registers.AUTO_PRESERVE 0 + + test "does not restore or return when sign is reset" + zest.initRegisters + call + + zest.fail "Routine returned" + + +: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0 including flags + call suite.registers.setAllToZero + + ; Call macro + utils.clobbers.end.retm + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + + test "restores and returns when sign is set" + zest.initRegisters + + utils.preserve utils.registers.ALL + jp + + -: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0; Reset flags + call suite.registers.setAllToZero + dec a + or a ; set sign + + ; Call macro + utils.clobbers.end.retm + zest.fail "Routine did not return" + utils.clobbers.end + +: + + call - + expect.all.toBeUnclobbered + utils.restore diff --git a/tests/utils/clobbers/clobbers.end.retnc.test.asm b/tests/utils/clobbers/clobbers.end.retnc.test.asm new file mode 100644 index 0000000..0d8bf59 --- /dev/null +++ b/tests/utils/clobbers/clobbers.end.retnc.test.asm @@ -0,0 +1,83 @@ +describe "utils.clobbers.end.retnc" + .redefine utils.registers.AUTO_PRESERVE 1 + + test "does not restore or return when carry is set" + zest.initRegisters + call + + zest.fail "Routine returned" + + +: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0 + call suite.registers.setAllToZero + scf ; set carry + + ; Call macro + utils.clobbers.end.retnc + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + + test "restores and returns when carry is reset" + zest.initRegisters + + utils.preserve utils.registers.ALL + jp + + -: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0; Reset flags + call suite.registers.setAllToZero + or a ; reset carry + + ; Call macro + utils.clobbers.end.retnc + zest.fail "Routine did not return" + utils.clobbers.end + +: + + call - + expect.all.toBeUnclobbered + utils.restore + +describe "utils.clobbers.end.retnc with nothing to restore" + .redefine utils.registers.AUTO_PRESERVE 0 + + test "does not restore or return when carry is set" + zest.initRegisters + call + + zest.fail "Routine returned" + + +: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0 + call suite.registers.setAllToZero + scf ; set carry + + ; Call macro + utils.clobbers.end.retnc + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + + test "restores and returns when carry is reset" + zest.initRegisters + + utils.preserve utils.registers.ALL + jp + + -: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0; Reset flags + call suite.registers.setAllToZero + or a ; reset carry + + ; Call macro + utils.clobbers.end.retnc + zest.fail "Routine did not return" + utils.clobbers.end + +: + + call - + expect.all.toBeUnclobbered + utils.restore diff --git a/tests/utils/clobbers/clobbers.end.retnz.test.asm b/tests/utils/clobbers/clobbers.end.retnz.test.asm new file mode 100644 index 0000000..016e939 --- /dev/null +++ b/tests/utils/clobbers/clobbers.end.retnz.test.asm @@ -0,0 +1,85 @@ +describe "utils.clobbers.end.retnz" + .redefine utils.registers.AUTO_PRESERVE 1 + + test "does not restore or return when zero is set" + zest.initRegisters + call + + zest.fail "Routine returned" + + +: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0 + call suite.registers.setAllToZero + or a ; set zero + + ; Call macro + utils.clobbers.end.retnz + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + + test "restores and returns when zero is reset" + zest.initRegisters + + utils.preserve utils.registers.ALL + jp + + -: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0; Reset flags + call suite.registers.setAllToZero + inc a + or a ; reset zero + + ; Call macro + utils.clobbers.end.retnz + zest.fail "Routine did not return" + utils.clobbers.end + +: + + call - + expect.all.toBeUnclobbered + utils.restore + +describe "utils.clobbers.end.retnz with nothing to restore" + .redefine utils.registers.AUTO_PRESERVE 0 + + test "does not restore or return when zero is set" + zest.initRegisters + call + + zest.fail "Routine returned" + + +: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0 + call suite.registers.setAllToZero + or a ; set zero + + ; Call macro + utils.clobbers.end.retnz + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + + test "restores and returns when zero is reset" + zest.initRegisters + + utils.preserve utils.registers.ALL + jp + + -: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0; Reset flags + call suite.registers.setAllToZero + inc a + or a ; reset zero + + ; Call macro + utils.clobbers.end.retnz + zest.fail "Routine did not return" + utils.clobbers.end + +: + + call - + expect.all.toBeUnclobbered + utils.restore diff --git a/tests/utils/clobbers/clobbers.end.retp.test.asm b/tests/utils/clobbers/clobbers.end.retp.test.asm new file mode 100644 index 0000000..c03c1b8 --- /dev/null +++ b/tests/utils/clobbers/clobbers.end.retp.test.asm @@ -0,0 +1,85 @@ +describe "utils.clobbers.end.retp" + .redefine utils.registers.AUTO_PRESERVE 1 + + test "does not restore or return when sign is set" + zest.initRegisters + call + + zest.fail "Routine returned" + + +: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0 + call suite.registers.setAllToZero + dec a + or a ; set sign + ld a, 0 + + ; Call macro + utils.clobbers.end.retp + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + + test "restores and returns when sign is reset" + zest.initRegisters + + utils.preserve utils.registers.ALL + jp + + -: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0; Reset flags + call suite.registers.setAllToZero + + ; Call macro + utils.clobbers.end.retp + zest.fail "Routine did not return" + utils.clobbers.end + +: + + call - + expect.all.toBeUnclobbered + utils.restore + +describe "utils.clobbers.end.retp with nothing to restore" + .redefine utils.registers.AUTO_PRESERVE 0 + + test "does not restore or return when sign is set" + zest.initRegisters + call + + zest.fail "Routine returned" + + +: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0 + call suite.registers.setAllToZero + dec a + or a ; set sign + ld a, 0 + + ; Call macro + utils.clobbers.end.retp + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + + test "restores and returns when sign is reset" + zest.initRegisters + + utils.preserve utils.registers.ALL + jp + + -: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0; Reset flags + call suite.registers.setAllToZero + + ; Call macro + utils.clobbers.end.retp + zest.fail "Routine did not return" + utils.clobbers.end + +: + + call - + expect.all.toBeUnclobbered + utils.restore diff --git a/tests/utils/clobbers/clobbers.end.retpe.test.asm b/tests/utils/clobbers/clobbers.end.retpe.test.asm new file mode 100644 index 0000000..08f2dab --- /dev/null +++ b/tests/utils/clobbers/clobbers.end.retpe.test.asm @@ -0,0 +1,81 @@ +describe "utils.clobbers.end.retpe" + .redefine utils.registers.AUTO_PRESERVE 1 + + test "does not restore or return when parity/overflow is reset" + zest.initRegisters + call + + zest.fail "Routine returned" + + +: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0 (inc flags) + call suite.registers.setAllToZero + + ; Call macro + utils.clobbers.end.retpe + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + + test "restores and returns when parity/overflow is set" + zest.initRegisters + + utils.preserve utils.registers.ALL + jp + + -: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0; Reset flags + call suite.registers.setAllToZero + or a ; set parity/overflow + + ; Call macro + utils.clobbers.end.retpe + zest.fail "Routine did not return" + utils.clobbers.end + +: + + call - + expect.all.toBeUnclobbered + utils.restore + +describe "utils.clobbers.end.retpe with nothing to restore" + .redefine utils.registers.AUTO_PRESERVE 0 + + test "does not restore or return when parity/overflow is reset" + zest.initRegisters + call + + zest.fail "Routine returned" + + +: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0 inc flags + call suite.registers.setAllToZero + + ; Call macro + utils.clobbers.end.retpe + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + + test "restores and returns when parity/overflow is set" + zest.initRegisters + + utils.preserve utils.registers.ALL + jp + + -: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0; Reset flags + call suite.registers.setAllToZero + or a ; set parity/overflow + + ; Call macro + utils.clobbers.end.retpe + zest.fail "Routine did not return" + utils.clobbers.end + +: + + call - + expect.all.toBeUnclobbered + utils.restore diff --git a/tests/utils/clobbers/clobbers.end.retpo.test.asm b/tests/utils/clobbers/clobbers.end.retpo.test.asm new file mode 100644 index 0000000..49e7d34 --- /dev/null +++ b/tests/utils/clobbers/clobbers.end.retpo.test.asm @@ -0,0 +1,85 @@ +describe "utils.clobbers.end.retpo" + .redefine utils.registers.AUTO_PRESERVE 1 + + test "does not restore or return when zero is set" + zest.initRegisters + call + + zest.fail "Routine returned" + + +: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0 + call suite.registers.setAllToZero + or a ; set zero + + ; Call macro + utils.clobbers.end.retpo + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + + test "restores and returns when zero is reset" + zest.initRegisters + + utils.preserve utils.registers.ALL + jp + + -: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0; Reset flags + call suite.registers.setAllToZero + inc a + or a ; reset zero + + ; Call macro + utils.clobbers.end.retpo + zest.fail "Routine did not return" + utils.clobbers.end + +: + + call - + expect.all.toBeUnclobbered + utils.restore + +describe "utils.clobbers.end.retpo with nothing to restore" + .redefine utils.registers.AUTO_PRESERVE 0 + + test "does not restore or return when zero is set" + zest.initRegisters + call + + zest.fail "Routine returned" + + +: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0 + call suite.registers.setAllToZero + or a ; set zero + + ; Call macro + utils.clobbers.end.retpo + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + + test "restores and returns when zero is reset" + zest.initRegisters + + utils.preserve utils.registers.ALL + jp + + -: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0; Reset flags + call suite.registers.setAllToZero + inc a + or a ; reset zero + + ; Call macro + utils.clobbers.end.retpo + zest.fail "Routine did not return" + utils.clobbers.end + +: + + call - + expect.all.toBeUnclobbered + utils.restore diff --git a/tests/utils/clobbers/clobbers.end.retz.test.asm b/tests/utils/clobbers/clobbers.end.retz.test.asm new file mode 100644 index 0000000..543cd12 --- /dev/null +++ b/tests/utils/clobbers/clobbers.end.retz.test.asm @@ -0,0 +1,83 @@ +describe "utils.clobbers.end.retc" + .redefine utils.registers.AUTO_PRESERVE 1 + + test "does not restore or return when carry is reset" + zest.initRegisters + call + + zest.fail "Routine returned" + + +: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0 + call suite.registers.setAllToZero + or a ; reset carry + + ; Call macro + utils.clobbers.end.retc + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + + test "restores and returns when carry is set" + zest.initRegisters + + utils.preserve utils.registers.ALL + jp + + -: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0; Reset flags + call suite.registers.setAllToZero + scf ; set carry + + ; Call macro + utils.clobbers.end.retc + zest.fail "Routine did not return" + utils.clobbers.end + +: + + call - + expect.all.toBeUnclobbered + utils.restore + +describe "utils.clobbers.end.retc with nothing to restore" + .redefine utils.registers.AUTO_PRESERVE 0 + + test "does not restore or return when carry is reset" + zest.initRegisters + call + + zest.fail "Routine returned" + + +: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0 + call suite.registers.setAllToZero + or a ; reset carry + + ; Call macro + utils.clobbers.end.retc + + ; Expect everything to still be zero + call suite.registers.expectAllToBeZero + utils.clobbers.end + + test "restores and returns when carry is set" + zest.initRegisters + + utils.preserve utils.registers.ALL + jp + + -: + utils.clobbers.withBranching utils.registers.ALL + ; Set registers to 0; Reset flags + call suite.registers.setAllToZero + scf ; set carry + + ; Call macro + utils.clobbers.end.retc + zest.fail "Routine did not return" + utils.clobbers.end + +: + + call - + expect.all.toBeUnclobbered + utils.restore diff --git a/tests/utils/clobbers/clobbers.endBranch.test.asm b/tests/utils/clobbers/clobbers.endBranch.test.asm new file mode 100644 index 0000000..c3919f8 --- /dev/null +++ b/tests/utils/clobbers/clobbers.endBranch.test.asm @@ -0,0 +1,52 @@ +describe "utils.clobbers.endBranch" + test "restores protected registers that are being clobbered" + zest.initRegisters + + utils.preserve "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + call suite.registers.clobberAll + + utils.clobbers.endBranch + + expect.all.toBeUnclobbered + + jp + + utils.clobbers.end + + +: + utils.restore + + test "only restores the protected registers" + ld bc, $bc01 + + utils.preserve "bc" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + ; Zero all registers + call suite.registers.setAllToZero + + utils.clobbers.endBranch + + ; Expect BC to be restored + expect.bc.toBe $bc01 + + ; Expect the rest to be 0 + expect.a.toBe 0 + expect.de.toBe 0 + expect.hl.toBe 0 + expect.ix.toBe 0 + expect.iy.toBe 0 + expect.i.toBe 0 + + ex af, af' + exx + + expect.a.toBe 0 + expect.bc.toBe 0 + expect.de.toBe 0 + expect.hl.toBe 0 + + jp + + utils.clobbers.end + + +: + utils.restore diff --git a/tests/utils/clobbers/clobbers.withBranching.test.asm b/tests/utils/clobbers/clobbers.withBranching.test.asm new file mode 100644 index 0000000..f147d04 --- /dev/null +++ b/tests/utils/clobbers/clobbers.withBranching.test.asm @@ -0,0 +1,144 @@ +describe "utils.clobbers.withBranching" + test "preserves the registers it clobbers" + zest.initRegisters + + utils.preserve + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'", "hl'" + call suite.registers.clobberAll + utils.clobbers.end + + expect.all.toBeUnclobbered + utils.restore + + test "preserves registers it clobbers when auto-preserve is enabled" + .redefine utils.registers.AUTO_PRESERVE 1 + + zest.initRegisters + + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'", "hl'" + call suite.registers.clobberAll + utils.clobbers.end + + expect.all.toBeUnclobbered + + ; Assert all clobber scopes have been resolved + ld a, utils.clobbers.index + expect.a.toBe -1 + + ; Assert all preserve scopes have been resolved + ld a, utils.registers.preserveIndex + expect.a.toBe -1 + + .redefine utils.registers.AUTO_PRESERVE 0 + + test "doesn't preserve registers that aren't marked for preservation" + utils.preserve "af" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'", "hl'" + expect.stack.size.toBe 1 + utils.clobbers.end + utils.restore + + test "doesn't preserve any registers if no preserve scopes are in progress" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'", "hl'" + expect.stack.size.toBe 0 + utils.clobbers.end + + test "doesn't preserve registers that aren't marked as clobbered" + utils.preserve "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'", "hl'" + utils.clobbers.withBranching "af" + expect.stack.size.toBe 1 + utils.clobbers.end + utils.restore + + test "preserves registers even if a previous withBranching scope has already done so" + ld bc, $bc00 + ld de, $de00 + + utils.preserve "bc" + utils.clobbers.withBranching "bc" + expect.stack.size.toBe 1 + ld bc, $bc02 + utils.clobbers.end + + utils.clobbers.withBranching "bc" + expect.stack.size.toBe 1 + ld bc, $bc02 + utils.clobbers.end + utils.restore + + expect.stack.size.toBe 0 + expect.bc.toBe $bc00 + + test "preserves registers even if a previous clobbers scope has already done so" + ld bc, $bc00 + ld de, $de00 + + utils.preserve "bc" + utils.clobbers "bc" + ld bc, $bc01 + utils.clobbers.end + + utils.clobbers.withBranching "bc" + expect.stack.size.toBe 2 + ld bc, $bc02 + utils.clobbers.end + + expect.stack.size.toBe 1 + expect.bc.toBe $bc01 + utils.restore + + expect.stack.size.toBe 0 + expect.bc.toBe $bc00 + + test "preserves registers even if an ancestor has done so" + ld bc, $bc00 + ld de, $de00 + + utils.preserve "bc" + utils.clobbers.withBranching "bc" + expect.stack.size.toBe 1 + expect.stack.toContain $bc00 + ld bc, $bc01 + + utils.clobbers.withBranching "bc" + expect.stack.size.toBe 2 + expect.stack.toContain $bc01 + utils.clobbers.end + utils.clobbers.end + utils.restore + + expect.stack.size.toBe 0 + expect.bc.toBe $bc00 + + test "endBranch restores the registers without closing the clobber scope" + zest.initRegisters + + utils.preserve "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + call suite.registers.clobberAll + + utils.clobbers.endBranch + + expect.all.toBeUnclobbered + expect.stack.size.toBe 0 + + ld a, utils.clobbers.index + expect.a.toBe 0 "Expected clobber scope to remain in progress" + utils.clobbers.closeBranch + utils.restore + + test "closeBranch ends the clobber scope without restoring the registers" + zest.initRegisters + + utils.preserve "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + utils.clobbers.withBranching "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'" "hl'" + call suite.registers.setAllToZero + utils.clobbers.closeBranch + + ; Expect nothing to have been restored + call suite.registers.expectAllToBeZero + + ; Expect clobber scope to have been closed + ld a, utils.clobbers.index + expect.a.toBe -1 "Expected no clobber scopes to be in progress" + utils.restore diff --git a/tests/utils/clobbers/sequentialClobberScopes.test.asm b/tests/utils/clobbers/sequentialClobberScopes.test.asm new file mode 100644 index 0000000..53678f2 --- /dev/null +++ b/tests/utils/clobbers/sequentialClobberScopes.test.asm @@ -0,0 +1,63 @@ +describe "register preservation: sequential (unnested) clobber scopes" + test "the second scope shouldn't preserve the same registers" + ld bc, $bc00 + + ; Preserve scope + utils.preserve "bc" + ; First clobber scope clobbers BC + utils.clobbers "bc" + expect.stack.size.toBe 1 + expect.stack.toContain $bc00 + ld bc, $bc01 + utils.clobbers.end + + ; Second clobber scope also clobbers BC, but shouldn't push to stack again + utils.clobbers "bc" + expect.stack.size.toBe 1 "Expected stack size to still be 1" + expect.stack.toContain $bc00 0 "Expected stack to still contain original BC value" + ld bc, $bc02 + utils.clobbers.end + utils.restore + + expect.bc.toBe $bc00 + + test "the second scope should preserve the registers the first hasn't" + ld bc, $bc00 + ld de, $de00 + + ; Preserve scope preserves BC and DE + utils.preserve "bc" "de" + ; First clobber scope clobbers DE + utils.clobbers "de" + expect.stack.toContain $de00 + ld de, $de01 + utils.clobbers.end + + ; Second clobber scope clobbers BC + utils.clobbers "bc" + expect.stack.toContain $bc00 + ld bc, $bc02 + utils.clobbers.end + utils.restore + + expect.bc.toBe $bc00 + expect.de.toBe $de00 + + test "restores the correct registers (random order)" + zest.initRegisters + + utils.preserve "af", "bc", "de", "hl", "ix", "iy", "i", "af'", "bc'", "de'", "hl'" + utils.clobbers "hl" + ld h, a + ld l, a + utils.clobbers.end + + utils.clobbers "ix", "bc" + ld b, a + ld c, a + ld ixh, a + ld ixl, a + utils.clobbers.end + utils.restore + + expect.all.toBeUnclobbered \ No newline at end of file diff --git a/tests/utils/registers/_helpers.asm b/tests/utils/registers/_helpers.asm new file mode 100644 index 0000000..057e5e4 --- /dev/null +++ b/tests/utils/registers/_helpers.asm @@ -0,0 +1,86 @@ +.section "suite.registers helpers" + suite.registers.setAllToZero: + xor a + suite.registers.setAllToA: + ld b, a + ld c, a + ld d, a + ld e, a + ld h, a + ld l, a + ld ixl, a + ld ixh, a + ld iyl, a + ld iyh, a + ld i, a + + push hl + pop af ; set flags + + ex af, af' + ld a, b + exx + ld b, a + ld c, a + ld d, a + ld e, a + ld h, a + ld l, a + + push hl + pop af ; set flags + + ret + + suite.registers.clobberAll: + ex af, af' ; switch AF and AF' to clobber both + exx ; switch main registers with shadow to clobber both sets + + ; Clobber index registers + ld ixl, a + ld ixh, a + ld iyl, a + ld iyh, a + ld i, a + ret + + suite.registers.expectAllToBeZero: + expect.a.toBe 0 + expect.bc.toBe 0 + expect.de.toBe 0 + expect.hl.toBe 0 + expect.ix.toBe 0 + expect.iy.toBe 0 + expect.i.toBe 0 + + ex af, af' + expect.a.toBe 0 + ex af, af' + exx + expect.bc.toBe 0 + expect.de.toBe 0 + expect.hl.toBe 0 + exx + ret + + suite.registers.setAllFlags: + push hl + ld h, a + ld l, $ff ; flags + push hl ; push to stack + pop af ; restore to AF + pop hl + ret + + suite.registers.resetAllFlags: + push hl + ld h, a + ld l, 0 ; flags + push hl ; push to stack + pop af ; restore to AF + pop hl + ret + + suite.registers.unexpectedJump: + zest.fail "Unexpected jump" +.ends \ No newline at end of file diff --git a/tests/utils/registers/autoPreserve.test.asm b/tests/utils/registers/autoPreserve.test.asm new file mode 100644 index 0000000..089cc8b --- /dev/null +++ b/tests/utils/registers/autoPreserve.test.asm @@ -0,0 +1,53 @@ +describe "automatic register preservation" + ; Activate AUTO_PRESERVE + .redefine utils.registers.AUTO_PRESERVE 1 + + test "preserves all clobbered registers" + zest.initRegisters + + utils.clobbers "af" "bc" "de" "hl" "ix" "iy" "i" "af'" "bc'" "de'", "hl'" + call suite.registers.clobberAll + utils.clobbers.end + + expect.all.toBeUnclobbered + + test "only preserves clobbered registers" + ld bc, $bc01 + ld de, $de01 + + utils.clobbers "bc" + expect.stack.size.toBe 1 + expect.stack.toContain $bc01 + utils.clobbers.end + + utils.clobbers "bc" "de" + expect.stack.size.toBe 2 + expect.stack.toContain $de01 + expect.stack.toContain $bc01 1 + utils.clobbers.end + + test "only preserves registers once if there are nested preserve scopes" + utils.clobbers "af" + expect.stack.size.toBe 1 + + utils.clobbers "af" + expect.stack.size.toBe 1 "Expected stack size to still be 1" + utils.clobbers.end + utils.clobbers.end + + test "does not override existing preserve scopes" + ld bc, $bc00 + + ; Preserve scope already exists - this should not be overridden + utils.registers.preserve "bc" + ; Clobber scope clobbers all registers + utils.clobbers "af", "bc", "de", "hl", "ix", "iy", "i", "af'", "bc'", "de'", "hl'" + ; Only BC should have been preserved + expect.stack.size.toBe 1 + expect.stack.toContain $bc00 + utils.clobbers.end + utils.restore + + expect.bc.toBe $bc00 + +.redefine registers.AUTO_PRESERVE 0 diff --git a/tests/utils/registers/iRegister.test.asm b/tests/utils/registers/iRegister.test.asm new file mode 100644 index 0000000..9fa9bc7 --- /dev/null +++ b/tests/utils/registers/iRegister.test.asm @@ -0,0 +1,36 @@ +describe "i-register preservation" + test "preserves and restores AF and I" + zest.initRegisters + + utils.preserve "af" "i" + utils.clobbers "i" + ld i, a + utils.clobbers.end + + utils.clobbers "af" + inc a + utils.clobbers.end + utils.restore + + expect.all.toBeUnclobbered + + test "should preserve multiple nested values" + ; Set I to 0 + ld a, 0 + ld i, a + + ; Create multiple nested preserve and clobber scopes that inc I + .repeat utils.registers.I_STACK_MAX_SIZE index index + utils.preserve "i" + utils.clobbers "i" + inc a + ld i, a + .endr + + ; Restore each nested preserve scope + .repeat utils.registers.I_STACK_MAX_SIZE index restoreIndex + utils.clobbers.end + utils.restore + + expect.i.toBe utils.registers.I_STACK_MAX_SIZE - restoreIndex - 1 + .endr diff --git a/tests/utils/registers/nestedPreserveScopes.test.asm b/tests/utils/registers/nestedPreserveScopes.test.asm new file mode 100644 index 0000000..1962e34 --- /dev/null +++ b/tests/utils/registers/nestedPreserveScopes.test.asm @@ -0,0 +1,123 @@ +describe "register preservation: nested preserve scopes" + test "preserves registers for each preserve scope" + ld a, $01 + call suite.registers.setAllToA + + ; Preserve all registers (containing $01) + utils.preserve + ; Clob all registers + utils.clobbers "af", "bc", "de", "hl", "ix", "iy", "i", "af'", "bc'", "de'", "hl'" + ld a, $02 + call suite.registers.setAllToA + + ; Preserve all registers again (containing $02) + utils.preserve + ; Clob all registers with $03 + utils.clobbers "af", "bc", "de", "hl", "ix", "iy", "i", "af'", "bc'", "de'", "hl'" + ld a, $03 + call suite.registers.setAllToA + utils.clobbers.end + utils.restore + + ; Expect registers to be back to $02 after inner context + expect.a.toBe $02 + expect.bc.toBe $0202 + expect.de.toBe $0202 + expect.hl.toBe $0202 + expect.ix.toBe $0202 + expect.iy.toBe $0202 + expect.i.toBe $02 + utils.clobbers.end + utils.restore + + ; Expect all registers to be back to $01 after outer context + expect.a.toBe $01 + expect.bc.toBe $0101 + expect.de.toBe $0101 + expect.hl.toBe $0101 + expect.ix.toBe $0101 + expect.iy.toBe $0101 + expect.i.toBe $01 + + test "preserves registers required by ancestor scopes" + ld bc, $bc01 + ld de, $de01 + ld hl, $ff01 + + ; Outer context requires BC to be preserved + utils.preserve "bc" + ; Middle context requires DE to be preserved + utils.preserve "de" + ; Nothing clobbered or preserved so far + expect.stack.size.toBe 0 + + ; Inner context preserves HL + utils.preserve "hl" + ; This clobber scope clobbers all three pairs + utils.clobbers "bc" "de" "hl" + expect.stack.size.toBe 3 + ld bc, $bc02 + ld de, $de02 + ld hl, $ff02 + utils.clobbers.end + utils.restore + + expect.hl.toBe $ff01 ; back to original + utils.restore + + expect.de.toBe $de01 ; back to original + utils.restore + + expect.bc.toBe $bc01 ; back to original + + test "does not preserve registers from ancestor scopes if they've already been preserved" + ld bc, $bc01 + ld de, $de01 + + ; Outer context requires BC to be preserved + utils.preserve "bc" + ; Clob scope clobbers BC + utils.clobbers "bc" + ; Expect BC to have been preserved by this point + expect.stack.size.toBe 1 + expect.stack.toContain $bc01 + ld bc, $bc02 + + ; Inner scope requires DE to be preserved + utils.preserve "de" + ; This clobber scope clobbers both BC and DE + utils.clobbers "bc" "de" + ; DE shouldn't be preserved again as the outer clobberStart + ; has already done so + expect.stack.size.toBe 2 + expect.stack.toContain $de01 ; original value of DE + expect.stack.toContain $bc01 1 ; original value of BC + + ld de, $de02 + utils.clobbers.end + utils.restore + + expect.de.toBe $de01 + utils.clobbers.end + utils.restore + + expect.bc.toBe $bc01 + + test "restores the correct registers (random order)" + zest.initRegisters + + utils.preserve "af", "bc", "de", "hl", "ix", "iy", "i", "af'", "bc'", "de'", "hl'" + utils.clobbers "hl" + ld h, a + ld l, a + + utils.clobbers "ix", "bc" + ld b, a + ld c, a + ld ixh, a + ld ixl, a + utils.clobbers.end + utils.clobbers.end + utils.restore + + expect.all.toBeUnclobbered \ No newline at end of file diff --git a/tests/utils/registers/registers.test.asm b/tests/utils/registers/registers.test.asm new file mode 100644 index 0000000..c4f025f --- /dev/null +++ b/tests/utils/registers/registers.test.asm @@ -0,0 +1,108 @@ +describe "register preservation" + ; Deactivate AUTO_PRESERVE + .redefine utils.registers.AUTO_PRESERVE 0 + + test "preserves all clobbered registers by default" + zest.initRegisters + + utils.preserve ; no registers - preserve all by default + utils.clobbers "af", "bc", "de", "hl", "ix", "iy", "i", "af'", "bc'", "de'", "hl'" + call suite.registers.clobberAll + utils.clobbers.end + utils.restore + + expect.all.toBeUnclobbered + + test "should not preserve any registers if no preserve scopes are in progress" + utils.clobbers "af", "bc", "de", "hl", "ix", "iy", "i", "af'", "bc'", "de'", "hl'" + expect.stack.size.toBe 0 + utils.clobbers.end + + test "preserves all registers marked as clobbered" + zest.initRegisters + + ; Preserve all registers + utils.preserve "af", "bc", "de", "hl", "ix", "iy", "i", "af'", "bc'", "de'", "hl'" + ; This clobber scope clobbers all registers + utils.clobbers "af", "bc", "de", "hl", "ix", "iy", "i", "af'", "bc'", "de'", "hl'" + call suite.registers.clobberAll + utils.clobbers.end + utils.restore + + ; Expect all registers to have been preserved + expect.all.toBeUnclobbered + + test "only preserves the requested registers" + ld bc, $bc01 + ld de, $de01 + ld hl, $ffff + + ; Preserve BC and DE only + utils.preserve "bc", "de" + ; This clobber scope clobbers all registers + utils.clobbers "bc", "de", "hl" + ld bc, 0 + ld de, 0 + ld hl, 0 + utils.clobbers.end + utils.restore + + ; Expect BC and DE to have been preserved + expect.bc.toBe $bc01 + expect.de.toBe $de01 + + ; Expect HL to have been clobbered + expect.hl.toBe 0 + + test "only preserves registers marked as clobbered" + ld bc, $bc01 + ld de, $de01 + ld hl, $ffff + + ; Preserve all registers + utils.preserve "af", "bc", "de", "hl", "ix", "iy", "i", "af'", "bc'", "de'", "hl'" + ; This clobber scope only clobbers DE and HL + utils.clobbers "bc", "de" + ld bc, 0 + ld de, 0 + ld hl, 0 + utils.clobbers.end + utils.restore + + ; Expect BC and DE to have been preserved + expect.bc.toBe $bc01 + expect.de.toBe $de01 + + ; Expect HL to have been clobbered + expect.hl.toBe 0 + + test "only preserves registers once per preserve context" + ld bc, $bc01 + + ; Preserve BC + utils.preserve "bc" + ; Nothing should have been preserved yet + expect.stack.size.toBe 0 + + ; This clobber scope clobbers BC + utils.clobbers "bc" + ; Expect BC to have been pushed to the stack + expect.stack.size.toBe 1 + expect.stack.toContain $bc01 + + ld bc, $bc02 + + ; This inner clobber scope also clobbers BC + utils.clobbers "bc" + ; The outer scope has already preserved BC in the stack, + ; so expect this not to have pushed it again + expect.stack.size.toBe 1 "Expected stack size to still be 1" + expect.stack.toContain $bc01 0 "Expected stack to still contain the original value" + + ld bc, $bc03 + utils.clobbers.end + utils.clobbers.end + utils.restore + + expect.stack.size.toBe 0 "Expected stack size to be back to 0" + expect.bc.toBe $bc01 diff --git a/tests/zest b/tests/zest deleted file mode 160000 index fda167b..0000000 --- a/tests/zest +++ /dev/null @@ -1 +0,0 @@ -Subproject commit fda167be69bf8fb07a25377c157f294ceea6d454