Simple switch trick

Discuss technical or other issues relating to programming the Nintendo Entertainment System, Famicom, or compatible systems.

Moderator: Moderators

Post Reply
User avatar
Bregalad
Posts: 8036
Joined: Fri Nov 12, 2004 2:49 pm
Location: Caen, France

Simple switch trick

Post by Bregalad »

I wonder something about the simple switch trick on the 6502. As simple switches can be slightly annoying to code in assembly, a trick is to use the CMP instruction to skip what would be the "else" statement in C :
For example, let's do a piece of code which eturns 01 if the thing to test is positive, and $02 else. The "clean" way to do it is :

Code: Select all

 lda something_to_test
 bmi _else
 lda #$01
 jmp _endif
_else
 lda #$02
_endif
 rts
However, doing it in a trickier way in order to save 2 bytes and one label (labels are annoying to define), the trick way is to do that.

Code: Select all

 lda something_to_test
 bmi _else
 lda #$01
 .db $cd (cmp $xxxx opcode)
_else
 lda #$02
 rts
Now what I have been wondering if that the cmp instruction does a dummy read to somewhere. There is nothign bad about it, exept if the read accidentally $2002 or $2007, or one of their numerous mirrors (for example reading $38a7 would also read $2007). In that case the results could be catastrophic, as the screen would be gabraged.

Since the next instruction (to skip) is always a two byte instruction, the one performing this trick has to be very carefull about the argument of the two byte instruction in question. If the argument is included between $20 and $3f, he has to become carefull. Something as innocent as sta $20 for example would in fact read $2085 (mirror $2005) which normally have no effect, but I'd still avoid reading it. Luckily, no two byte instruction has $7 or $e as their last nyble of their opcode, making reading accidentally $2007 technically impossible (maybe I missed one tough). But something like ldx $20 would read $20a2 (so $2002) and this could possibly affect the PPU during rendering.
Am I correct to worry about such things or am I imaginating things.
User avatar
Dwedit
Posts: 4470
Joined: Fri Nov 19, 2004 7:35 pm
Contact:

Post by Dwedit »

Reading 2002 would clear the vblank flag, that's about it. I bet reading joystick input would have a much greater effect, but usually joystick reading is done all at once.
That's pretty cool, I hadn't thought of that trick. It works much better on the Z80, since you can just use a conditional jump with an opposite condition.
Here come the fortune cookies! Here come the fortune cookies! They're wearing paper hats!
User avatar
Disch
Posts: 1848
Joined: Wed Nov 10, 2004 6:47 pm

Re: Simple switch trick

Post by Disch »

Bregalad wrote:(labels are annoying to define)
Which is exactly why I can't live without nameless (or at least local) labels anymore. Having to give unique names to every single label regardless of how minor it is just isn't practical.

Anyway your trick is pretty clever. This kind of thing can lead to really hard to read code, though, as well as problems like undesirable reads (like you mention)


Either/or checks like this can usually be done pretty simply by returning the C flag state rather than setting A. From there you can use C in some calculations to get the desired value of A -- or perhaps use a LUT if calculations would be impractical.

How I would probably approach that same situation:

Code: Select all

  LDA something_to_test
  CLC
  BPL :+
    SEC
: LDA #$00
  ADC #$01
  RTS
Of course since you're checking a single bit position this could be optimized further without needing any branches:

Code: Select all

  LDA something_to_test
  ASL A         ; move N to C
  LDA #$02
  SBC #$00
  RTS
But these methods probably aren't practical in most real world situations. In which case I would probably lean towards the use of a LUT:

Code: Select all

mylut:
  .db $01, $02

myroutine:
  LDX #$00
  LDA something_to_test
  BPL :+
    INX
: LDA mylut,X
  RTS
Granted... this isn't quite as compact as your trick.
User avatar
Bregalad
Posts: 8036
Joined: Fri Nov 12, 2004 2:49 pm
Location: Caen, France

Post by Bregalad »

Unnames labels rock, exept for switches as shown as above because they have to be nested and that becomes a headache. Finding label's name can be a headache as well. However, unnamed labels really REALLY rock for loops, where they are never nested (they are hierarchised instead).

Yes, in the case of the $01/$02 switch there is plenty of way to do this, but you may want to read two different variables instead of just switching between two constants (I actually do this in my current project's game engine). In that case you don't do it just with the carry. And that may not be a routine that returns a value, as shown in the example, but a piece of code directly using the constant (or variables) for actual computing.

Reading the controller could have some effect, but I don't think it would be so bad, anyways.
User avatar
Disch
Posts: 1848
Joined: Wed Nov 10, 2004 6:47 pm

Post by Disch »

I was just viewing SMB's disassembly, and it actually does this trick!

Code: Select all

Bridge_High:
      lda #$06  ;start on the seventh row from top of screen
      .db $2c   ;BIT instruction opcode

Bridge_Middle:
      lda #$07  ;start on the eighth row
      .db $2c   ;BIT instruction opcode

Bridge_Low:
      lda #$09             ;start on the tenth row
waddaya know!
User avatar
blargg
Posts: 3717
Joined: Mon Sep 27, 2004 8:33 am
Location: Central Texas, USA
Contact:

Post by blargg »

It's a pretty common technique on other processors too. You basically want a one-byte branch instruction, so you use a multi-byte instruction which has a benign effect. There are all those two-byte and three-byte unofficial NOPs on the NES that I know Lolo 3 uses one of, though they probably do some kind of memory access. If the 6502 didn't pointlessly set the flags for lda #, you could do this:

Code: Select all

    cmp ...
    lda #value1
    b... skip
    lda #value2
skip:
This works if your branch is based on the C or V flags, since lda # doesn't affect them. You could also use X or Y as a workaround:

Code: Select all

    ldx #value1
    cmp ...
    b... skip
    ldx #value2
skip:
    txa ; if even necessary
It would be interesting to check all the unofficial NOPs in case there's one that doesn't do any extra memory accesses.
Post Reply