It is currently Sat Nov 18, 2017 5:15 am

All times are UTC - 7 hours





Post new topic Reply to topic  [ 18 posts ]  Go to page 1, 2  Next
Author Message
PostPosted: Tue Jan 17, 2017 4:22 pm 
Offline
User avatar

Joined: Sat Sep 07, 2013 2:59 pm
Posts: 1454
Alright, filling the screen with tiles from compressed data is working in my program.

Now I encounter the problem that I was able to circumvent in my previous game by simply hardcoding the upper static background graphics and by only using one and the same palette for the lower, dynamic part of the screen:

How do I set a palette attribute value if I know the x and y position of the corresponding tiles?

Let me elaborate:

I understand that each attribute value refers to a 16 x 16 pixels (2 x 2 tiles) area. Alright, fair enough.
But why did they also have to align each attribute byte in a square format?

Why couldn't one attributes byte, which contains the palette information for four 16 x 16 areas, simply refer to a 64 x 16 area? Why does it have to be a 32 x 32 area?

This makes connecting the PPU address offset of a certain tile with the PPU address offset of the corresponding attribute value a pain in the ass.

So, is there a good function that can change the attributes value according to an x and y position?

What I need is basically a function like this:

void ChangeAttributeBits(byte x, byte y, byte palette, ref byte[64] attributes);

x: Meta tile x position from 0 to 15. (Tile position would be x * 2 and pixel position would be x * 16.)
y: Same as x, only for y.
palette: Palette index value from 0 to 3.

attributes:
An array that is a copy of the 64 attribute bytes from a single name table from the PPU.
This value gets changed:
The palette index value is written to the correct two bits in this array. So, after writing the changed array back into the PPU to $23C0, $27C0, $2BC0 or $2FC0, the 2 * 2 tiles at location x * 16, y * 16 use this new palette value.

_________________
Available now: My game "City Trouble".
Website: https://megacatstudios.com/products/city-trouble
Trailer: https://youtu.be/IYXpP59qSxA
Gameplay: https://youtu.be/Eee0yurkIW4
German Retro Gamer article: http://i67.tinypic.com/345o108.jpg


Top
 Profile  
 
PostPosted: Tue Jan 17, 2017 4:32 pm 
Offline
User avatar

Joined: Sun Jan 22, 2012 12:03 pm
Posts: 5822
Location: Canada
So if you have X and Y as 16x16 pixel tile locations:

X/2 and Y/2 give you the 32x32 attribute region location.
X mod 2 and Y mod 2 give you the 4 quadrants of that location (i.e. which bits to select)

Since there are 8 attribute regions per row: ((Y/2)*8) + (X/2) = attribute byte offset


Alternatively if you have a PPU tile address and want to convert to its corresponding attribute, you can use the formula here: PPU_scrolling#Tile_and_attribute_fetching


Top
 Profile  
 
PostPosted: Tue Jan 17, 2017 6:15 pm 
Offline
User avatar

Joined: Sat Feb 12, 2005 9:43 pm
Posts: 10112
Location: Rio de Janeiro - Brazil
DRW wrote:
Why couldn't one attributes byte, which contains the palette information for four 16 x 16 areas, simply refer to a 64 x 16 area? Why does it have to be a 32 x 32 area?

An uniform distribution makes more sense, I guess, and several games took advantage of the format by using 32x32-pixel metatiles. There's isn't much you can do with 64x16.

Quote:
This makes connecting the PPU address offset of a certain tile with the PPU address offset of the corresponding attribute value a pain in the ass.

It's indeed a pain in the ass, specially if you scroll vertically and have to deal with the bottomost attribute row being only 16 pixels high.

Quote:
So, is there a good function that can change the attributes value according to an x and y position?

The math I use is the one rainwarrior posted. Divide the coordinates by 2 to find the address of the attribute byte, use mod 2 to index a set of masks that can be used to manipulate the bits.

