Video mode operation
This is an in-depth guide on the operation of the Uzebox kernel, strongly tied to how the video signal is generated. It is intended for people who want to design technologically more advanced games for the Uzebox, or want to use SPI RAM video modes or audio.
The overall view
The Uzebox is a single-chip game console, where that single ATmega644 does everything with precise real-time control. This means it is driving the video and audio signals at cycle-accuracy to produce the appropriate visual and audible results. If you are interested in these, you could check out the documentations on the main page, such as these:
- http://belogic.com/uzebox/howitsmade.htm (An overall view)
- http://belogic.com/uzebox/video_primer.htm (NTSC video specifics)
However further on, the actual signals generated are not that important as the kernel does the job just fine. What is important from the perspective of game design is how these tasks interact with each other.
The timing of frames
In each second, 60 display frames are generated, each of these frames have identical timing. The most often games do all their tasks within a single frame including processing user input, calculating game logic, and preparing the display and audio for the next frame. The timing of a frame is best shown on a picture, as below:
As you can observe, user code (your game's code what you write) only runs in the Vertical blanking (VBlank). During the Video frame, it is not possible to run your code as then the microcontroller is busy generating video and audio signals.
Most often a game uses WaitVsync(1); to time itself relative to the display. This call waits until the end of the next Video frame, which means that no matter where it is called (anywhere in the green region running user code), then it will only return after the next Video frame, so you have a complete frame to work with, and the game is synchronized neatly to 60Hz.
What you have to pay great attention to is the Vsync interrupt. If your game code takes too long to execute, this interrupt will suspend it, so the kernel can do its job, however your code may still finish before the next Video frame. However then the state of the system will be more or less different.
Also during the Vertical Blanking, there are periodic Hsync interrupts in which most notably audio is generated. However for practical purposes this task is completely transparent and wouldn't ever interfere with you game code.
Getting more time for your game logic
Typically there are two ways to get more time for your game logic.
- Shrinking the screen (the brown section)
- Shrinking the Vsync interrupt (the red section)
Both of these will give you more time to run your game's code.
Shrinking the screen doesn't need much explanation, just use SetRenderingParameters() to set the desired start line and count of lines (a start line of 20 and a count of lines of 224 are the defaults). Keep in mind that the Vsync interrupt will keep striking at the same location, so considering that you may want to keep your display vertically centered, you will grow both the region before and after it available for user code.
Shrinking the Vsync interrupt relates mostly to game design. The most costly task in this interrupt is the rendering of sprites in several video modes (Mode 3 in particular), it could become so costly that it barely fits, or even spills over causing artifacts. How you can reduce this task is obviously less sprites, but there is more to that. You can also make sprites less demanding if you keep them aligned to tile boundaries: an aligned sprite (even on only one of the axes) is cheaper than a free-moving one.
There is another possibility you might have overlooked: pay attention to use the inline mixer if possible! (Check whether the parameter "-DSOUND_MIXER=MIXER_TYPE_INLINE" is present in your Makefile, and add it if not) This not only reduces the Vsync interrupt, but also frees up about 512 bytes of RAM, which is a lot!
The Vsync interrupt carries some hazards which may give you some difficult time debugging if you have a game code which can't complete before it. These are as follows:
- Controller input: After it, new inputs are read, so the spilled over part of your game code might see different inputs.
- Video: Sprites are rendered, whatever you attempt to do with the display could have unintended effects the oncoming frame.
- Audio: Audio routines are not adequately guarded against such a condition, so it may severely mess up (harsh noises).
There are some options to avoid these. The most straightforward, of course is timing your game code right (Use WaitVsync(1); appropriately to sync, and avoid doing so much that it spills over Vsync), however it is also possible to remove tasks from Vsync for your use.
Video Mode 3 has several options in this regard, and some other modes may not even have Vsync tasks (such as Mode 74 and related), using these can eliminate display related problems, allowing you to have more complex display code.
It is also possible to eliminate the controller read using CONTROLLERS_VSYNC_READ (set it zero) in your Makefile. This allows you to read the controllers right when you need their input.
Currently there are no equivalent solutions for audio, so you should keep in mind that you have to deal with audio-specifics early, before the Vsync interrupt could fire.
If you are only using the SPI RAM extension for extra storage, you don't really have to worry about the timing, as your own code is the only thing which accesses the SPI bus.
However if you use video modes built on this extension, you have to pay attention that in the Video frame (brown region) the SPI bus will be accessed unless the video mode is told to not do that. This is important as not paying attention will possibly result in broken filesystem access (SD card, on the same SPI bus), and also broken SPI RAM accesses whenever the Video frame interrupt strikes within an SPI transaction.
Also you need to pay attention to this when using audio extensions relying on the SPI RAM, which might try to access it within the Vsync interrupt.