.macro delay_n n ≺ ··· contents ··· ≻ .endmacroThis macro produces exactly N cycles of delay, for use in timing-sensitive NES code, such as scanline effects, screen splits, or PCM sound generation. The delay code is algorithmically computed, optimized for size, extremely tight packed, and uses techniques such as partial opcode execution.
Examples of these generated delay routines:
php plp
rol $26 ror $26
lda #$2A ;hides 'rol a' clc bpl *-2
cmp #$CD ;hides 'cmp $BDA2' ldx #189 dex bmi *-4
php pha txa ldx #202 ;hides 'dex' bne *-1 tax pla plp
From real life, example code that uses this macro:
.include "6502-inline_delay-keepy-ram.inc" ··· @nearby_rts = $C452 @nearby_rts_14cyc = $C43B ; clc + rts @nearby_rts_15cyc = $C649 ; jmp + rts @zptemp = $07 ··· .if (MAPPER = 24) .or (MAPPER = 26) ;VRC6 cost_d1 = 92 MAP_CHANGE_COST = 12 + 2*4 + (4+2+6)*2 + 2*4 + 8*4 MAP_CHANGE_OFFSET = 15 .endif ··· delay_n (cost_d-MAP_CHANGE_COST+MAP_CHANGE_OFFSET)
*) Barring discoveries of even more efficient code.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
i=5 j=10 delay_n i+j ;wrong delay_n (i+j) ;correct
If you know exactly what you are playing, it is possible to compensate, and still get very nearly cycle-accurate delays with a small margin of error.
An example of a delay macro, that does such cycle-compensation, can be found here: http://iki.fi/bisqwit/src/6502-inline_dmc_delay_compensation.inc.
This DMC-compensation module works only with NES Simon’s Quest, as it relies on knowing exactly which DPCM sample is currently playing; but the code can be changed to work with any other game, as long as it is possible to know which sample is currently playing and at which speed.
I have used this macro as follows:
.macro MYDELAY n_scanlines, ntscadjust, paladjust, extra .if PAL=0 compensate_dmc_delay extra, (n_scanlines)*341 /3 + ntscadjust .else compensate_dmc_delay extra, (n_scanlines)*341*5/16 + paladjust .endif .endmacro ··· ; Delay exactly 48 scanlines, plus cost_c1 or cost_c2 cycles, ; depending whether we're PAL or NTSC; ; with the assumption that MAP_CHANGE_COST cycles ; of DMC-unaware delay were already performed by a ; previously issued mapper-change routine. MYDELAY 48, cost_c1, cost_c2, MAP_CHANGE_COST
And that garbage can be safely written into the following addresses:
Reading from write-only ports and writing into read-only ports is considered safe, as the recipient circuitry ignores those accesses. Writing into ROM is not considered safe, because many cartridges contain mapper circuitry that intercepts those writes, causing side-effects. Technically I could have made that a selectable option, but I had to draw the line somewhere: generating all these options already consumed weeks of CPU time.
If @zptemp is enabled, it is also assumed that memory address $xxE6, where xx=@zptemp, can be safely read. Of particular concern are @zptemp values of $40–$5F. Technically there could be a device on the gamecart that responds with side-effects into reads concerning such addresses, for example $51E6. If this is the case, do not use @zptemp, or choose another value for @zptemp.
If the not-Famicom option (nofc) is chosen, then only the memory region $0000–$07FF is assumed readable, and nothing is considered writable, unless explicitly enabled.
The purpose of these macros is to verify at link-time that the branch does not cross page boundaries. The reason is that branch instructions consume an extra cycle when a page-crossing happens, and the delay code is not written to account for that.
.macro branch_check opc, dest opc dest .assert >* = >(dest), warning, "branch_check: failed, crosses page" .endmacro .macro Jcc dest branch_check bcc, dest .endmacro .macro Jcs dest branch_check bcs, dest .endmacro .macro Jeq dest branch_check beq, dest .endmacro .macro Jne dest branch_check bne, dest .endmacro .macro Jmi dest branch_check bmi, dest .endmacro .macro Jpl dest branch_check bpl, dest .endmacro .macro Jvc dest branch_check bvc, dest .endmacro .macro Jvs dest branch_check bvs, dest .endmacro
Even if you disable the clobbering of the D flag, the code might include CLD / SED instructions and ADC / SBC operations that assume binary mode; just wrapped in PHP⋯PLP.
An example situation where this may happen: You permit clobbering A, X, and Y, but you prohibit stack writes (nostack), and you also prohibit clobbering Z&N. In this case, there is no possible code that can utilize A, X or Y while honoring the other constraints, so the request is merged into a more strict file that preserves all registers.
If the vending machine gives you code that clobbers something you did not give permission to clobber, then it is an error that should be reported.