For each possible quadrant of an attribute byte, I have a pair of masks, one to clear the relevant bits in a pre-existing attribute byte, and another (the inverse of the other) to clear the unwanted bits of a byte fetched from the metatile's attributes that contains the palette index repeated 4 times. After clearing the bytes separately, I OR them together to form the final byte.


Top
 Profile  
 
PostPosted: Tue Jan 17, 2017 7:05 pm 
Offline

Joined: Wed May 19, 2010 6:12 pm
Posts: 2357
You can also mask the bits this way.

lda attribute_byte
eor attribute_data
and mask
eor attribute_byte
sta attribute_byte


Top
 Profile  
 
PostPosted: Tue Jan 17, 2017 7:57 pm 
Offline
User avatar

Joined: Sat Feb 12, 2005 9:43 pm
Posts: 10112
Location: Rio de Janeiro - Brazil
psycopathicteen wrote:
You can also mask the bits this way.

lda attribute_byte
eor attribute_data
and mask
eor attribute_byte
sta attribute_byte

Nice trick! You only need one mask and there's no need for a temporary variable. Here's an example of how this works:

Code:
;%00111011 (attribute_byte)
;%11111111 (attribute_data) (%11 4 times)
;%00001100 (mask)

lda attribute_byte ;%00111011
eor attribute_data ;%11000100
and mask           ;%00000100
eor attribute_byte ;%00111111
sta attribute_byte


Top
 Profile  
 
PostPosted: Wed Jan 18, 2017 9:55 am 
Offline

Joined: Wed May 19, 2010 6:12 pm
Posts: 2357
I think you can also flag the attribute bytes to update so you don't end up rewriting the same tiles more than once during vblank.


Top
 Profile  
 
PostPosted: Wed Jan 18, 2017 10:12 am 
Offline
User avatar

Joined: Sat Feb 12, 2005 9:43 pm
Posts: 10112
Location: Rio de Janeiro - Brazil
With attributes, since you have to read-modify-write each byte, it's often more convenient to keep a copy of the attribute data in RAM for unrestricted manipulation, and then upload rows or columns to VRAM as necessary.


Top
 Profile  
 
PostPosted: Wed Jan 18, 2017 3:22 pm 
Offline
User avatar

Joined: Sat Sep 07, 2013 2:59 pm
Posts: 1454
rainwarrior wrote:
X/2 and Y/2 give you the 32x32 attribute region location.
X mod 2 and Y mod 2 give you the 4 quadrants of that location (i.e. which bits to select)

Since there are 8 attribute regions per row: ((Y/2)*8) + (X/2) = attribute byte offset

Thanks. This was exactly what I was looking for.

I also included the EOR hint by psycopathicteen and this is the C function that I came up with:

Code:
byte Attributes[8 * 8];

/* x: 0-15. y: 0-14. paletteIndex: 0-3. */
void ChangeAttributeBits(byte x, byte y, byte paletteIndex)
{
    byte attributesIndex = ((y >> 1) << 3) | (x >> 1);
    byte attribute = Attributes[attributesIndex];

    Attributes[attributesIndex] =
        ((attribute ^ 0xFF) & (paletteIndex << ((((y & 1) << 1) | (x & 1)) << 1))) ^ attribute;
}

Apart from the fact that on the NES, you should rather work with static and global variables instead of local ones, would you say the function is efficient enough or is there something that can be optimized?

_________________
Available now: My game "City Trouble".
Website: https://megacatstudios.com/products/city-trouble
Trailer: https://youtu.be/IYXpP59qSxA
Gameplay: https://youtu.be/Eee0yurkIW4
German Retro Gamer article: http://i67.tinypic.com/345o108.jpg


Top
 Profile  
 
PostPosted: Thu Jan 19, 2017 6:10 am 
Offline

Joined: Tue Oct 06, 2015 10:16 am
Posts: 581
Not in C, but in asm certainly. I think my equivalent function got four times faster and half the space when asm-optimized.


