|
ForumsSega Master System / Mark III / Game GearSG-1000 / SC-3000 / SF-7000 / OMV |
Home - Forums - Games - Scans - Maps - Cheats - Credits Music - Videos - Development - Hacks - Translations - Homebrew |
Author | Message |
---|---|
|
A few hints on coding a medium/large sized game using WLA-DX
Posted: Mon Dec 14, 2015 1:47 pm Last edited by sverx on Wed Dec 16, 2015 10:42 am; edited 1 time in total |
After having completed Waimanu Scary Monsters Saga, I feel to share a few things I learnt -mainly through my own mistakes- so to help anyone willing to work with WLA-DX on a non-trivial project. I hope you'll find these hints useful.
Of course I'm not saying this is 'the only' or 'absolutely the better' way to deal with this stuff, it's just what I found that could better ease my work. Feel free to comment and let's debate! :) To page or not to page The first thing to address is whether your project needs paging or not, and it's better not to postpone that decision. If you're going to use background music, SFXs, splash screens, rich backgrounds, different sprites, animations... chances are you'll hardly be able to fit everything into just 48 KB. If you're still in doubt: go for it, as it's easier to remove paging than to add it later. So, once you decided your big project does need paging, you have to choose your strategy. The simpler approach, which means to page in ROM banks using slot 2 only, will be enough for your needs at least in 95% of cases. If for some peculiar reason you've got a single asset which is bigger than 16 KB and you really can't break it into smaller parts, you could consider paging in both slot 1 and slot 2 so that you could map a contiguous block of 32 KB. Paging into slot 0 is instead discouraged, as you'll end up paging out the code you didn't store in the first 1024 bytes... and that means you'll have to face bigger problems. Once you've decided your paging strategy, you've got to write your own ROM map and learn how to use free and superfree sections. Paging can really become a nightmare if you don't learn to use superfree sections. Instead, using superfree sections for every single asset you can page out (so you've got to exclude what you can't page out, more on this later) will relieve you of the burden of finding the right spot to place each asset in ROM. As for free sections, it's the way to go to place into unpaged ROM your code and the assets you don't want to page out. See next section. examples of ROM maps Very simple memory map with 3 16 KB slots. 3 16 KB banks defined. Can be used for paging in any slot. .memorymap
defaultslot 0 slotsize $4000 ; ROM slot 0 $0000 slot 1 $4000 slot 2 $8000 slotsize $2000 ; RAM slot 3 $c000 .endme .rombankmap bankstotal 3 banksize $4000 banks 3 .endro Memory map with a single (almost) 32 KB bank 0, a 16 bytes bank 1 just for the SEGA ROM header and a 16 KB slot 2. Can be used for slot 2 only paging. This is from Waimanu SMS source. .memorymap
defaultslot 0 slotsize $7ff0 ; ROM (won't page this) slot 0 $0000 slotsize $0010 ; SEGA ROM header (won't page this too) slot 1 $7ff0 slotsize $4000 ; ROM (... will page this!) slot 2 $8000 slotsize $2000 ; RAM slot 3 $c000 .endme .rombankmap bankstotal 8 banksize $7ff0 ; 32 KB minus 16 bytes banks 1 banksize $0010 ; 16 bytes, for SEGA ROM header banks 1 banksize $4000 ; 16 KB (each) banks 6 ; 6 of them. Makes this 128 KB total .endro Switch to dynamic placement of code and assets Using free and superfree sections also means you're switching to dynamic placement of code and assets into your ROM. At first, it might seem you're losing control on what's happening into your project but... no, it isn't so. Instead, you're gaining a big advantage: you don't absolutely have to worry where to place your stuff around, it's just like having a warehouse that organizes itself each time you add/remove or modify an item. So the only code that shouldn't be dynamically placed is boot code (at .org $0000), IRQ and NMI service handlers (at .org $0038 and .org $0066 respectively): these will have their own force sections. Everything else (code and data!) should be contained within a free, superfree or semisubfree (see note) section. examples of superfree and free sections Superfree sections: .section "Intro (MUSIC Asset)" superfree
Intro: .incbin "inc/intro.psg" .ends .section "Points (SFX)" superfree SFXPoints: .incbin "inc/punti.psg" .ends Free sections (code): .section "waitForVBlank" free
waitForVBlank: xor a ld (VBlankFlag),a -:ld a,(VBlankFlag) or a jr z,- ret .ends .section "Div and Mod" free ; courtesy of http://www.smspower.org/Development/DivMod ; Integer divides D by E ; Result in D, remainder in A ; Clobbers F, B DivMod: xor a ld b,8 -:sla d rla cp e jr c,+ sub e inc d +:djnz - ret .ends The boot, IRQ and NMI sections .org $0000 ; this goes at ROM address 0 (boot) : standard startup
.section "Startup" force di ; disable interrupt im 1 ; interrupt mode 1 (this won't change) ld sp, $dff0 ; set stack pointer at end of RAM jp init_mapper ; run init_mapper .ends .org $0038 .section "Interrupt handler" force push af in a,(VDPStatusPort) ; read port to satisfy interrupt ld a,$01 ; here the only interrupt enabled is VBlank so... ld (VBlankFlag),a ; ... write down that it actually happened pop af ei ; enable interrupt (that were disabled by the IRQ call) reti ; return from interrupt .ends .org $0066 .section "Pause handler" force push af ld a,$01 ld (PauseRequested),a ; raise flag to indicate pause was pressed pop af retn ; return from NMI .ends .org $0400 .section "Initialize mappers" semisubfree ; This maps the first 48K of ROM to $0000-$BFFF and resets RAM mapper to $00 too ; this section must exist within first 1K of ROM, thus it's semisubfree (.org $0400) ; also, this clears RAM mem setting $00 from $C000 to $dff0 init_mapper: ld de,$fffc ld hl,mapper_init_values ld bc,$0004 ldir xor a ; clear RAM (to value 0x00) ld hl,$c000 ; by setting value 0 ld (hl),a ; to $c000 and ld de,$c001 ; copying (LDIR) it to next byte ld bc,$1ff0 ; for 8 KB minus 16 bytes ldir ; do that jp main ; jump to main program mapper_init_values: .db $00, $00, $01, $02 .ends Note: semisubfree tells WLA-DX allocator to place the section freely but entirely within a given region. In this case it ensures the code is placed in an available spot within ROM first 1024 bytes. Everything you shouldn't place in superfree sections Superfree sections, as said, should be the most common, for assets. If you're going to use PSGlib for music and SFXs, you should declare a superfree section for each tune you put into your project, and a superfree section for each SFX too, even the smaller ones, even if they're just few bytes each. WLA-DX allocator will make great use of the assets fine granularity, packing everything the perfect way you really couldn't achieve manually. Code and some data, instead, shouldn't be ever paged out, so you shouldn't place them into slot 2. Instead, use free sections into slot 0 and slot 1 (or slot 0 only if you choose for a single almost-32 KB unpaged ROM block). LUTs, especially those you use for speeding up your math, are probably the best example of the few assets you should keep in your unpaged ROM. Also, LUTs that are 256-bytes aligned can be accessed faster, so you'll probably want to align them all. Sections can be aligned easily, just tag them as so. examples of LUTs .section "Divide by 5" align 256 free
divide5: .rept 60 index n ; 60 are enough for my needs... .db n/5 .endr .ends .section "Module 3" align 256 free module3: .rept 43 .db 0,1,2 ; 129 values are more than enough... .endr .ends Use dynamic placement on variables too! Just as we did with the code, it's very handy to let WLA-DX allocate our variables in RAM, for the very same reasons given before. So you really should avoid placing your variables at fixed addresses and let ramsections work. You can define as many ramsections as you need, in fact I suggest you define one for each 'module' you're coding. examples of ramsections .ramsection "PSGlib variables" slot 3
; fundamental vars PSGMusicStatus db ; are we playing a background music? PSGMusicStart dw ; the pointer to the beginning of music PSGMusicPointer dw ; the pointer to the current PSGMusicLoopPoint dw ; the pointer to the loop begin .ends .ramsection "system variables" slot 3 SpriteTableY dsb MAXSPRITES ; shadow sprite table SpriteTableXN dsb MAXSPRITES*2 VBlankFlag db ; VBlank flag PauseRequested db ; Pause button flag KeysDown db ; controllers NextAvailableSprite db ; sprite management MusicROMPage db ; Music/SFX banks SFXROMPage db RestoreROMPage db savedbank db ; previous mapped bank .ends .ramsection "game variables" slot 3 CurrentLevel db ; 0-31 CurrentFrame db ; fraction of second (0-59) SecondsLeft db Lives db ; BCD! AliveWekas db ; number of Wekas currently roaming WekaEggs db ; number of eggs still hidden in the map Score dsb 4 ; BCD (up to 99.999.999) HiScore dsb 4 ; BCD (up to 99.999.999) GameMode db ; EASY or HARD .ends Know your macros I found very convenient to create small macros for addressing some small but very common coding needs, such as shifting or rotating the accumulator a given number of times, or slightly more complex tasks, such as adding the accumulator value to HL. Using simple macros you can imagine to somewhat expand your Z80 instruction set. example of some of the macros I actually use .macro ASLA args shift
; this is ARITHMETIC SHIFT LEFT A by 'shift' bits using ADD A,A since it's more efficient than SLA A ; cycles taken = 4*shift ; affects A, Flags .rept shift add a,a .endr .endm .macro ASLHL args shift ; this is ARITHMETIC SHIFT LEFT HL by 'shift' bits using ADD HL,HL ; cycles taken = 11*shift ; affects HL, Flags unaffected .rept shift add hl,hl .endr .endm .macro RRA args shift ; this is ROTATE RIGHT A by 'shift' bits ; cycles taken = 4*shift ; affects A, Flags .rept shift rrca .endr .endm .macro ADDHLA ; this is HL=HL+A, since ADD HL,A doesn't exist... ; cycles taken = 20 ; affects HL and A, Flags trashed add a,l ld l,a adc a,h sub l ld h,a .endm Never skip compression Both if you plan to use paging or if you don't, you should always consider applying compression to your assets. For instance, if you don't need to randomly access and stream tiles into VRAM as fast as possible, you shouldn't store an uncompressed tileset into ROM, it just wastes precious space. Tiles can be compressed quite well using PSGaiden compression, for instance, which is also a built-in option into Maxim's BMP2Tile. Decompression can be achieved simply using this code snippet. I rolled out a faster PSGaiden tileset decoder, if you find the previous one too slow. Also, my version uses free sections and ramsections. Tilemaps can be compressed too and there are a few options for compressing them. I can't suggest any particular choice here, except for my own STMcomp, which is anyway only suitable for compressing and decompressing whole screen maps (or at least whole screen lines) directly into VRAM. The compressor (it's a plugin for BMP2Tile) performs especially well on title/splash screens. Decompression from this format is probably the fastest option available too. Non-graphic assets can be compressed too, either using a simple RLE or more powerful tools. Tunes and SFXs (.psg files) can be compressed once using psgcomp tool and can be used directly in this compressed form, as the library plays compressed files too, thus I suggest to always compress these files. (do you think I skipped something of importance? let me know!) |
|
|
Posted: Mon Dec 14, 2015 4:51 pm Last edited by Kagesan on Wed Dec 16, 2015 8:10 am; edited 1 time in total |
Very good topic.
When developing Bruce Lee I actually never used any superfree sections, I just put mostly free ones manually into the banks. Since my ROM was going to be clearly bigger than 64 KB but not coming very near the 128 KB, that worked quite well. I can see how superfree sections can help when space is tight and you have to squeeze your game in, though. One more hint from me: Organize your assets Try to plan beforehand how you want to use the available space the VDP offers you. Re-use sprite parts whenever possible, and try to have a good idea at which points you are going to load in new data. |
|
|
Posted: Mon Dec 14, 2015 9:45 pm |
Thanks for the writeup, sverx! I'll surely draw on this information when I go beyond 32 kb next time! | |
|
Posted: Mon Dec 14, 2015 10:27 pm |
I think it would be good to have some general purpose tools for compression outside of graphics, as that's coming up a lot recently. Level data can be uncompressed in the ROM, or decompressed to RAM. The former offers you almost unlimited level sizes, but costs ROM space. The latter allows you to optimise much better for ROM size. | |
|
Posted: Mon Dec 14, 2015 10:31 pm Last edited by Maxim on Tue Dec 15, 2015 11:11 am; edited 1 time in total |
A tip to add to your macros: you might want to install some of them as functions - for example, the "add hl, a" one - in the interrupt vectors. This makes a call fairly cheap (rst $08 etc) and you can hide that inside the macro. However, the inlined code is faster, but takes a lot more space. Code space is highly likely not to be a problem though :) it's hard to write 32KB of code. | |
|
Posted: Tue Dec 15, 2015 9:49 am Last edited by sverx on Tue Dec 15, 2015 9:59 am; edited 1 time in total |
Pucrunch? aPLib? (LOL! I'm sending you a link to a page on your website! :D )
I started that way too, but soon I realized too many times I had to move stuff around to avoid adding a new 16 KB bank when there was still more than 16 KB unused in my ROM. I ended leaving some free sections in the banks and using superfree sections from that point on. We mostly learn by doing, you know... but I would use superfree sections straight from the beginning, now. Out of curiosity: how much free space is left into Bruce Lee ROM? |
|
|
Posted: Tue Dec 15, 2015 7:33 pm |
About 12 KB. |
|
|
Posted: Wed Dec 16, 2015 10:49 am |
Oh, I see why you didn't struggle too much placing assets manually. (BTW I guess you're using quite a bit of compression on your assets, at least it seems to me, as WinRAR compresses your Bruce Lee ROM less than my Waimanu ROM, which contains quite a lot of compressed stuff...) |
|
|
Posted: Fri Dec 18, 2015 12:22 pm |
Can I also suggest that you might want to try my WLA-DX z80 IDE.
It's great for larger projects and comes with syntax highlighting syntax checking z80 cycle / byte counting built-in z80 instruction set docs quick navigation to labels/macros/defines etc collapsable sections find references to labels define / macros shown in tool tip while hovering over label :) To install just download Eclipse http://www.eclipse.org/downloads/packages/eclipse-ide-java-developers/mars1 navigate to the menu --> Help --> Install New Software... --> Add... Enter the update site details https://dl.bintray.com/yuv422/EclipseZ80Editor Then create a new general project and create a file with the extension .asm Click yes when asked about xtext and then you're ready to go. |
|
|
Posted: Mon Dec 21, 2015 10:06 pm |
Positioning the spriteTable on the memory:
This may read obvious, but I believe the best address to your sprite table mirror on Ram is in this format: $xx40 and with no space between the Y's array and the X:Patten's array. That way a simple shift in the least significant part of the pointer alternates between the sprite components. Exemple: ld b, 64 ld hl, spriteTable -: ;a := calculatesY ld (hl), a rlc l ;de: calculateXPattern ld (hl), d inc l ld (hl), e inc l rrc l djnz - Also, when streaming the sprite table to the Vdp, theres no need to repositioning HL, only the VDP address: xor a out (VdpControlPort), a ld a, VramWrite.hi | VramSpriteTable.hi out (VdpControlPort), a ld hl, spriteTable .rept 64 outi .endr ld a, VramSpriteTableBottom.lo out (VdpControlPort), a ld a, VramWrite.hi | VramSpriteTable.hi out (VdpControlPort), a .rept 64 outi .endr |
|
|
Posted: Tue Dec 22, 2015 8:31 am |
That's quite cunning. You're making the address of sprite n be 64+n so a shift gets you 128+2n (without overflow) which builds the 64 byte gap into the multiplication by 2 which you need anyway...
I've always spent my frame time to collect the y, x, n from all over the place into a sprite table mirror since I'd have been using some structs per "actor" with other data too. This also dissociates them from the sprite table order, allowing you to implement some reordering to counter sprite overflow., |
|
|
Posted: Tue Dec 22, 2015 5:39 pm |
Now that I posted this, I came across with a better, but a bit uglier solution. Putting the Y's array in a 65 to 128 position and filling the sprote table bacwards:
Ld hl, spriteTable ;$xx80 ld b, 64 -: ;a:calculate Y ld (hl), a sla l ;de:calculate x,pattern ld (hl), e dec l ld (hl), d Sra l Djnz - This way, there is no need for the an extra dec l, with the disadvantage of the last sprite patten been at a $xx00 address, but this can be easily resolved when streeming the sprite table to the VDP. |
|
|
Posted: Sun Dec 27, 2015 6:54 pm |
Why should the SEGA header have a separate slot and bank? | |
|
Posted: Sun Dec 27, 2015 7:12 pm |
Would you suggest using one $8000 bank followed by multiple $4000 banks? With that the bank starting from $8000 will be numbered as bank 1, $c000 as bank 2 and so on. So you won't be able to use the :label directive because it would always be off by 1. Theoretically this could be solved by adding a bank of size 0 in between but WLA-DX won't allow you to do that. As you won't want to overwrite the header anyways an obvious solution is to put the header into its own bank. |
|
|
Posted: Sun Dec 27, 2015 8:23 pm |
You got me :) But now I see the light! |
|
|
Posted: Mon Feb 15, 2016 11:46 pm |
I am not sure if I fully understand the concept of slots. Is there a diagram or illustration to help me visualize the relationship with paging and banking? | |
|
Posted: Tue Feb 16, 2016 6:10 pm Last edited by Maxim on Tue Feb 16, 2016 11:08 pm; edited 1 time in total |
Each 16KB of ROM is a bank. You can put each of them* in three 16KB slots in the CPU address space. Paging is the process by which you put a bank in a slot. At assembly time, the code has to be assembled to know what slot it will be in, so WLA DX has directives for that.
You can view the RAM as another slot, but let's not complicate matters. * Not entirely true, slot 0 is weird. |
|
|
Posted: Tue Feb 16, 2016 6:44 pm |
I wrote http://colecovision.eu/ColecoVision/development/compression.shtml for my ColecoVision games. While I mostly use it for graphics, in some games level data is compressed that way, too. Philipp |
|
|
Posted: Tue Feb 16, 2016 8:59 pm |
Ah! Ok, thank you. I was thinking backwards with each bank being split up into slots. This makes much more sense. |
|
|
Posted: Sun Feb 12, 2017 9:42 pm |
I'm having more difficulty with paging. Everything works fine if I put my graphical data in free sections in bank 2 slot 2, but everything gets glitchy if I use superfree or bank 3.
For reference, my curret setup has a memory map that looks like this: .memorymap defaultslot 0 slotsize $7ff0 ;unpaged slot 0 $0000 slotsize $0010 ;header slot 1 $7ff0 slotsize $4000 ;paged rom slot 2 $8000 slotsize $2000 ;ram slot 3 $c000 .endme .rombankmap bankstotal 8 banksize $7ff0 banks 1 banksize $0010 banks 1 bamksize $4000 bamk 6 ;mostly an arbitrary number; just growing room. .endro And my data area looks something like this: .bank 2 slot 2 .org $8000 And then everything is currently in superfree sections. Now, my game logic is basically just loading the appropriate tileset tilemap and palette data into pointers that the VRAM writing routines use every frame. I know this works perfectly well without banking. And then it just goes into a loop waiting for VBlank and checking the directions which will send the program to the other routine which loads different data locations and then loops and does the same thing again... The thing is, even with everything in superfree sections and no matter how I arrange them in the editor, the palette always loads perfectly, but then tilemaps or sets won't load properly. A set will load but map is garbage or the other way around or neither will load and I can't seem to detect the pattern of errors. To be fair, I don't have anything compressed yet and all the data is just pasted in my main file for the moment. I was waiting to make sure banking would work in the first place before adding any more potential complications. So what is it that I'm missing here? |
|
|
Posted: Sun Feb 12, 2017 10:06 pm |
I'd need the code to diagnose it. Check the symbol file for the page and address data ends up with, or step through the code to see what the data looks like when it gets there at runtime.
I seem to remember you need a .slot directive for superfree to work, could that be it? |
|
|
Posted: Mon Feb 13, 2017 9:42 am |
yes, it's probably just a missing
.slot 2
before the superfree sections. I wonder if I have to fix the first post to be clearer... |
|
|
Posted: Mon Feb 13, 2017 10:01 am |
I guess specifying bank 2 and slot 2 once before the superfree sections should be enough. :P @Phano: Did you insert the necessary paging instructions? The assembler won't insert paging instructions for automatically. You have to manually do it. So for example if you want to use the data labeled with myTiles you need to do something like: ld a, :myTiles
ld ($ffff), a ld hl, myTiles call loadTiles The colon (:) in front of the label tells the assembler to put the bank number of the label there. Another idea: If I remember correctly .org $8000 means $8000 bytes relative to the current slot. But then it should complain that $8000 exceeds the slot size. So instead it should either be .orga $8000 or .org 0 . But you said without superfree sections it worked correctly so I guess this isn't really the issue.
|
|
|
Posted: Mon Feb 13, 2017 6:36 pm |
Aha! As a matter of fact I was just playing around with loading :LABEL into ($FFFF). I wasn't getting it to work for every situation but I suspect there is just a syntax/logical error somewhere I need to work out.
Also, I've since studied the listing file to see where my data was going and if I read it correctly, it was indeed everything that was getting put into bank 3. I suspect bank 2 works because it is still part of the z80 address space, correct? It's switch into the slot by default and I just haven't been switching the other banks in. I'm not sure why I assumed the assembler would take care of this! |
|
|
Posted: Mon Feb 13, 2017 6:55 pm |
The paging register must be set correctly for every access to slot 2. Otherwise, at some point it might be set to bank 3 while you want to access data in bank 2 or other way round.
No. It only works because at the point your program starts the mapper already maps bank 2 into slot 2. If all 3 slots mapped to bank 0 you wouldn't even be able to access the data starting from address $4000 in your ROM. It has nothing to do with z80 address space. |
|
|
Posted: Mon Feb 13, 2017 10:12 pm |
I'm going to say that's what I meant. In any case, is it at all possible to load the bank number into a variable to use later, or pull one of these (:Label)? I can't seem to get either to work but I assume I'm missing or don't understand something before anything else. I'll take a break and come back to it later and stop botherig everyone now! |
|
|
Posted: Mon Feb 13, 2017 10:31 pm |
When the assembler encounters a :label it replaces it with the bank number of the label. For example if label is in bank 3 and you have ld a, :label
the assembler will replace the :label with 3 and the instruction becomes ld a, 3
So it's just a constant and that means yes, you can save it in a variable: ld a, :label
ld (bankVariable), a You can also have it in a table for example holding label bank pairs such as: .dw label1
.db :label1 .dw label2 .db :label2 ... I hope this is clear enough. Unfortunately I don't know what you mean by
|
|
|
Posted: Mon Feb 20, 2017 4:03 pm |
First of all, thank you for the effort to figure it out and then to organize and write down your thoughts in here.
Top post should really be expanded, polished and posted along with Maxim's tutorials |
|