Jump to content
IGNORED

Playfield procedural drawing, compression


iesposta

Recommended Posts

The lines to be written I called ldat
I put ldat into a single table because
I thought it would be easier to count
through bytes than select amongst tables
and I didn't want to duplicate the bit
selection code for each table (since the
aim is compression)

So it counts by (playfield) rows
if the row is even we get the next two
nibbles of pointers, mask for the correct
bits and divide by 4 (leaving room to count
4 bytes of the line) if the row is odd mask
and multiply by 4

Bits are selected by shifting a 1 in cbits
as a mask which is also the bit counter (for
the 8 bits in each byte)

The 4 bytes of a line are counted by incrementing
ldat_ptr, masking for the lower two bits and
checking if they've rolled over to 0

col, the playfield columns, is incremented for
each bit and reset to 0 each time we start a row
8 bits/byte * 4 bytes/line

untested

edit oops had the current bit test wrong

auxillary edit
row and col reversed in pfpixel
moved col reset outside the byte loop
fixed the line data

data L4p
$01, $21, $21, $21, $21, $21, $21, $21
$21, $21, $21, $21, $21, $21, $32, $45
$66, $65, $22, $32, $22, $32, $22, $32
$22, $32, $22, $32, $22, $32, $72, $32
$44, $86, $66, $82, $27, $22, $27, $22
$27, $22, $27, $22, $27, $22, $27, $29
$27, $22, $4A, $BB, $BA, $22, $92, $22
$92, $22, $92, $22, $92, $22, $92, $22
$92, $C2, $92, $44, $DB, $BB, $D2, $2C
$22, $2C, $22, $2C, $22, $2C, $22, $2C
$22, $2C, $22, $2C, $22, $2E, $FF, $FE
end

data ldat
%00000111, %11111111, %11111111, %00000000
%00000000, %10000000, %00001000, %00000000
%00000000, %00000000, %00000000, %00000000
%00001001, %00000000, %00000100, %10000000
%00000010, %00000000, %00000010, %00000000
%00001111, %11111111, %11111111, %10000000
%00001010, %10101010, %10101010, %10000000
%00010000, %00000010, %00000000, %01000000
%00011111, %11111111, %11111111, %11000000
%00100000, %01000000, %00010000, %00100000
%00111111, %11111111, %11111111, %11100000
%00101010, %10101010, %10101010, %10100000
%01000000, %00000010, %00000000, %00010000
%01111111, %11111111, %11111111, %11110000
%11111111, %11111111, %11111111, %11111000
%10101010, %10101010, %10101010, %10101000
end

for row = 0 to 175
if row & 1 then ldat_ptr = (L4p[L4p_ptr]&$0F)*4 else L4p_ptr = row/2 : ldat_ptr = (L4p[L4p_ptr] & $F0)/4
col = 0
byte_loop
cbyte = ldat[ldat_ptr] : cbit = $80
bit_loop
if cbyte & cbit then pfpixel col row on else pfpixel col row off
col = col + 1 : cbit = cbit/2
if cbit then bit_loop
ldat_ptr = ldat_ptr+1
if ldat_ptr & $03 then byte_loop
next


Edited by bogax
Link to comment
Share on other sites

Thanks again.

Your code is so elegant it is hard to follow even with your describing what is happening, however I can learn it, follow it, I know what it is doing after seeing it all coded.

I think we may be able to drop the pfpixel off part because pfclear was added, so procedurally drawing a PF won't just add to what was there if pfclear is used.

 

Now we just need a program that takes the raw PF and removes the duplicates, building the data table.

I wish I knew java or python.

Link to comment
Share on other sites

Well, something falls apart:

Plus that is the wrong center section.

Edit: Also to fill the DPC+ data streams, I believe the PF1 and PF2 data would have to be together. Like PF1L & PF1R as one dataset.

And I don't know if bB is drawing PF1 PF2 PF2 PF1 or PF1 PF2 PF1 PF2 ??

 

