vram_write() and vram_read() test their length counter after the transfer body, so a size of 0 underflows to $ffff and the routine transfers 64 KB. The header documents no minimum length.
The loop
_vram_write (neslib.sinc L769-L780) and _vram_read (neslib.sinc L731-L742) share this tail:
@2:
lda <TEMP
bne @3
dec <TEMP+1
@3:
dec <TEMP
lda <TEMP
ora <TEMP+1
bne @1
Entering with TEMP/TEMP+1 both 0, one byte has already been transferred before the counter is looked at. lda <TEMP is 0, so dec <TEMP+1 takes the high byte to $ff and dec <TEMP takes the low byte to $ff. ora is non-zero, so the loop runs another 65535 times.
Effect
vram_write(src, 0) — 65536 stores to $2007. The source pointer wraps the address space, so this sprays 64 KB of arbitrary bytes into VRAM.
vram_read(dst, 0) — 65536 loads from $2007 stored through dst, which wraps and overwrites all of RAM including the 6502 stack at $0100-$01ff. The routine's own return address is destroyed, so it never returns.
Measurements
I linked a minimal ROM against the library (cc65 2.19, nes.cfg from the cnrom sample) and ran the resulting binary, counting accesses to $2007:
vram_write(src, size=5) -> 5 PPU_DATA writes (sanity check)
vram_write(src, size=0) -> 65536 PPU_DATA writes
vram_read(dst, size=0) -> never returns; clobbers $0000..$7fff,
including the stack at $0100-$01ff
Sizes 1, 2, 5, 255, 256, 257, 960, 1024 and 4096 all transfer correctly. Only 0 is affected.
Request
The comment above these functions covers the rendering requirement but says nothing about length. A computed length can legitimately be 0 at a call site — a variable-length string that happens to be empty, for instance — so it would help to either document that size must be >= 1, or reject it. Documenting it is fine by me; what makes this one awkward is that the failure is silent and lands nowhere near its cause.
If a guard is preferred instead, popping the argument first keeps the cc65 stack balanced:
jsr popax
sta <TEMP+2
stx <TEMP+3
+ lda <TEMP
+ ora <TEMP+1
+ beq @4
+
ldy #0
@1:
@@
ora <TEMP+1
bne @1
+@4:
+
rts
The same applies to _vram_read, placed before the priming lda PPU_DATA. With it applied both zero cases become 0 transfers and the sizes listed above are unchanged.
This looks inherited from the original rather than introduced here — nesdoug's fork carries the identical loop.
vram_write()andvram_read()test their length counter after the transfer body, so asizeof 0 underflows to $ffff and the routine transfers 64 KB. The header documents no minimum length.The loop
_vram_write(neslib.sinc L769-L780) and_vram_read(neslib.sinc L731-L742) share this tail:Entering with
TEMP/TEMP+1both 0, one byte has already been transferred before the counter is looked at.lda <TEMPis 0, sodec <TEMP+1takes the high byte to $ff anddec <TEMPtakes the low byte to $ff.orais non-zero, so the loop runs another 65535 times.Effect
vram_write(src, 0)— 65536 stores to $2007. The source pointer wraps the address space, so this sprays 64 KB of arbitrary bytes into VRAM.vram_read(dst, 0)— 65536 loads from $2007 stored throughdst, which wraps and overwrites all of RAM including the 6502 stack at $0100-$01ff. The routine's own return address is destroyed, so it never returns.Measurements
I linked a minimal ROM against the library (cc65 2.19,
nes.cfgfrom the cnrom sample) and ran the resulting binary, counting accesses to $2007:Sizes 1, 2, 5, 255, 256, 257, 960, 1024 and 4096 all transfer correctly. Only 0 is affected.
Request
The comment above these functions covers the rendering requirement but says nothing about length. A computed length can legitimately be 0 at a call site — a variable-length string that happens to be empty, for instance — so it would help to either document that
sizemust be >= 1, or reject it. Documenting it is fine by me; what makes this one awkward is that the failure is silent and lands nowhere near its cause.If a guard is preferred instead, popping the argument first keeps the cc65 stack balanced:
The same applies to
_vram_read, placed before the priminglda PPU_DATA. With it applied both zero cases become 0 transfers and the sizes listed above are unchanged.This looks inherited from the original rather than introduced here — nesdoug's fork carries the identical loop.