Top
 Profile  
 
PostPosted: Thu Jan 19, 2017 7:41 am 
Offline
User avatar

Joined: Sat Sep 07, 2013 2:59 pm
Posts: 1454
Yeah, Assembly is surely better.
But I managed to write a complete scrolling platformer in C with only the low level stuff (everything that is in vblank, access to the PPU etc.) and some general purpose functions (copy array, fill array, randomizer) written in Assembly.
Therefore, unless I really get into trouble, I continue writing the functions in C for now. Makes it 10 times easier for me to write code.

_________________
Available now: My game "City Trouble".
Website: https://megacatstudios.com/products/city-trouble
Trailer: https://youtu.be/IYXpP59qSxA
Gameplay: https://youtu.be/Eee0yurkIW4
German Retro Gamer article: http://i67.tinypic.com/345o108.jpg


Top
 Profile  
 
PostPosted: Thu Jan 19, 2017 4:47 pm 
Offline
User avatar

Joined: Sat Sep 07, 2013 2:59 pm
Posts: 1454
Code:
byte Attributes[8 * 8];

/* x: 0-15. y: 0-14. paletteIndex: 0-3. */
void ChangeAttributeBits(byte x, byte y, byte paletteIndex)
{
    byte attributesIndex = ((y >> 1) << 3) | (x >> 1);
    byte attribute = Attributes[attributesIndex];

    Attributes[attributesIndex] =
        ((attribute ^ 0xFF) & (paletteIndex << ((((y & 1) << 1) | (x & 1)) << 1))) ^ attribute;
}

I just found out that my above function produces incorrect results in certain constellations.

So, either I didn't implement psycopathicteen's suggestion correctly or the algorithm with the EOR itself is incorrect.

The following function seems to work correctly:
Code:
void ChangeAttributeBits(byte x, byte y, byte paletteIndex)
{
   byte attributesIndex = ((y >> 1) << 3) | (x >> 1);
   byte shifts = (((y & 1) << 1) | (x & 1)) << 1;

   Attributes[attributesIndex] =
      (Attributes[attributesIndex] & (~(0x03 << shifts))) | (paletteIndex << shifts);
}

If someone is interested, maybe he likes to check the error in the old function and whether it's just my implemenation or the whole idea of using EOR here.

_________________
Available now: My game "City Trouble".
Website: https://megacatstudios.com/products/city-trouble
Trailer: https://youtu.be/IYXpP59qSxA
Gameplay: https://youtu.be/Eee0yurkIW4
German Retro Gamer article: http://i67.tinypic.com/345o108.jpg


Top
 Profile  
 
PostPosted: Thu Jan 19, 2017 11:38 pm 
Offline
User avatar

Joined: Sun Jan 22, 2012 12:03 pm
Posts: 5822
Location: Canada
Looking at the two ends of your equation, and thinking transitively:

Code:
// original
((attribute ^ 0xFF) & (paletteIndex << ((((y & 1) << 1) | (x & 1)) << 1))) ^ attribute;

// simplified (mask = the whole & term)
((attribute ^ 0xFF) & mask) ^ attribute;

// some truths about XOR
(attribute ^ attribute) = 0x00;
0x00 ^ 0xFF = 0xFF;
(attribute ^ 0xFF) ^ attribute = 0xFF;

// what the statement really does
((attribute ^ 0xFF) & mask) ^ attribute = mask | attribute;


All your mask does here is select which bits to set. It's incapable, however, of clearing any bits. You need to do both to replace the palette index.

i.e. paletteIndex of 3 will work, 1/2 will set one correct bit and fail to clear the other, and 0 will fail to clear either bit.

(By the way, you can use the bitwise complement operator ~a as a shorter form of a ^ 0xFF.)


Top
 Profile  
 
PostPosted: Fri Jan 20, 2017 12:09 am 
Offline
User avatar

