aboutsummaryrefslogtreecommitdiff
path: root/crates/nasm
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <mssola@mssola.com>2026-08-18 16:39:37 +0200
committerMiquel Sabaté Solà <mssola@mssola.com>2026-08-18 16:39:37 +0200
commitfcd4c9a267b407e653ed6b21ae87f219f3e4c4af (patch)
tree0b5f3e9bf99ab7377fa0bb6d204661ac8fbe3380 /crates/nasm
parentd479b0fbd3778f5b6fc8b63f46aa0c5364e4fe7e (diff)
downloadtools.nes-fcd4c9a267b407e653ed6b21ae87f219f3e4c4af.tar.gz
tools.nes-fcd4c9a267b407e653ed6b21ae87f219f3e4c4af.zip
Add global labels
These are labels that are declared by prefixing a '#' symbol to the name, and it allows the label to be declared at the global scope instead of the current one. This is a feature which is not to be abused so to not make scopes pointless, but it can be quite handy with some optimizations while not abandoning scopes completely. Signed-off-by: Miquel Sabaté Solà <mssola@mssola.com>
Diffstat (limited to 'crates/nasm')
-rw-r--r--crates/nasm/README.md63
1 files changed, 63 insertions, 0 deletions
diff --git a/crates/nasm/README.md b/crates/nasm/README.md
index f96e0bb..1cfea8f 100644
--- a/crates/nasm/README.md
+++ b/crates/nasm/README.md
@@ -98,6 +98,69 @@ missing in `ca65`. In this case, `nasm` has the `--prelude` flag, which will
print some code that can fill the gaps when running `ca65` with some
nasm-specific code. See below.
+#### Defining global labels
+
+For optimization reasons, sometimes it's necessary to write a label that can be
+accessed globally, regardless of the current scope. The
+[jetpac.nes](https://git.mssola.com/nes/jetpac.nes/) game has a good example of
+this need on its `enemies.s` file. In there, we define a function pointer that
+is set to the current function handler for the enemies' algorithm. Then enemy
+handling can go along like this:
+
+```asm
+ ;; previous code
+
+ lda #.hibyte(@return_from_movement_handler - 1)
+ pha
+ lda #.lobyte(@return_from_movement_handler - 1)
+ pha
+ jmp (zp_movement_fn)
+
+@return_from_movement_handler:
+ ;; Rest of the code after enemy movement has been handled.
+```
+
+That is, we push onto the stack the address of `@return_from_movement_handler`,
+and then `jmp` to the function handler. Then, the function handler can simply
+call `rts` and everything will be fine. As explained in the source code, other
+techniques like trampolines or the "rts trick" were not desirable on this
+context.
+
+Moreover, note that a simple `jmp` was not possible from the context of enemy
+handlers, as `@return_from_movement_handler` is inside of the scope of the
+`Enemies::update` proc. For this game the performance penalty with this setup
+was acceptable, but note that it's inside of a loop, and these extra cycles are
+given for each enemy.
+
+But this is potentially bad if your game is more tight on the cycle count and
+every cycle you can squeeze matters. For this reason, `nasm` has the possibility
+to define labels globally. That is, the above example could be rewritten like
+this:
+
+```asm
+ jmp (zp_movement_fn)
+
+#@return_from_movement_handler:
+ ;; Rest of the code after enemy movement has been handled.
+```
+
+Notice the leading '#' symbol. This tells `nasm` that
+`@return_from_movement_handler` should be defined at the global scope,
+regardless of the current one (hence, it's no longer hidden inside of the
+`Enemies::update` scope). With this, then each handler can switch from an `rts`
+to:
+
+```asm
+ jmp @return_from_movement_handler ; or hide it on an 'RTS_FROM_ENEMY_HANDLER' macro.
+```
+
+In total, for each iteration loop, the setup would save 10 cycles (2 cycles x
+lda, 3 cycles x pha), and replacing each handler's `rts` with a `jmp` would save
+3 cycles (6 cycles 'rts' - 3 cycles 'jmp'). That is, given that this game
+allowed 4 enemies at once, then each call to `Enemies::update` could save 13
+cycles x 4 enemies = 52 cycles. Not a crazy amount, yes, but this optimization
+is now so easy to pull that it's well worth it.
+
#### The `__fallthrough__` pseudo-instruction
It's a [well-known