bB draws PF0-PF1-PF2-PF2-PF1-PF0, or a reflected playfield. It doesn't move anything into PF0L and PF0R, so the leftmost and rightmost 4 playfield pixels are blank (unless you move something into PF0).

 

If the center section is wrong in that picture, it's because you're pushing the wrong column of data into that particular data queue (or whatever it's called in DPC+). For one thing, I can see by your code that you're pushing the data for the LEFT half of the screen. So what you need to do is figure out which queue goes with which data column.

 

But now I realize there's a problem with MY code-- if you reverse the index like I did, then the nibbles also need to be handled in the reverse order:

 

 

   current_row = 87
decompress_L4_loop
   print_row = L4p[current_row]
 rem   current_byte = print_row / 16
   current_byte = print_row & 15
  DF6PUSH = PF1L_L4_0n[current_byte]
  DF7PUSH = PF2L_L4_0n[current_byte]
 rem   DF6PUSH = PF1R_L4_0n[current_byte]
 rem   DF7PUSH = PF2R_L4_0n[current_byte]
 rem 
 rem   DF7PUSH = PF2R_L4_0n[current_byte]
 rem   DF6PUSH = PF1R_L4_0n[current_byte]
 rem 
 rem    PF1L[2 * current_row] = PF1L_L4_0n[current_byte]
 rem    PF2L[2 * current_row] = PF2L_L4_0n[current_byte]
 rem    PF2R[2 * current_row] = PF2R_L4_0n[current_byte]
 rem    PF1R[2 * current_row] = PF1R_L4_0n[current_byte]
 rem   current_byte = print_row & 15
   current_byte = print_row / 16
  DF6PUSH = PF1L_L4_0n[current_byte]
  DF7PUSH = PF2L_L4_0n[current_byte]
 rem   DF7PUSH = PF2R_L4_0n[current_byte]
 rem   DF6PUSH = PF1R_L4_0n[current_byte]
 rem 
 rem    PF2L[2 * current_row + 1] = PF2L_L4_0n[current_byte]
 rem    PF2R[2 * current_row + 1] = PF2R_L4_0n[current_byte]
 rem    PF1R[2 * current_row + 1] = PF1R_L4_0n[current_byte]
   current_row = current_row - 1
   if current_row <> 255 then goto decompress_L4_loop

 

Sorry about that.

Link to comment
Share on other sites

When your screen is pretty busy with other objects, I found it easier to handle when the PF is reflected. The invisible area is usually used for color and sprite changes. Some writes can be prepared at the very end of the previous scanline. So you should try to make your PF writes in the left to middle area of the visible scanline.

 

And a reflected PF is better when you do not use or change PF0.

Link to comment
Share on other sites

I can see that is helps to have most writes in one go with little need for delays inbetween, then again, even beginners often hassle with the reflected timing.

Learning proper timing and THEN relax is probably better than the other way around, come to think of it ;-)

Thanks.

Link to comment
Share on other sites

A bit Offtopic (forgive me), why do people use mirrored playfield for asymmetric gfx? Isnt it much more relaxed to set registers in normal repeat mode?

 

I prefer to use the repeated mode for asymmetric playfields, due to the less stringent timing demands. As far as I know, the reflected mode offers no benefits with regard to timing, EXCEPT when you want to use a narrower playfield for whatever reason-- it might be to save some scan line cycles, or to save some data bytes (or both), or it might simply be for game design reasons. In that case you'd WANT to use the reflected mode so PF0 will be on the far left and far right sides of the screen and you can just initialize it to all 0s, or all 1s, or whatever, and then leave it be. Of course, you can draw a narrow playfield in the repeated mode, but then you have to update all three playfield registers twice each line, so (as I see it) the only good reason for doing it that way would be for game design reasons-- i.e., cycles per scan line aren't an issue, and ROM space isn't an issue, but nevertheless you want to use a narrow playfield for other reasons, and you decide to go with a repeated playfield because the relaxed timing for the playfield writes works out better for you.

 

