Interlace mode (LSMx=3)

Ask anything your want about Megadrive/Genesis programming.

Moderator: BigEvilCorporation

Near
Very interested
Posts: 109
Joined: Thu Feb 28, 2008 4:45 pm

Interlace mode (LSMx=3)

Post by Near » Sun Aug 20, 2017 9:00 pm

So, I can't find any info on how the 8x16 tile interlace mode (LSMx=3) works. I think this is the last major feature I'm missing.

Continually surpised at how difficult it is to just brute force the answers to these things, so ... here goes.

I presume the nametable height is still 32 or 64 based on the IO bit, and works the same way.

When generating a pixel, assumption is that the tile index goes from y>>3 & nametableHeight-1 to y>>4 & nametableHeight-1. Likewise Vflip is y^15 instead of y^7.

Also assume nametableAddress computation is the same: nametableAddress() + ((tileY * nametableWidth() + tileX) & 0xfff);

Then at rendering, use a field value that toggles each output field (60 fields/second.)

Would assume sprites would just work the same as LSMx=1 and just interlace.

This gets me ...

Image

...... sigh. If it were even at least partially right, I could work with that and tweak things to get it running. But this ... sprites don't even show up, and the tiles look to be being pulled from entirely the wrong locations.

So, ... how exactly does one implement LSMx=3 interlace mode? What exactly internally changes their computations when this mode is enabled?

If it helps, this is my current VDP renderer, sans the recent shadow/highlight support:

https://gitlab.com/higan/higan/blob/mas ... ground.cpp

Also, I assume we don't need any of the I/O register read bits (field#, Vcounter.d0 -> d8, etc) hooked up for Sonic 2, do we? I will add them, of course. But if they're required then I guess I'll do that first.

Thanks again! Hopefully this'll be the last question of mine for a good while ^^;

Sik
Very interested
Posts: 939
Joined: Thu Apr 10, 2008 3:03 pm
Contact:

Re: Interlace mode (LSMx=3)

Post by Sik » Mon Aug 21, 2017 1:12 am

byuu wrote:
Sun Aug 20, 2017 9:00 pm
Continually surpised at how difficult it is to just brute force the answers to these things, so ... here goes.
Lack of community docs isn't helping matters. Feel free to put full blame on that one to me, too much complaining and too little work >_>

Also tiles become 8×16 but their address is calculated exactly the same way (i.e. pattern number × 32). That's probably the part you're screwing up. Learned that one the hard way when I was first messing with interlaced mode ages ago =P