Joined: Sat Sep 07, 2013 2:59 pm
Posts: 1454
rainwarrior wrote:
Code:
// simplified (mask = the whole & term)
((attribute ^ 0xFF) & mask) ^ attribute;

[...]

All your mask does here is select which bits to set. It's incapable, however, of clearing any bits.


O.k., but where exactly is the error? I thought that I simply implemented a C version of the following code:
tokumaru wrote:
Code:
;%00111011 (attribute_byte)
;%11111111 (attribute_data) (%11 4 times)
;%00001100 (mask)

lda attribute_byte ;%00111011
eor attribute_data ;%11000100
and mask           ;%00000100
eor attribute_byte ;%00111111
sta attribute_byte


Isn't this exactly the same?

lda attribute_byte
eor attribute_data

-->
attribute = Attributes[attributesIndex]
attribute ^ 0xFF


and mask
-->
& mask

eor attribute_byte
-->
^ attribute;

sta attribute_byte
-->
Attributes[attributesIndex] =

So, is it my code that's still in error because I didn't convert the algorithm correctly from Assembly to C? Or is the algorithm itself as written down by psychopaticteen and tokumaru that's incorrect/insufficient?

_________________
Available now: My game "City Trouble".
Website: https://megacatstudios.com/products/city-trouble
Trailer: https://youtu.be/IYXpP59qSxA
Gameplay: https://youtu.be/Eee0yurkIW4
German Retro Gamer article: http://i67.tinypic.com/345o108.jpg


Top
 Profile  
 
PostPosted: Fri Jan 20, 2017 1:10 am 
Offline
User avatar

Joined: Sun Jan 22, 2012 12:03 pm
Posts: 5822
Location: Canada
Code:
attribute                      // lda attribute_byte
^ (palette << shift)           // eor attribute_data
& (3 << shift)                 // and mask
^ attribute                    // eor attribute_byte
                               // sta attribute_byte

result = ((attribute ^ (palette << shift)) & (3 << shift)) ^ attribute;

If you want to compare it to what your second statement would have looked like in the same form, here's the reverse transformation for that:

Code:
result = (attribute & (~(3 << shift))) | (palette << shift);

lda mask                       ;
eor #$FF                       ; ~ (3 << shift)
and attribute_byte             ; & attribute
ora attribute_data             ; | (palette << shift)
sta attribute_byte             ;


We're probably comparing apples to oranges here, there's a lot of unspoken overhead here about how you prepare the mask/shift/palette values, and that's all part of doing it efficiently.


Last edited by rainwarrior on Fri Jan 20, 2017 1:29 am, edited 3 times in total.

Top
 Profile  
 
PostPosted: Fri Jan 20, 2017 1:27 am 
Offline
User avatar

Joined: Sat Sep 07, 2013 2:59 pm
Posts: 1454
rainwarrior wrote:
Code:
^ (palette << shift)           // eor attribute_data

Now you totally lost me.

Why does eor attribute_data transform to ^ (palette << shift) instead of ^ 0xFF?

In the example, tokumaru declared attribute_data as %11111111:
tokumaru wrote:
Code:
;%11111111 (attribute_data) (%11 4 times)

So I assumed it's a constant for negation.

Even if attribute_data was a variable, how could palette << shift have ever been %11111111 in the first place if the palette value is only a value from 0 to 3?

I'm confused.

_________________
Available now: My game "City Trouble".
Website: https://megacatstudios.com/products/city-trouble
Trailer: https://youtu.be/IYXpP59qSxA
Gameplay: https://youtu.be/Eee0yurkIW4
German Retro Gamer article: http://i67.tinypic.com/345o108.jpg


Top
 Profile  
 
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 18 posts ]  Go to page 1, 2  Next

All times are UTC - 7 hours


Who is online

Users browsing this forum: Bing [Bot] and 11 guests


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Jump to:  
Powered by phpBB® Forum Software © phpBB Group