Actually, I think asymmetric playfields have sort of spoiled us. It's a little bit humbling to look at games like Tutankham and realize that they drew their "asymmetric" playfields by switching from the repeated mode to the reflected mode, and vice versa, from row to row. That requires more creativity-- and also saves you some cycles and data bytes.

Link to comment
Share on other sites

Great thread! :)

 

I see some parallels with the scrolling virtual worlds of the ASDK:
post-30777-0-82101200-1378741810_thumb.jpg

The ASDK supports storing large virtual world bitmaps in Assembly data statements in WYSIWYG format just like bB defines playfields. The 20x10 playfield area can be panned from anywhere in the 92x20 virtual world using x,y camera coordinates and gets decompressed into a 40x10 pixel grid.

 

Both of these operations happen either between screens, or via flicker in real time - an entire frame is used to handle the heavy calcs.

 

SeaGtGruff made a good point about not being able to do complex operations like decompression during the scanlines; there's just about enough time to draw the display pixels and set the sprite objects so decompression and scrolling has to happen during a larger timeslice.

 

I haven't used DPC+ but bB 1.0 stores it's 32x32 pixel playfields entirely in SuperChip RAM which allows the entire playfield to be malleable via pfpixel. The ASDK uses CBS RAM (256 bytes) to store it's 92x20 virtual worlds to the same effect - pixels are malleable using pfpixel commands.

 

Since DPC+ has access to much more RAM and an ARM processor, supporting large virtual worlds of greater detail and a CAM object for scrolling the playfield could be possible future enhancements for the kernel :)

Link to comment
Share on other sites

SeaGtGruff made a good point about not being able to do complex operations like decompression during the scanlines; there's just about enough time to draw the display pixels and set the sprite objects so decompression and scrolling has to happen during a larger timeslice.

 

Actually, the point I originally made was that, depending on the "compression" method, it might be possible to do the "decompression" during the scan line loop. But then I realized that iesposta is using batari Basic and the DPC+ kernel, rather than assembly and a non-DPC+ kernel, so that rules out trying to do the "decompression" during the scan line loop. The method I proposed-- storing the data for just the unique row patterns, then having an index to point to whichever pattern needs to be drawn on a given row-- is not really "compression" per se in my opinion, but is really just an additional level of "indirection." So as long as you have a few extra cycles free in your scan line kernel, you could do the following:

 

 

kernel_loop
   LAX (PF_rows),Y ; assumption is that Y is the index for the current scan line
   LDA PF0L_data,X
   STA PF0
   LDA PF1L_data,X
   STA PF1
   LDA PF2L_data,X
   STA PF2
   LDA PF2R_data,X
   STA PF2
   LDA PF1R_data,X
   STA PF1
   LDA PF0R_data,X
   STA PF0
   DEY
   BNE kernel_loop

 

Obviously that isn't a complete kernel-- you'd want to do some of those commands at just the right cycle, you might not want to update PF0, and you may or may not want to do a WSYNC in there somewhere, not to mention drawing the sprites-- but there's only one extra line in there, the LAX (PF_rows),Y instruction to get the desired PF pattern for the current scan line. (The assumption is that PF_rows_lo and PF_rows_hi have been set to point to the appropriate table of pattern IDs for the current level.) Then you just read from the PF data tables using X as the index instead of Y. This example assumes that Y is being used with the (Indirect),Y addressing mode for drawing the sprites, so it uses X as the index into the actual PF data, which means you'd either need a separate kernel loop for every screen (each containing different table addresses for the LDA PF_data,X instructions), or put the kernel loop in RAM and execute it in RAM so you can modify the PF_data table addresses on the fly, or have no more than 256 unique row patterns for all screens such that you need only one set of PF_data tables rather than separate ones for each level screen. Note also, this example assumes that the row pattern numbers are stored one-per-byte, rather than squeezing two into a byte (high nibble, low nibble).

