The video player: Optimized VRAM writes

The data in the diffs is applied onto the right page, and needs to be done as fast as possible. To do this I use a technique similar to the one used in my OPL4 replayer, described in the O4S replayer. The main difference is that we do not have one stream this time, but two. One for control codes and one for data. This is slightly inferior to the O4S performance-wise, but the video format/player was made first.

In short:

  1. Using the stack for fast streaming/reading
  2. Using absolute jump addresses in data stream (with implicit embedding of data write lengths)
  3. Unrolled OUTIs for fastest possible writing

As this is about writing directly to the VRAM using the VDP port, the data is filled in, starting from the given VRAM address, and continues in linear fashion, left to right, top to bottom. VDP block copy from RAM is not used.

The diff image is scanned and all individual sequences of lines are identified as chunks, like the pink outlines below. The amount of chunks is massive.

If a CODE_FRAME_UPDATE or a CODE_FRAME_SET_LOWER_UPDATE code appears in the control stream, the format is like this (bytes):

<code><number of chunks><chunk 0><chunk n-1> ...

One chunk comes like this:

<vram_address><jump_address>

The VRAM address is a precomputed VRAM write address in the 0x4000 range. These two bytes are just OUT’ed to port 0x99 directly, which is very fast. The data to OUT to the VDP is found in the data stream.

Here is the source for grabbing the code from the stream, showing the optimized inner loop for the CODE_FRAME_UPDATE:

To follow the code, keep in mind that SP points into the control stream on entry; the two subsections below explain where PC ends up when that ret is hit.

The difference between CODE_FRAME_UPDATE and a CODE_FRAME_SET_LOWER_UPDATE is only that the latter sets VDP #14 to point to the lower end of the screen (line 128 and below) before doing exactly the. I’m saving space by not including the full VRAM address in every chunk.

Chunks very close to each other are joined by the encoder as long as the result renders faster and does not impose bigger data sizes.

Inverting the palette

When the encoder detects that a frame diff results in a change in more than 50% of the pixels (which, with lots of details, will not be possible to write to VRAM over our 4 frames), the encoder inverts the palette and (implicitly) the source data on the encoder side. This way we can handle drastic changes, like full screen blinks and similar. The bigger the change, the cheaper the rendering during such a frame. This is triggered by a single code in the control stream: CODE_FRAME_INVERT_NEXT

Stream of data

Most of the time we have a data stream with variable data values, and the OUTI instruction does its job. The block where the JP (just an address in the chunk message) jumps to, looks like this in memory:

outi
outi
outi

outi
outi
outi
  
exx      ; restore return address in HL
jp (hl)  ; can’t use ret when when our data stream is the stack

As the encoder knows where this block is in (static) memory, it can calculate where to jump to pump the right amount of bytes to the VDP in the fastest possible way, with no counters or calculations at runtime.

Same color data

The same idea applies here. But if the encoder detects that a run of data being sent to the VDP consists of identical values, it tries to reduce the storage size. Once again by putting all the logic into the encoder, so the renderer itself doesn't need to know the difference. All repeating pixel update parts, regardless of size, will only need 2 or 3 bytes of data in the data stream.

The block where the JP command (just an address in the chunk message) jumps to, for odd lengths, looks like this:

outi, outd
outi, outd
outi, outd

outi, outd
outi, outd
outi, outd

outi
outi
outi     ; last outi ensures it points to next data

exx      ; restore return address in HL
jp (hl)  ; can’t use ret when when our data stream is the stack

The block where the JP command (just an address in the chunk message) jumps to, for even lengths, looks like this:

outi, outd
outi, outd
outi, outd
outi, outd
outi, outd
outi, outd

outi
outi     ; last outi ensures it points to next data

exx      ; restore return address in HL
jp (hl)  ; can’t use ret when our data stream is the stack




< Frame scheme | Optimized VRAM writes | VDP command engine >

Comments