The last post covered Flexible Line Distance, a technique that the VIC-II wasn’t really intended to support, but did anyway. If you look at the chip’s mechanism for generating text displays, FLD does sort of naturally fall out of it.
This post will cover three techniques that violate things the VIC-II thinks ought to be invariants. This puts the chip into unusual states and a few surprising things happen as a result. All of these techniques involve cases where a badline does not actually last the complete line it’s intended to cover. Whether something is a badline is checked continuously, so if the chip’s logic says “wait for a badline to happen, then do this thing” it can be made to happen earlier or later.
Sometimes the design of the chip can withstand timing variations, but sometimes it can’t. These techniques all have dramatic effects and they also produce some kind of glitch.
- Linecrunch. If we’ve just finished a line of text, and thus have gone idle pending the next text line, and then we let the badline start at the beginning of that next text line, but then interrupt it by moving the next line further down before it begins, the chip gets confused, redisplays the eighth pixel line of the previous line of text, but advances its text pointer 40 characters as if it had just displayed a complete line. Effect: Stretching out a previous line of text, or gobbling up data you want to hide or scroll up. Glitch: You’ll blow off the end of screen memory partway through and wrap around back to the top, horizontally shifted a bit, and with some garbage data around where sprite pointers are kept.
- Partial Rescan. This doesn’t have a formal name. It’s a core technique in a variety of effects we’ll get to once we master our tools, but in conversations and article reading I haven’t seen a consistent name for it, so I’m going to name it Partial Rescan. Here we assert that the line we’re on is a badline and should be starting a new line of text even though we’re in the middle of drawing some other text line right now. Effect: It rereads the character data from the point we changed the scroll value on from screen memory. Normally the text matrix is only checked on a badline, and normally a badline also kicks us back to the top of the character. However, because we started late, the VIC-II doesn’t get around to actually resetting which pixel row in the character is changed. We can display a character whose top half is an A and whose bottom half is a B without custom fonts. Another fun effect is that this stabilizes your raster: no matter what model C64 you have, and no matter when you trigger the badline, the VIC immediately takes control away from the CPU and stuns it until the end of the line. Thus, no matter when you start a partial rescan, you finish it on cycle 54.Glitch: Data to the left of the point we rewrote the scan value is not updated because the rescan starts at the character being rendered at the time. Also, the three characters after the right are full of garbage because it takes three cycles for the VIC-II to take control of the bus.
- HSP. Partial rescan correctly places the rescanned characters because we’re displaying characters already and it’s thus kept count of where it’s supposed to be. If we trigger a partial rescan while idle—because we haven’t started our characters yet or because we’ve introduced a gap by scrolling down mid-line—it hasn’t been keeping track. This means the line begins partway across the screen. Effect: The whole screen is pushed right by some number of characters depending on which cycle you triggered it on. It wraps around by the second pixel line. Glitch: It corrupts your RAM contents. This is the Forbidden Technique. Interrupting the VIC-II in this way can confuse the DRAM refresh circuitry. This can be mitigated, somewhat, but this is a lot of work and it’s work we aren’t doing here.
When HSP is used, it’s generally as an effect in its own right. Linecrunch can be used as such an effect, but it and partial rescan are most commonly used to build real effects out of. They’re closer to circuit elements than circuits themselves. As such, I won’t be doing my usual what it is/what’s really happening/how to do it breakdown; here we start with “how you do it” and that leads to “here is what happens”. When we delve into techniques like FLI and VPP and text stretching, we will revisit these techniques and include them directly.
Our test programs
Our goal here is really more proof of concept rather than doing something cool with the technique. As such, all three test programs here will be similarly structured. We’ll start with our header:
.word $0801 .org $0801 .word start, 2015 .byte $9e, " 2062",0 start: .word 0
Then we’ll fill
$3c00-$3fff with the number 2, so that if we point the screen memory to it we’ll get a screen full of Bs. This will make it obvious when we’ve rescanned a line; either we’ll see text we expect to see, or we’ll see a bunch of Bs.
lda #$02 ldx #$00 * sta $3c00,x sta $3d00,x sta $3e00,x sta $3f00,x dex bne - lda #$00 sta $3fff
After that, we’ll set up an screen interrupt at raster zero and return to BASIC. We can thus experiment with displays by typing stuff out moving the cursor around and reset the state with RUN-STOP/RESTORE.
lda #$7f sta $dc0d lda #$1b sta $d011 lda #$00 sta $d012 lda #<irq sta $314 lda #>irq sta $315 lda #$01 sta $d01a rts
Preliminaries now established, we can move on to the three techniques.
To execute Linecrunch, our constraints are as follows:
- The previous line of text completed normally. (We may have spent some scanlines after this being idle, but the important thing is that we aren’t in the middle of displaying a line of text.)
- We are at the beginning of a badline right now; either naturally via the advance of the raster display, or unnaturally by writing $d011 just now. The signal must be up for at least one cycle to put us into display state, so this must be happening between cycles 1 and 8. (A “natural” badline may be considered to be set at cycle 0 automatically with no effort by us.)
- We must cancel this badline before it is committed, which means we must rewrite
$D011by cycle 12 at the latest. Starting the write at cycle 8 will ensure that for us.
Since we’re dealing with behavior at the beginning of a line, if we’re going to rely on IRQs to do this, we’re going to need to start one line previous and then count out that line as well. That will tighten our tolerances a bit if we want to work on both PAL and NTSC. The case with the least delay for us means we are waiting 65 (new NTSC scanline) + 1 (target cycle) – 38 (fastest IRQ processing time = write on cycle 28 or later. Doing the write before then will mean that we never spent any time with the badline condition valid, which moves us into having executed an FLD-like technique instead. The longest safe delay for us is 63 (PAL scanline) + 12 (last safe cycle) – 44 (slowest IRQ processing time) = write on cycle 31 or earlier. That’s relatively tight timing, and if we want to do this more than once, raster drift will ruin us very quickly. We can still sneak in two consecutive crunches without any stabilization or model-checking code, though:
- Trigger our IRQ the line before a badline.
$D01130 cycles after the IRQ begins. This leaves us at least one cycle of breathing room on each side.
$D011again after 64 cycles, causing us to drift at most one cycle in either direction (one cycle later on PAL, one cycle earlier on new NTSC, perfect match on old NTSC). This keeps us in the safe zone, barely.
- Once the screen display is complete, reset the YSCROLL value to 3.
Here’s some code that does that:
irq: lda #$01 ; 2 (Acknowledge interrupt) sta $d019 ; 6 bit $d012 ; 10 (Top half of screen?) bvc top_irq ; 12 (if so, prepare for midscreen case) lda #$01 ; 14 (New IRQ raster) sta $d012 ; 18 nop ; 20 nop ; 22 nop ; 24 inc $d011 ; 30 (Trigger linecrunch!) ldx #11 ; Burn 58 cycles * dex bne - nop inc $d011 ; Do it again, losing one cycle (PAL) or gaining one (new NTSC) ;; Process timer interrupt, if any lda $dc0d beq notime jmp $ea31 top_irq: lda #$1b ; Fix scroll sta $d011 lda #$62 ; And trigger just before a badline sta $d012 notime: jmp $febc ; Never do timer IRQ here
One wrinkle here is that we’re splitting on scanline
$62. We’re putting it there because that’s in the middle of the LOADING-READY-RUN text you’d get if loading this program off a disk right after power-on, so that makes our results more dramatic. It does, however, introduce a minor problem in that it’s less than 128, which means we can’t just check the sign of the raster count. We instead use the
BIT instruction, which copies bits 6 and 7 into the overflow and sign flags. This means we can check bit 6 with
BVS. We must simply make sure that bit 6 isn’t set on our “top of screen” raster count, though this is simple enough to ensure.
When we run this program, we get this screen:
Two lines have been consumed from the system text, replaced with two apparently blank lines. They’re not completely blank, though. If we switch the display to lowercase…
… we can see from the “y” in “ready” that it’s actually replicating the last line of pixels there while devouring its line of new text.
Also of note is that garbage at the bottom of the screen. That’s actually replicating the first line of text up up the top—Once the pointer reaches location 999 (the last character on the screen), it just keeps going and dumps random junk, the sprite pointers, and then starts over, rolling from 1023 back to zero in the middle of the line. If you want you can go move the cursor up to the top of the screen and type stuff in to watch it be mirrored.
The constraints for a partial rescan are a lot simpler. We have to be in the middle of drawing a line of characters, and also be in the middle of drawing the character graphics (that is, we aren’t off in HBLANK or drawing the border). That means we’re at cycle 14 or later on the line. Our cycle constraint int he routine is thus that we must write at 65 New NTSC scanline + 14 (target cycle) – 38 (fastest IRQ) = cycle 41 or later. We’re mid-draw here, so this will produce ugly artifacting. As a test program, though, we actually want that artifacting, so we can see what’s happening.
We start our IRQ routine by handling the top-of-screen case, turning on the screen full of Bs and then preparing a the mid-screen IRQ:
irq: lda #$01 ; 2 (Acknowledge Interrupt) sta $d019 ; 6 bit $d012 ; 10 (Check for midscreen) bvs split ; 13 lda #$06 ; Top of screen: fix background color sta $d021 ldx #$1b ; Fix scroll ldy #$f4 ; Set the video matrix to the all-B case lda #$76 ; And set the mid-screen interrupt stx $d011 sty $d018 bne done
Then, in the splitscreen case, we need to properly set up our write. We write
$D018 extremely early. Normally this would not take effect until the next badline, but that would restart the character from the first line again. (We saw this in action in the vertical split screen project.). What we hope to find here are strange chimera letters that are half B and half something else.
split: lda #$14 ; 15 sta $d018 ; 19 lda #$1f ; 21 ldx #$03 * dex bne - ; 36 sta $d011 ; 41
Once we make that write, we’re immediately stunned by the VIC-II chip, which proceeds to start rescanning graphics data and then hands control back at the end of the line. It’s time to clean up for the next frame. We write the initial scan value back on cycle 65…
nop nop nop lda #$1b sta $d011
And then, just to prove that this raster is, in fact, stable, we wait a while to make sure we’re a bit down from the rescan and then change the background color. We should expect the graphical artifacting at the top to jitter left and right, but we expect this color change to be rock-solid.
ldx #$04 * dex bne - inc $d021
After that, it’s just cleanup, for both top and mid-screen IRQs.
lda #$00 done: sta $d012 lda $dc0d beq notim jmp $ea31 notim: jmp $febc
The result is dramatic, and kind of ugly:
The checkerboard part is unstable. Those are what the VIC-II sees on the bus when it’s spending three cycles taking the bus from the CPU, but thinks it should be reading character data anyway. As you can confirm with a
POKE 2023, 255 command, this is actually it interpreting its high-impedance state as the value
$ff. Since characters are read every badline, the misreads stick around for the remaining scanlines in that character. Also, we can see that while the checkerboard effect is unstable, the background color change is not.
The other interesting aspect of this is that since we fixed YSCROLL before the character we interrupted ended, the display state remains active for the entirety of the screen. We don’t have any idle lines.
Horizontal Screen Positioning
Unlike our previous test program for partial rescan, now we want idle lines. We also want a stable raster, which the partial rescan test program gives us in a sort of ugly way. We’ll need to get to proper raster stabilization techniques eventually, but for now we can just modify our previous program. We’ll strip out the code that messes with the background color, and also the part that fixes the scroll.
Once we do our write at cycle 14+, we are transported to cycle 54 on that raster line. This is the raster line after our IRQ line—
$77. This means our YSCROLL value is now 7, as well. We started with a YSCROLL of 3, and so this means that lines
$7E will be idle. If we in fact set YSCROLL back to 3 during scanline
$7B, that will not only trigger the Horizontal Screen Positioning effect, it will also fix our scroll. That’s pretty great.
Doing that is pretty simple. We’re near the end of line
$77, so we want to wait approximately 3.5 scanlines worth of cycles so we’re in the middle of scanline
$7b We need consistency for a stable display, but following up from the partial rescan means we have that. Accuracy is less important, so we note that 3.5 scanlines is roughly 64*3.5 = 224 cycles, and the easiest loop we can get near that is 45 cycles through our spinloop ((226-1)/5 = 45 iterations).
We thus replace everything between the write on cycle 41 and the cleanup stanza with a delay loop and a single write:
ldx #$2d * dex bne - lda #$1b sta $d011
The result, at least in emulation, is immediate and clear:
Running this on real hardware may eventually cause it to reset (as per a RUN-STOP/RESTORE), corrupt screen memory, or hang. I refer you to Lft’s 2013 demo for full details on how to tame it in the general case. We didn’t do any of that here.
The three test programs here are available in full source form on my Github. The three files you want are
At this point in our journey through the secrets of the VIC-II, we’ve actually hit every “abusive” technique that was commonly used. Everything else from here on it is merely shrewd deployment of the capabilities we have been given.