Link to comment
Share on other sites

bogax,

To let you know, that saved over 480 bytes.

In my game, that screen would be the best-case scenario. The rest are more complex.

The pie/cement factory has 19 unique PF lines. So I guess using nybbles are out.

The start Level 1 has around 76 unique PF lines.

JumpmanBLU20130909a.bas

JumpmanBLU20130909a.bas.bin

Link to comment
Share on other sites

SeaGtGruff,

Your routine has got me this far. The entire right half of the PF is perfect. (One bad index bit.) I also had to flip horizontally each byte?? in PF1R_L4_0n

post-29575-0-28112400-1378791261_thumb.png

 

DF6LOW = 184

DF7LOW = 184

DF6HI = 7

DF7HI = 6

 

Puts data into the left half of the screen, but it fills all weird.

JumpmanBLU20130908.bas

JumpmanBLU20130908.bas.bin

Link to comment
Share on other sites

bogax,

To let you know, that saved over 480 bytes.

In my game, that screen would be the best-case scenario. The rest are more complex.

The pie/cement factory has 19 unique PF lines. So I guess using nybbles are out.

The start Level 1 has around 76 unique PF lines.

 

 

rig it so you can change dictionaries

 

I didn't actual tote it up but I would

guesstimate that if a line is repeated

only once ie it occurs twice you only

break even.

 

the ladders are where you realy save.

 

changing dictionaries would add some

overhead but I don' think it would

matter how many unique lines there are

so much as how much redundancy there is

ie how many times each line is repeated

 

you haven't shown your other screens

 

you could try and adopt a playfield design

rule that any particular unique line will

be used at least twice.

 

 

Link to comment
Share on other sites

SeaGtGruff,

Your routine has got me this far. The entire right half of the PF is perfect. (One bad index bit.) I also had to flip horizontally each byte?? in PF1R_L4_0n

 

DF6LOW = 184

DF7LOW = 184

DF6HI = 7

DF7HI = 6

 

Puts data into the left half of the screen, but it fills all weird.

 

I don't know if bB's DPC+ kernel reverses the playfield bytes automatically. If you're making an assembly game then you definitely need to reverse the bytes yourself. If you're using the "playfield:" statement in bB then the bB compiler will automatically reverse the bytes where appropriate. But if you used vbB's playfield tool to draw the playfield and are then pushing the data into the RAM queues, I don't know how vbB and bB and DPC+ handle the bytes.

 

Thanks for posting the .BAS file; I'll take a look at it, try compiling it, check the assembly listing, etc., to see if I can figure out what's what.

Link to comment
Share on other sites

 

I don't know if bB's DPC+ kernel reverses the playfield bytes automatically. If you're making an assembly game then you definitely need to reverse the bytes yourself. If you're using the "playfield:" statement in bB then the bB compiler will automatically reverse the bytes where appropriate. But if you used vbB's playfield tool to draw the playfield and are then pushing the data into the RAM queues, I don't know how vbB and bB and DPC+ handle the bytes.

 

Thanks for posting the .BAS file; I'll take a look at it, try compiling it, check the assembly listing, etc., to see if I can figure out what's what.

I got it working today.

I had to dupicate your loop, since I really can't follow what's going on.

This is important batari Basic research, I feel, because changing a playfield, or most likely part of a playfield, during a game gives many possibilities.

It would be nice to have a routine that can set short rows of Playfield. Probably a small section scrolling like in Parker Brothers Star Wars - The Emperor Strikes Back.

JumpmanBLU20130910PUSHa.bas

JumpmanBLU20130910PUSHa.bas.bin

Link to comment
Share on other sites

I got it working today.

I had to dupicate your loop, since I really can't follow what's going on.

This is important batari Basic research, I feel, because changing a playfield, or most likely part of a playfield, during a game gives many possibilities.