Y coordinates do indeed match the higher resolution (i.e. 448/480px, they get an extra bit). This goes for both tilemaps and sprites (unlike SNES where sprites are not affected). The only other catch is that, well, interlaced mode is interlaced (d'oh) and so even fields show even lines and odd fields show odd lines. But the VDP is working like it's actually high resolution otherwise.


EDIT: actually you know what, I'll have to go recheck what happens when the bottom bit is 1 (i.e. pattern numbers 1, 3, 5, etc.), since I was using an emulator back then and you aren't supposed to use those numbers anyway - no idea if that bit just goes ignored or the whole thing is used. In any case, mapping of bits to address lines is never changed (this is consistent how it handles granurality drops in other portions, e.g. window address and sprite table address between H32 and H40).
Sik is pronounced as "seek", not as "sick".

Near
Very interested
Posts: 109
Joined: Thu Feb 28, 2008 4:45 pm

Re: Interlace mode (LSMx=3)

Post by Near » Mon Aug 21, 2017 11:17 am

Well, I am no saint at writing documentation, either. I try to at least produce emulator code that can be read by other humans (seriously, Genesis VDP is either all x86 ASM or incomprehensible macro abuse with 30 versions of background renderers ... does every Genesis emulator have to run on a toaster?), but I probably don't do the best job there either. It's hard to view one's own code outside the context of their own pre-existing knowledge.

Anway, this is where a couple hours of exhausting every possible code change I could think of got me:

Image

The only thing was a definite clear win was this:

Code: Select all

  uint15 tileAddress = tileAttributes.bits(0,9) << 4;
  if(vdp.io.interlaceMode == 3) tileAddress = tileAttributes.bits(1,10) << 6;
(note that it's actually <<5, I just skipped bit 0 because I was told bit 0 on the tile map entry is ignored in double height mode. I also tried adding io.field<<5 or (y&8)<<5 to tileAddress in interlaceMode==3, but that did not help either.)

This change seems essential to get tiles rendering that look anything even remotely like they are supposed to, but there's still really extensive distortion on the tiles that I just can't get rid of.

And I'm not convinced whether this is helping or hurting ... this change makes the bottom screen position look better, but screws up the top screen:

Code: Select all

  x -= state.horizontalScroll;
  if(vdp.io.interlaceMode == 3) {
    y += state.verticalScroll * 2 + vdp.io.field;
  } else {
    y += state.verticalScroll;
  }
Trying something like y=y+io.field*240 made things way worse, so doesn't seem to be the case of what's going on.

Also, all of my attempts at 8x8->8x16 tiles just make things look worse. So, I don't know what to do there.

Sik
Very interested
Posts: 939
Joined: Thu Apr 10, 2008 3:03 pm
Contact:

Re: Interlace mode (LSMx=3)

Post by Sik » Mon Aug 21, 2017 12:33 pm

byuu wrote:
Mon Aug 21, 2017 11:17 am
The only thing was a definite clear win was this:

Code: Select all

  uint15 tileAddress = tileAttributes.bits(0,9) << 4;
  if(vdp.io.interlaceMode == 3) tileAddress = tileAttributes.bits(1,10) << 6;
(note that it's actually <<5, I just skipped bit 0 because I was told bit 0 on the tile map entry is ignored in double height mode. I also tried adding io.field<<5 or (y&8)<<5 to tileAddress in interlaceMode==3, but that did not help either.)

This change seems essential to get tiles rendering that look anything even remotely like they are supposed to, but there's still really extensive distortion on the tiles that I just can't get rid of.
Wait, how didn't this break everything? The tile ID is 11-bit in normal mode and 10-bit in interlaced mode. There's no way that bits(0,9) would ever work, that only gives you 10 bits. Is tileAttributes just the 16-bit value stored in the tilemap or something else?

I'd have assumed it'd be like this if it's the value taken from the tilemap:

Code: Select all

  uint15 tileAddress = tileAttributes.bits(0,10) << 4;
  if(vdp.io.interlaceMode == 3) tileAddress = tileAttributes.bits(1,10) << 5;
Sik is pronounced as "seek", not as "sick".

Near
Very interested
Posts: 109
Joined: Thu Feb 28, 2008 4:45 pm

Re: Interlace mode (LSMx=3)

Post by Near » Mon Aug 21, 2017 1:13 pm

> Wait, how didn't this break everything? The tile ID is 11-bit in normal mode and 10-bit in interlaced mode. There's no way that bits(0,9) would ever work, that only gives you 10 bits. Is tileAttributes just the 16-bit value stored in the tilemap or something else?

My VRAM is a 16-bit array. 16x32768=64KiB. 0-9 = 10-bits << 5 = 15-bits = 0-32767. I could say (0,10)<<5, but the top bit would just be clipped off anyway, due to not having the full 128KiB VRAM in the Mega Drive.

Sik
Very interested
Posts: 939
Joined: Thu Apr 10, 2008 3:03 pm
Contact:

Re: Interlace mode (LSMx=3)

Post by Sik » Mon Aug 21, 2017 1:55 pm

Erm, there are 2048 tiles (11-bit) in normal mode and 1024 tiles (10-bit) in interlaced mode. That comparison in the code is telling me that the first line only applies when not in interlaced mode, yet it's somehow getting only 10 bits instead of the 11 it'd need to specify a tile.

What is that code even supposed to be doing?
Sik is pronounced as "seek", not as "sick".

Near
Very interested
Posts: 109
Joined: Thu Feb 28, 2008 4:45 pm

Re: Interlace mode (LSMx=3)

Post by Near » Mon Aug 21, 2017 3:42 pm

Ah, you're right, sorry. I made that change while working on a test branch to add interlace support. It's (0,10)<<4 in the main repository.

I think I see now, tile tile# d0 bit is still there, it's just that d0-d10 represents a1-a11 now in interlace mode, eg:

Code: Select all

uint15 tileAddress = tileAttributes.bits(0,10) << (vdp.io.interlaceMode == 3 ? 5 : 4);
Image

Substantially better, but ... now it seems like vertical scrolling is way off. And even on the top frame, the clouds are distorted somewhat and it feels like the screen is a few rows lower than it's supposed to be. As I run, I can often see under the platform Sonic is on.

Here's the current full routine:

Code: Select all

auto VDP::Background::run(uint x, uint y) -> void {
  updateVerticalScroll(x, y);

  x -= state.horizontalScroll;
  y += state.verticalScroll;

  uint width  = nametableWidth();
  uint height = nametableHeight();

  uint tileX = x >> 3 & width  - 1;
  uint tileY = y >> 3 & height - 1;

  auto address = nametableAddress();
  address += (tileY * width + tileX) & 0x0fff;

  uint16 tileAttributes = vdp.vram.read(address);
  uint15 tileAddress = tileAttributes.bits(0,10) << (vdp.io.interlaceMode == 3 ? 5 : 4);
  uint pixelX = (x & 7) ^ (tileAttributes.bit(11) ? 7 : 0);
  uint pixelY = (y & 7) ^ (tileAttributes.bit(12) ? 7 : 0);
  tileAddress += pixelY << 1 | pixelX >> 2;

  uint16 tileData = vdp.vram.read(tileAddress);
  uint4 color = tileData >> (((pixelX & 3) ^ 3) << 2);
  output.color = color ? tileAttributes.bits(13,14) << 4 | color : 0;
  output.priority = tileAttributes.bit(15);
}
I've tried starting with "y = y * 2 + vdp.state.field", or "if(vdp.state.field) y += 240", or doing this to state.verticalScroll instead, or doing it to y after the "y += state.verticalScroll" line, but none of these improved things to any measurable degree.

Is there some secret to how Y works in LSMx=3 mode?

...

Also, I tried extending this logic to sprites:

Code: Select all

    uint tileNumber = tileX * (o.height >> 3) + tileY;
    uint15 tileAddress = o.address + (tileNumber << (vdp.io.interlaceMode == 3 ? 5 : 4));
I still can't get any sprites to render at all in LSMx=3 mode.

Near
Very interested
Posts: 109
Joined: Thu Feb 28, 2008 4:45 pm

Re: Interlace mode (LSMx=3)

Post by Near » Mon Aug 21, 2017 7:29 pm

Sik and I went at this for a while, this was the best cherrypicked result:

Image

The Vscroll is completely busted, and nothing seems to fix it.

The two things I've found so far that are clear wins ... first, as before:

Code: Select all

uint15 tileAddress = tileAttributes.bits(0,10) << (vdp.io.interlaceMode == 3 ? 5 : 4);
And second:

Code: Select all

tileAddress += (2 * pixelY + vdp.state.field) << 1 | pixelX >> 2;
Basically, I'm still treating the VDP like it's rendering 8x8 tiles for the map lookup, but for actual rendering I'm either drawing the even or the odd 8 lines of a given 8x16 tile.

In my main loop, state.vcounter counts from 0-261 (and for rendering, to 223/239), regardless of mode. io.verticalScroll is not adjusted at all for interlace mode. All the interlacing stuff happens in the tile fetching logic.

But, we're both completely stumped at this point so I guess unless someone else has any other ideas... this is the best it's going to get =|

Eke
Very interested
Posts: 884
Joined: Wed Feb 28, 2007 2:57 pm
Contact:

Re: Interlace mode (LSMx=3)

Post by Eke » Mon Aug 21, 2017 11:13 pm

What I think you are missing, as far as I can tell:

- for vscroll, value read from VSRAM must be shifted right by one (VSRAM width is 11-bit but bit0 is ignored in interlaced mode 2) before being added to vcounter value (9-bit)

- for sprites, when preparing the sprites list (i.e looking which sprites will be visible on the next line), sprite ypos value read from SAT cache must also be shifted right by one (value is 10-bit but bit0 is ignored) before being compared/substracted to vcounter value (9-bit)

The reason for that is that virtual screen (the one for scroll planes and the one for sprites) height is actually doubled in interlaced mode 2 (and correspond to full interlaced frame) but the line counter (vcounter) is related to the current field.

Mask of Destiny
Very interested
Posts: 615
Joined: Thu Nov 30, 2006 6:30 am

Re: Interlace mode (LSMx=3)

Post by Mask of Destiny » Tue Aug 22, 2017 12:01 am

Eke wrote:
Mon Aug 21, 2017 11:13 pm
What I think you are missing, as far as I can tell:

- for vscroll, value read from VSRAM must be shifted right by one (VSRAM width is 11-bit but bit0 is ignored in interlaced mode 2) before being added to vcounter value (9-bit)

- for sprites, when preparing the sprites list (i.e looking which sprites will be visible on the next line), sprite ypos value read from SAT cache must also be shifted right by one (value is 10-bit but bit0 is ignored) before being compared/substracted to vcounter value (9-bit)
This doesn't seem quite right as it would imply you don't actually have full resolution scrolling/positioning in this mode. Instead, what you probably want to do is multiply the vcounter value by 2 and add 1 if you're on the "lower" field (I'm uncertain as to whether this is the odd or even field though).

If you go this route though, you will have to adjust the coordinate for the top of the screen for sprites (256 instead of 128).

Near
Very interested
Posts: 109
Joined: Thu Feb 28, 2008 4:45 pm

Re: Interlace mode (LSMx=3)

Post by Near » Tue Aug 22, 2017 12:05 am

... wow, very nice!

Really happy to see you're working off the same 9-bit (0-261) Vcounter for both fields that I am with your examples.

I kept trying to double the VSRAM scroll value ... can't believe I didn't think to try halving it.

Code: Select all

y += state.verticalScroll >> (vdp.io.interlaceMode == 3);
This gets the backgrounds all but perfect (slight shift on the bottom half of the screen, not super worried about that just yet ...)

The sprite change also at least gets sprites visible:

Code: Select all

uint objectY = object.y >> (vdp.io.interlaceMode == 3);

Code: Select all

uint objectY = 128 + y - (o.y >> (vdp.io.interlaceMode == 3));
To get the correct tiledata, I had to make a few more changes:

Code: Select all

object.address = data.bits(0,10);
Don't pre-shift by 4 here at the initial scanline sprite parsing. Instead, do it later in the per-pixel rendering:

Code: Select all

uint15 tileAddress = (o.address + tileNumber) << (vdp.io.interlaceMode == 3 ? 5 : 4);
Use similar interlacing for sprite pixelY as with backgrounds:

Code: Select all

if(vdp.io.interlaceMode == 3) pixelY = pixelY << 1 | vdp.state.field;
tileAddress += pixelY << 1 | pixelX >> 2;
The final issue was that the Y coordinate in the sprite attribute table was 10-bits instead of 9-bits. Without that, sprites on the bottom half of the display didn't show up.

Code: Select all

object.y = data.bits(0,(vdp.io.interlaceMode == 3 ? 9 : 8));
With all of that put together, it's near perfect!

Image

The only remaining issue is that the first few scanlines (4-5 or so) after the screen split seem to be rather glitchy. But that's most likely going to be a much more complicated timing issue =(
[EDIT: actually, Youtube playthroughs seem to indicate it's an issue with the actual game ...? o_O]

Last question, though ... is this correct for counter read-out? Sonic 2 doesn't use it, and I don't know of any other interlace "mode 2" game.

Code: Select all

  //counter
  case 0xc00008: case 0xc0000a: case 0xc0000c: case 0xc0000e: {
    uint16 vcounter = state.vcounter;
    if(io.interlaceMode == 3) vcounter.bit(0) = vcounter.bit(8);
    return vcounter << 8 | (state.hdot >> 1) << 0;
  }
What I'm wondering is if it should be "vcounter = state.vcounter*2+state.field" instead. It seems really kinda useless to put d8 into d0 if it's just 0-261.

Oh actually, two last questions ... in the status register read-out ...

Code: Select all

result |= io.interlaceMode.bit(0) ? state.field << 4 : 0;
Is the field bit always zero when not in an interlace mode? Charles MacDonald's genvdp.txt was really super ambiguously worded here. On the SNES, the field bit toggles even in non-interlace mode.
Last edited by Near on Tue Aug 22, 2017 12:22 am, edited 1 time in total.

Near
Very interested
Posts: 109
Joined: Thu Feb 28, 2008 4:45 pm

Re: Interlace mode (LSMx=3)

Post by Near » Tue Aug 22, 2017 12:11 am

Mask of Destiny wrote:
Tue Aug 22, 2017 12:01 am
This doesn't seem quite right as it would imply you don't actually have full resolution scrolling/positioning in this mode. Instead, what you probably want to do is multiply the vcounter value by 2 and add 1 if you're on the "lower" field (I'm uncertain as to whether this is the odd or even field though).

If you go this route though, you will have to adjust the coordinate for the top of the screen for sprites (256 instead of 128).
Nah, the halving trick worked out beautifully.

The thing I'm being stubborn about is I'm still treating Vcounter as 0-261/262 on each field. I'm not letting it count from 0-524. This means halving the input background VSRAM scroll offset, and halving the input SAT Y coordinate values. As a reuslt of this, I do not have to use 256 instead of 128 for the sprite offset. In the actual tile rendering portion, this is where the Y*2+field logic comes in.

Here's my full sprite rendering code, for reference:

Code: Select all

auto VDP::Sprite::write(uint9 address, uint16 data) -> void {
  if(address > 320) return;

  auto& object = oam[address >> 2];
  switch(address.bits(0,1)) {

  case 0: {
    object.y = data.bits(0,(vdp.io.interlaceMode == 3 ? 9 : 8));
    break;
  }

  case 1: {
    object.link = data.bits(0,6);
    object.height = 1 + data.bits(8,9) << 3;
    object.width = 1 + data.bits(10,11) << 3;
    break;
  }

  case 2: {
    object.address = data.bits(0,10);
    object.horizontalFlip = data.bit(11);
    object.verticalFlip = data.bit(12);
    object.palette = data.bits(13,14);
    object.priority = data.bit(15);
    break;
  }

  case 3: {
    object.x = data.bits(0,8);
    break;
  }

  }
}

auto VDP::Sprite::scanline(uint y) -> void {
  objects.reset();

  uint7 link = 0;
  uint tiles = 0;
  uint count = 0;
  do {
    auto& object = oam[link];
    link = object.link;
    uint objectY = object.y >> (vdp.io.interlaceMode == 3);

    if(128 + y <  objectY) continue;
    if(128 + y >= objectY + object.height) continue;
    if(object.x == 0) break;

    objects.append(object);
    tiles += object.width >> 3;
  } while(link && link < 80 && objects.size() < 20 && tiles < 40 && ++count < 80);
}

auto VDP::Sprite::run(uint x, uint y) -> void {
  output.priority = 0;
  output.color = 0;

  for(auto& o : objects) {
    if(128 + x <  o.x) continue;
    if(128 + x >= o.x + o.width) continue;

    uint objectX = 128 + x - o.x;
    uint objectY = 128 + y - (o.y >> (vdp.io.interlaceMode == 3));
    if(o.horizontalFlip) objectX = (o.width - 1) - objectX;
    if(o.verticalFlip) objectY = (o.height - 1) - objectY;

    uint tileX = objectX >> 3;
    uint tileY = objectY >> 3;
    uint tileNumber = tileX * (o.height >> 3) + tileY;
    uint15 tileAddress = (o.address + tileNumber) << (vdp.io.interlaceMode == 3 ? 5 : 4);
    uint pixelX = objectX & 7;
    uint pixelY = objectY & 7;
    if(vdp.io.interlaceMode == 3) pixelY = pixelY << 1 | vdp.state.field;
    tileAddress += pixelY << 1 | pixelX >> 2;

    uint16 tileData = vdp.vram.read(tileAddress);
    uint4 color = tileData >> (((pixelX & 3) ^ 3) << 2);
    if(!color) continue;

    output.color = o.palette << 4 | color;
    output.priority = o.priority;
    break;
  }
}

Mask of Destiny
Very interested
Posts: 615
Joined: Thu Nov 30, 2006 6:30 am

Re: Interlace mode (LSMx=3)

Post by Mask of Destiny » Tue Aug 22, 2017 12:23 am

byuu wrote:
Tue Aug 22, 2017 12:11 am
Nah, the halving trick worked out beautifully.
It will work, but the resolution of sprite positioning will be half of what it should be. For instance, you won't be able to position a sprite's first line on a line that's in the bottom field (short of modifying the SAT between fields anyway). Since you're also halving the vscroll resolution, everything probably still lines up correctly, but vertical movement will be chunkier.
byuu wrote:
Tue Aug 22, 2017 12:05 am
I don't know of any other interlace "mode 2" game.
Combat Cars also uses interlace mode 2 for split screen multiplayer. To my knowledge, it's the only other commercial release to do so.

Sik
Very interested
Posts: 939
Joined: Thu Apr 10, 2008 3:03 pm
Contact:

Re: Interlace mode (LSMx=3)

Post by Sik » Tue Aug 22, 2017 3:02 am

byuu wrote:
Tue Aug 22, 2017 12:05 am
With all of that put together, it's near perfect!

Image

The only remaining issue is that the first few scanlines (4-5 or so) after the screen split seem to be rather glitchy. But that's most likely going to be a much more complicated timing issue =(
[EDIT: actually, Youtube playthroughs seem to indicate it's an issue with the actual game ...? o_O]
Pretty sure there should be a thick line of background color at the split (about four scanlines or so (forgot the exact amount), since that's how long it takes to reload the whole sprite table with forced blanking). Seems to match the area that is broken.
Sik is pronounced as "seek", not as "sick".

Mask of Destiny
Very interested
Posts: 615
Joined: Thu Nov 30, 2006 6:30 am

Re: Interlace mode (LSMx=3)

Post by Mask of Destiny » Tue Aug 22, 2017 6:46 am

byuu wrote:
Tue Aug 22, 2017 12:05 am
Last question, though ... is this correct for counter read-out? Sonic 2 doesn't use it, and I don't know of any other interlace "mode 2" game.

Code: Select all

  //counter
  case 0xc00008: case 0xc0000a: case 0xc0000c: case 0xc0000e: {
    uint16 vcounter = state.vcounter;
    if(io.interlaceMode == 3) vcounter.bit(0) = vcounter.bit(8);
    return vcounter << 8 | (state.hdot >> 1) << 0;
  }
What I'm wondering is if it should be "vcounter = state.vcounter*2+state.field" instead. It seems really kinda useless to put d8 into d0 if it's just 0-261.
This code is not correct in two ways. First, the weird bit swapping thing happens in both interlace mode (i.e. whenever LSM0=1). Second, double resolution mode (LSMx=3) actually does introduce a new bit 0 to the vcounter and pushes the other bits up one. This effectively results in a rotate left of the 8-bits you normally get in progressive mode. So just swapping the bits is only valid for the LSMx=3 case if you actually shift in the field as bit 0 of the vcounter first.

I actually didn't realize the first of those until I tested it now. It makes the externally visible part of the vcounter kind of useless in single res interlace mode (though single res mode is kind of pointless anyway). I suspect the weirdness is due to the late addition of interlace support. They probably shifted in the field as the new bit 0 someplace early since it's needed in a bunch of places. Shifting things back for the HV counter must have been too many transistors so they just swapped the mostly redundant new bit 0 with bit 8.
byuu wrote:
Tue Aug 22, 2017 12:05 am
Is the field bit always zero when not in an interlace mode?
This bit is forced to zero whenever LSM0=0.

Post Reply