This PR is another step forward towards properly emulating the NES, specifically that we ultimately want to be able to cache multiple pieces of data during the rendering phase and compute color values based on them, versus computing colors dependent on the live values of multiple pieces of state. There are several motivations for this:
Caching different data elements to be used later on when computing the color of a pixel is actually more faithful to the way the NES actually functioned.
Computing color values for pixels, although largely correct, is also inefficient and incurs a performance hit, as many data elements used in those algorithms are computed over and over again on the fly instead of being reused.
Certain pieces of state, such as the scroll X value, change in the midst of rendering a line of pixels, and so currently the end result is that the HUD for Super Mario Bros. doesn't render properly and even flickers at certain times. Using cached values which are refreshed at the same rate that they are in an actual NES will (hopefully) get rid of such artifacts.
currentSharedAddress: a UInt16 which mostly corresponds with the so-called V register in the NES
nextSharedAddress: a UInt16 which mostly corresponds with the so-called T register in the NES
currentNametableByte: a UInt8 which contains the byte in the current nametable relevant to the current tile
currentPaletteIndex: also a UInt8 but is effectively only a two-bit value representing an index into the palette for a given tile
currentLowTileByte: also a UInt8 which contains the low byte relevant to the current tile
currentHighTileByte: also a UInt8 which contains the high byte relevant to the current tile but
currentTileData: a UInt64 with contains data relevant to the current and next tile
currentFineX: a UInt8 but is effectively the three-bit value representing the index of the x-coordinate within the current tile. (Note, that we don't need to cache the current fine Y value separately as it is incorporated into currentSharedAddress and can be easily extracted.)
... which are updated directly by the following new caching functions:
cacheNametableByte()
cachePaletteIndex()
cacheLowTileByte()
cacheHighTileByte()
cacheTileData()
copyX() and copyY()
incrementX() and incrementY()
... as well as slightly altered versions of these existing functions:
updateAddress(): which mostly corresponds with writing to PPUADDR
updateController(): which mostly corresponds with writing to PPUCTRL
writeScrollByte(): which mostly corresponds with writing to PPUSCROLL
The new updateCaches() function is what coordinates most of the updates above, and is now wired up to the tick() function.
There is a new type alias, namely Address, which centralizes a great deal of the bit-twiddling that is required in many of the new functions above. I have also striven to add detailed comments wherever relevant to help both other readers and $FUTURE_DANIELLE to understand the code.
For now, none of the functions in the call chain in the current renderPixel() function utilize any of the caches. This PR merely introduces these new caching strategies but has no effect of the current rendering implementation. The next PR will be incorporate the new caches.
NOTA BENE: It should be made clear up front that up until this PR, a good deal of the design of this emulator was based on the tutorial that I had originally started following, https://bugzmanov.github.io/nes_ebook/. However, this codebase has been significantly veering away from that design with the last several PRs, and I think I've reached the point where the modeling of PPU registers needs to be revisited. However, in the interest of preserving current behavior and minimizing regressions, I am taking an iterative approach, slowing introducing new (and hopefully improved) code while keeping as much of the old code as possible to be able to play games as I have been able to previously.
This PR is another step forward towards properly emulating the NES, specifically that we ultimately want to be able to cache multiple pieces of data during the rendering phase and compute color values based on them, versus computing colors dependent on the live values of multiple pieces of state. There are several motivations for this:
This set of changes is modeled largely on relevant portions of implementations in two codebases, https://github.com/robb/NES.swift and https://github.com/fogleman/nes. The main things are:
New cached values:
currentSharedAddress
: aUInt16
which mostly corresponds with the so-called V register in the NESnextSharedAddress
: aUInt16
which mostly corresponds with the so-called T register in the NEScurrentNametableByte
: aUInt8
which contains the byte in the current nametable relevant to the current tilecurrentPaletteIndex
: also aUInt8
but is effectively only a two-bit value representing an index into the palette for a given tilecurrentLowTileByte
: also aUInt8
which contains the low byte relevant to the current tilecurrentHighTileByte
: also aUInt8
which contains the high byte relevant to the current tile butcurrentTileData
: aUInt64
with contains data relevant to the current and next tilecurrentFineX
: aUInt8
but is effectively the three-bit value representing the index of the x-coordinate within the current tile. (Note, that we don't need to cache the current fine Y value separately as it is incorporated intocurrentSharedAddress
and can be easily extracted.)... which are updated directly by the following new caching functions:
cacheNametableByte()
cachePaletteIndex()
cacheLowTileByte()
cacheHighTileByte()
cacheTileData()
copyX()
andcopyY()
incrementX()
andincrementY()
... as well as slightly altered versions of these existing functions:
updateAddress()
: which mostly corresponds with writing to PPUADDRupdateController()
: which mostly corresponds with writing to PPUCTRLwriteScrollByte()
: which mostly corresponds with writing to PPUSCROLLThe new
updateCaches()
function is what coordinates most of the updates above, and is now wired up to thetick()
function.There is a new type alias, namely
Address
, which centralizes a great deal of the bit-twiddling that is required in many of the new functions above. I have also striven to add detailed comments wherever relevant to help both other readers and$FUTURE_DANIELLE
to understand the code.For now, none of the functions in the call chain in the current
renderPixel()
function utilize any of the caches. This PR merely introduces these new caching strategies but has no effect of the current rendering implementation. The next PR will be incorporate the new caches.NOTA BENE: It should be made clear up front that up until this PR, a good deal of the design of this emulator was based on the tutorial that I had originally started following, https://bugzmanov.github.io/nes_ebook/. However, this codebase has been significantly veering away from that design with the last several PRs, and I think I've reached the point where the modeling of PPU registers needs to be revisited. However, in the interest of preserving current behavior and minimizing regressions, I am taking an iterative approach, slowing introducing new (and hopefully improved) code while keeping as much of the old code as possible to be able to play games as I have been able to previously.