It would be nice to have a routine that can set short rows of Playfield. Probably a small section scrolling like in Parker Brothers Star Wars - The Emperor Strikes Back.

 

That's good news!

 

As far as what's going on in the loop I posted, it retrieves the pattern ID (or the index into the unique rows tables) for each row.

 

But in this case there are 2 IDs per byte (1 per nibble), so the loop has to be a "2-line loop" so 1 pass through the loop can process 2 nibbles (1 byte).

 

Rather than use a "for...next.. step 2" (or "step -2") loop, I decided to set the starting value for the counter and then increment or decrement it manually.

 

The IDs are stored in the nibbles with the 1st row in the high nibble and the 2nd row in the low nibble.

 

So if you're incrementing the counter (moving forward through the list) you need to process the high nibble first, then the low nibble.

 

So for the 1st row you use byte/16 to get the value of the high nibble, then use that value to retrieve the data for the 1st row from the tables.

 

For the 2nd row you use byte&15 to mask off the high nibble and get the value of just the low nibble, then use that value to retrieve the data for the 2nd row from the tables.

 

But that displays the playfield upside-down, so we decrement the counter instead (start at the bottom and go up).

 

That means we need to do the low nibble first, followed by the high nibble (my boo-boo was I forgot to do that).

 

So for the 1st row you use byte&15, then for the 2nd row you use byte/16.

 

If you have more than 16 unique patterns and can't squeeze 2 IDs (or pointers) into 1 byte, then you can use a 1-line loop and dispense with the byte&15, byte/16 stuff.

 

By the way, I used this method of "compression" (or "indirection") to revisit a "playfield text" titlescreen that I'd done years ago for a game idea. I'd previously "compressed" the data by creating a data table as follows:

 

number of times to loop to process the whole list

number of times the 1st data byte occurs

actual value for the 1st data byte

number of times the 2nd data byte occurs

actual value for the 2nd data byte

etc.

 

For example, suppose you have the following list of numbers (data bytes):

 

4

8

8

8

4

4

4

4

4

5

2

2

8

8

8

8

 

I compressed the list as follows:

 

1, 4 (4 occurs 1 time)

3, 8 (8 occurs 3 times)

5, 4 (4 occurs 5 times)

1, 5 (etc.)

2, 2

4, 8

 

Then I added another number at the beginning of the list to tell me how many times I needed to loop:

 

6 (there are 6 pairs of numbers to be processed)

1, 4 (1st pair)

3, 8 (2nd pair)

5, 4 (3rd pair)

1, 5 (etc.)

2, 2

4, 8

 

What I was doing was decompressing the compressed playfield data, storing it in RAM (using MNetwork bankswitching with extra RAM), and then reading/displaying the decompressed playfield from RAM.

 

By rewriting that to use the "indirection" method, I'm now able to display the "compressed" playfield directly from ROM by doing the "decompression" inside the scanline loop:

   LDX #166                   ; 02   ; 05
Active_Lines
   LDY Title_Rows-1,X         ; 04+  ; 10
   LDA Title_PF0LR,Y          ; 04+  ; 14
   STA PF0                    ; 03   ; 17
   LDA Title_PF1L,Y           ; 04+  ; 21
   STA PF1                    ; 03   ; 24
   LDA Title_PF2L,Y           ; 04+  ; 28
   STA PF2                    ; 03   ; 31
   LDA Title_PF0LR,Y          ; 04+  ; 35
   ASL                        ; 02   ; 37
   ASL                        ; 02   ; 39
   ASL                        ; 02   ; 41
   ASL                        ; 02   ; 43
   STA PF0                    ; 03   ; 46
   LDA Title_PF1R,Y           ; 04+  ; 50
   STA PF1                    ; 03   ; 53
   LDA Title_PF2R,Y           ; 04+  ; 57
   STA PF2                    ; 03   ; 60
   STA WSYNC                  ; 03   ; 00
   DEX                        ; 02   ; 02
   BNE Active_Lines           ; 02++ ; 04

