GP32 and LCD DMA
Those who has taken a closer look at GP32's LCD hardware know that the GP32 really does not have any special LCD features you would expect from a handheld games machine. The virtual screen feature is almost the only positive exception. There are no VBlank interrupt, no HBlank interrupt, no LCD controlled DMA triggering, no sprites, no multiple playfields, no tiled modes (well some think this is just a positive thing) and so on. Also the LCD is somewhat *hard* to set up due its support for varying frame rates and screen sizes.
But the GP32 LCD has linear frame buffer albeit it is rotated 90 degrees counter-clockwise. So the GP32 somewhat is ideal for 3D graphics and with its powerful and fast ARM processor complex 3D games are possible.
This document explains how to do so called "Raster Effects" on GP32 LCD hardware. One example of "Raster Effects" are "Split Screens", which e.g. allow dividing the screen into several independent blocks and e.g. change frame buffer memory location during the frame update.
There are only two key things when doing Raster Effects on GP32. The first is timing i.e. waiting for a certain scan line (some prefer the term raster line). Fortunately LCDCON1 register provides us with the current raster line counter (counting from the largest value down to zero). The second key thing is to understand how the LCD DMA works and what is possible to do with it. The rest of this document concentrates on the LCD DMA and how to control it more than it is described in the official documentation.
The "Wicked" and "Sicko" demos make intensive use of LCDSADD1, LCDSADDR2, LCDSADDR3, LCDCON2, and LCDCON3... because they are the main LCD registers to control the LCD DMA. We need LCDSADDR1.LCDBASEU and LCDSADDR2.LCDBASEL to change the frame buffer memory location (remeber that the frame buffer must located within the 4MB area LCDSADDR1.LCDBANK defines and that the cache & writeback settings are correct). Write to LCDSADDR1, LCDSADDR2, LCDSADDR3, LCDCON2, and LCDCON3 registers are buffered in the same way as with Timers. Values written into those registers are updated into the LCD DMA internal registers during the next DMA refresh period, which in the normal case means during the next frame update. Also note that the LCD DMA repeats itself i.e. if the number of pixels you define the LCD driver to display is greater than the number of halfwords defined for the LCD DMA, the DMA will repeat the same data over and over again. The DMA gets updated at the beginning of each raster line and at the very beginning of the frame. You can affect LCD DMA updates with LCDSADDR2.LCDBASEL and LCDSADDR3.PAGEWIDTH register settings. Another important note is that the size of one LCD DMA burst is 4 words i.e. 16 bytes, which means 16 8bits pixels or 8 16bits pixels. That is the minimum granularity you can achieve using Raster Effects along one raster line. The DMA burst size also restricts the frame buffer size in the memory. The frame buffer width (in bytes) must be divideable by the number of bytes in one DMA burst.
Now we basically have all pieces needed to do Raster Effects on GP32 LCD hardware:
- Ability to wait for a certain raster line
- LCD DMA repeating feature
- Size of 4 words during one LCD DMA burst
- Reloading of LCD DMA internal registers after the DMA has finished its current task
The LCD documentation states a formula for calculating correct LCDSADDR2.LCDBASEL value:
LCDBASEL = ((the frame end address) >> 1) + 1
= LCDBASEU +
(PAGEWIDTH+OFFSIZE)*(LINEVAL+1)
In order to make vertical Split Screens lets put the same formula into a bit different form.. just to clarify things out:
LCDBASEL = ((the frame end address) >> 1) + 1
= LCDBASEU +
(PAGEWIDTH+OFFSIZE)*(Height_of_Split)
And that is all for a vertical Split Screen! Simple isn't it?! Keep in mind that the (LINEVAL+1) MOD Height_of_Split must be zero. Otherwise the screen starts wandering around. Now if you do nothing more the LCD DMA will repeat the same Height_of_Split sized screen (LINEVAL+1) / Height_of_screen times. If you want to have several different Split Screens or scroll those Splits then you need to change the start of the LCD frame buffer pointer _once_ inside the Split Screen for each Split. Note that the new LCD frame buffer pointer will affect the next Split Screen and not the one during you poke the new values. For example if the Height_of_Split == 32 you can wait lets say 30 raster lines and the poke in the LCD frame buffer pointers for the Split number 1 (assuming the numbering starts from 0). To set the LCD frame buffer pointers for the Split number 0 you need to wait to the last raster line or so and set required registers. With some clever and accurate tweaks you can even break the (LINEVAL+1) MOD Height_of_Split == 0 requirement :)
Note! Half Word = 2 bytes and Word = 4 bytes.. this might be slightly confusing for people with a Windows programming background.
In order to make a horizontal Split Screen the main principle is almost the same as with vertical Split Screen. There are minor differences but the registers used to do the trick are the same. Lets define the formula used to calculate LCDSADDR2.LCDBASEL into the following form:
LCDBASEL = ((the frame end address) >> 1) + 1
= LCDBASEU +
(Width_Of_Split)*(1)
And basically that is all you need for a horizontal Split Screen effect. There are few constrains though. This is what I found out so it does not mean it is the absolute truth. firstly the Width_Of_Split must be divideable evenly by the LCD DMA burst size, which is 16 bytes. Note that Width_Of_Split is expressed as half words and the LCD DMA burst size (i.e. 16) as bytes. Secondly the LCDSADDR3.PAGEWIDTH MOD Width_Of_Split must be zero. When these two rules match then the LCD DMA will repeat only Width_Of_Split half words of the frame buffer on every line through the the whole display. But you still need to do a bit to get actully anything useful displayed with the horizontal Split Screen effect. Here is where timing etc takes place. You just have to load LCDSADDR1 and LCDSADDR2 registers on every raster line and if you have horizontal Splits also then several times during that raster line. Same buffered writes to registers applies here also (like with vertical Splits) so you need to adjust your frame buffer pointer updates accordingly. Now you go and time your code yourself :)
Happy coding + hacking + timing..
So to conclude the LCD DMA:
- Always update LCDSADDR1 and LCDSADDR2 in pairs.
- For horizontal Split Screens LCDSADDR3.PAGEWIDTH must be divideable evenly by the Width_Of_Split and the Width_Of_Split must be divideable evenly by the LCD DMA burst size.
- For vertical Split Screens..
- Writes to most LCDCONx and LCDSADDRx registers are buffered and do not use new values immediately.
- ...
Some notes and tips: if you turn the LCD on and off during one frame refresh period chances are great that the GP32 hangs. If the turning on and off happens during the same raster line then the GP32 will survive but if the time between on/off spans raster lines -> a hang/lock. You get the same effect if you change the color depth of the screen during one frame refresh.