In this case, I'm using all 40 playfield pixels, and storing the PF0L and PF0R nibbles in 1 byte, hence the need to use ASL to shift the data for the right copy of PF0. And this method was feasible only because I'm not doing anything else in the loop, just drawing the playfield to display the title text. I haven't checked the number of bytes used by the old method versus the new method, but I think the new method takes fewer bytes to store the data-- since any values that occur in different places (like 4 and 8 in my example) are stored just once-- and the "decompression" process is faster and takes less code, since I don't need to decompress the data into RAM ahead of time and then display it from RAM, everything is done as the display is being drawn. :)

Edited by SeaGtGruff
Link to comment
Share on other sites

Here I rearranged SeaGtGruff's code and
added the facility to switch dictionaries
(sets of lines)

Instead of referencing the lines relative
to the start of the table(s) they are relative
to dbase.

When a zero is encountered the next byte
is loaded to dbase this adds two bytes to
the count (crow)ie a count of 88 for the lines
+ 2 for each time dbase is set.

So here crow is intialized to 90 (one count
gets thrown away at the start of the loop
0-87 inclusive for the lines + 2 for setting
dbase)

The switches will have to be byte aligned, a switch
must happen on an even number of lines (2 lines
per byte)

JumpmanBLU.bas

Edited by bogax
  • Like 1
Link to comment
Share on other sites

Now we need a "Playfield Tool" that takes an entire playfield and outputs data and tables. Isn't that what computers are good at? At this point I still have to type in data tables.

Also, "When a zero is encountered" - is that a zero bit or zero byte? I guess I can study the code.

Link to comment
Share on other sites

"When a zero is encountered" - is that a zero bit or zero byte?

 

From the context, it must mean byte, since it skips to the next byte.

 

I'm not sure how skipping bits or bytes because they're 0 would work out. Wouldn't this leave the existing data unaltered? What I mean is, DPC+ doesn't automatically initialize the playfield bytes to 0 on every line, does it? So if you're setting the bits individually and skip a bit because it's 0, or if you skip an entire byte because it's 0, wouldn't that just leave the old data in the playfield bit/byte?

Link to comment
Share on other sites

 

From the context, it must mean byte, since it skips to the next byte.

 

I'm not sure how skipping bits or bytes because they're 0 would work out. Wouldn't this leave the existing data unaltered? What I mean is, DPC+ doesn't automatically initialize the playfield bytes to 0 on every line, does it? So if you're setting the bits individually and skip a bit because it's 0, or if you skip an entire byte because it's 0, wouldn't that just leave the old data in the playfield bit/byte?

 

 

Just to clarify how I'm using the teminology

you have a list of "symbols" which are nibbles

which are used as pointers into a "dictionary"

of possible lines to be pushed to the playfield

 

The lookup into the dictionary is now relative

to an offset called dbase so instead of a

symbol/nibble being relative to the beginning

of the dictionary it's relative to dbase

the look up is (dbase + symbol) relative to the

beginning of the dictionary.

 

Instead of 16 possible line symbols there's now

15 because zero is used for switching dbase

 

If a zero byte is encontered in the symbol list

dbase gets set to the byte after the zero.

 

I't doesn't miss pushing any of the lines, it

skips two bytes in the symbol/nibble list

which is why the count (and the symbol list)

must be increased by two, one for the zero marker

and one for the new offset into the symbol list.

 

 

A blank line is still in the dictionary

 

I was considering making a blank line a special

case and not putting it in the dictionary since

it's likely to be used any where.

 

I know almost nothing about DPC+ but I assume

that to get the data fetchers to skip a line

would require setting them to new values which

seems like a lot more bother than it would be

worth just for blank lines.

 

But is might be worth while if you could

change just a part of the playfield.

 

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...