Best practices for instancing?

Discuss technical or other issues relating to programming the Nintendo Entertainment System, Famicom, or compatible systems. See the NESdev wiki for more information.

Moderator: Moderators

User avatar
koitsu
Posts: 4201
Joined: Sun Sep 19, 2004 9:28 pm
Location: A world gone mad

Re: Best practices for instancing?

Post by koitsu »

pwnskar wrote:

Code: Select all

ldy #10
lda ($90ff), y
If I've understood correctly, this would give me the same result as:

Code: Select all

lda $910f
Not exactly, but close -- and a common stumbling point, so don't feel bad. There are a few forms of indirect addressing on the 6502, but WRT to what you just described, there's mainly two:

1. Indexed indirect (sometimes called "pre-indexed") e.g. lda ($12,x) -- you can only use the X register for this
2. Indirect indexed (sometimes called "post-indexed"), e.g. lda ($12),y -- you can only use the Y register for this

The links there explain the differences with code. The difference is that one adds the index register *before* indirection, the other adds the index register *after* indirection. I should also note that you can't use absolute 16-bit addresses with either of these modes, only zero page.

Don't confuse either of those with simple absolute indexed addressing, e.g. lda $1234,x or lda $1234,y, which I think is what what you were thinking of with your above code (i.e. no indirection used).

I think this 6502 opcode chart might have mistakes in it, I forget, but you can see what available addressing modes there are per-opcode. Look up LDA for example. Once you see the available options, it should become a bit more clear.
russellsprouts
Posts: 53
Joined: Sun Jan 31, 2016 9:55 pm

Re: Best practices for instancing?

Post by russellsprouts »

I hope that chart doesn't have mistakes -- I've referenced it a lot. The only thing I know it's missing is that both PLA and TSX set the S and Z flags.
User avatar
koitsu
Posts: 4201
Joined: Sun Sep 19, 2004 9:28 pm
Location: A world gone mad

Re: Best practices for instancing?

Post by koitsu »

One of the common go-to 6502 resources/charts/opcode descriptions is known to have problems/bugs/issues. I just can't be bothered to remember which one.
pwnskar
Posts: 119
Joined: Tue Oct 16, 2018 5:46 am
Location: Gothenburg, Sweden

Re: Best practices for instancing?

Post by pwnskar »

Hmmm.. I think I might be getting contradicting explanations from you guys about indirect indexed addressing?
koitsu wrote: 1. Indexed indirect (sometimes called "pre-indexed") e.g. lda ($12,x) -- you can only use the X register for this
2. Indirect indexed (sometimes called "post-indexed"), e.g. lda ($12),y -- you can only use the Y register for this

The links there explain the differences with code. The difference is that one adds the index register *before* indirection, the other adds the index register *after* indirection. I should also note that you can't use absolute 16-bit addresses with either of these modes, only zero page.
tokumaru wrote:
pwnskar wrote:

Code: Select all

; pointer_lo = $ff
; pointer_hi = $00

ldy #$10
lda (pointer_lo), y
and that would result in me getting the value of $000f rather than $010f.
That's not how it works, you should be getting the value at $010f. Wrapping only occurs in ZP indexed addressing.
So koitsu, from your explanation I actually should loop around to get the value from $000f while tokumaru says I should be getting the value from $010f. I'm going to try this out and see what I get. :P
User avatar
tokumaru
Posts: 12427
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: Best practices for instancing?

Post by tokumaru »

Koitsu is saying that the *pointers* themselves have to be in ZP, but they can point to anywhere from $0000 to $FFFF.
User avatar
koitsu
Posts: 4201
Joined: Sun Sep 19, 2004 9:28 pm
Location: A world gone mad

Re: Best practices for instancing?

Post by koitsu »

Both his and my explanations are correct. Look very carefully at the locations of the ZP variables being used in his examples vs. my examples.

I really hate code quotes on this forum, so I'm just going to do it this way:

Code: Select all

Memory contents:

$0000 = $ee
$00ff = $34
$0100 = $12
$1234 = $aa
$ee39 = $bb

Code:

pointer_lo = $ff

ldy #5
lda (pointer_lo),y
You know what's going on with ldy #5 so no need to explain that.

The first thing the CPU is going to do is read a byte (the "low byte") from ZP location $ff. That value is $34.

The next thing the CPU is going to do is read a byte (the "high byte") from ZP location $ff+1. What address do you think the CPU is going to read from (for the "high byte" of the effective address it's trying to calculate), and what value do you think it's going to get?

If you answered $100 (thus a value of $12 for the "high byte"), you'd be incorrect. The CPU will actually read the 2nd byte from ZP location $00, thus value $ee.

This is because of page wrapping; ZP is called zero page for a reason: it represents memory $0000-$00FF. With ZP addressing, you cannot "wrap" from $ff ($00ff) to $100 ($0100) -- the addressing has to stay within the $00xx range. This concept applies to all ZP addressing modes.

So this is what actually happens with the above code and those values in memory:

The CPU reads the low byte of the effective address from ZP location $ff. It gets a value of $34.
The CPU reads the high byte of the effective address from ZP location $00. It gets a value of $ee.
The CPU then adds Y to that effective address; $ee34 + 5 = $ee39.
The CPU then reads the byte located at memory location $ee39 (value $bb) and puts that in the accumulator.

If you want me to explain indexed indirect ("pre-indexed", e.g. lda ($ff,x) with an example, I can do that too. The situation is more or less the same, just that the addition of X to the effective address is being done at a different time (earlier in the process, rather than later). There was a discussion on the forum recently about how the "pre-indexed" mode on 6502 is substantially less useful than the "post-indexed" mode, but I can't be bothered to find it.

All of this is entirely different if you're using absolute addressing. But, sadly, there is no indirect indexed or indexed indirect addressing mode that uses absolute addresses on the 6502 -- i.e. there is no lda ($1234),y or lda ($1234,x). You're stuck using ZP.

Likewise, understanding page wrapping as a concept is equally important. A "page wrap" is when the low byte of an address causes the upper byte to have to be incremented. A great example is this simple code:

Code: Select all

ldx #2
lda $80ff,x
Here, the CPU will calculate an effective address of $80ff + 2 ($8101), thus reading from memory location $8101. This caused a "page wrap" because $ff+1 had to increase the value in the upper byte of the effective address ($80 had to become $81). This costs 1 extra CPU cycle, too.

This type of math that happens is actually something that you'll have to do yourself when you get into more advanced situations, like if you want to add 2 to an unsigned 16-bit value (2 bytes) in memory and ensure that the upper byte of the 16-bit value gets incremented properly. There's a really cute/clever way to this on 6502 that surprises people when they see it. I consider this "advanced" material and isn't immediately necessary for understanding CPU basics, but it works like this:

Code: Select all

Memory contents:

$0014 = $fe
$0015 = $20

Subroutine:

add_a_to_14:
  clc
  adc $14
  sta $14
  lda $15
  adc #0
  sta $15
  rts

Main program:

  lda #4
  jsr add_a_to_14
Before the jsr, the 16-bit value in $0014/0015 is $20fe ($0014 = $fe, $0015 = $20)

After the jsr, the 16-bit value in $0014/0015 is $2102 ($0014 = $02, $0015 = $21)

If you want to know how this trick works, just ask. You might think the adc #0 serves no purpose, but it's incredibly important. Hint: it involves use of the carry flag and what adc does both WITH it and TO it. Despite having done 65xxx for a lot of my life (though I'm quite rusty), I still find stuff like this awesome/cool/clever.

You'll find things like this incredibly useful when needing to do things like math on 16-bit values that are used for PPU RAM addressing (e.g. what ends up in $2006).

Edit: forgot the important lda $15 before the adc #0. Yikes!
Last edited by koitsu on Sat Oct 27, 2018 3:04 pm, edited 1 time in total.
pwnskar
Posts: 119
Joined: Tue Oct 16, 2018 5:46 am
Location: Gothenburg, Sweden

Re: Best practices for instancing?

Post by pwnskar »

koitsu wrote: Before the jsr, the 16-bit value in $0014/0015 is $20fe ($0014 = $fe, $0015 = $20)

After the jsr, the 16-bit value in $0014/0015 is $2102 ($0014 = $02, $0015 = $21)

If you want to know how this trick works, just ask. You might think the adc #0 serves no purpose, but it's incredibly important. Hint: it involves use of the carry flag and what adc does both WITH it and TO it. Despite having done 65xxx for a lot of my life (though I'm quite rusty), I still find stuff like this awesome/cool/clever.

You'll find things like this incredibly useful when needing to do things like math on 16-bit values that are used for PPU RAM addressing (e.g. what ends up in $2006).
That part I think I might understand. I should hope so, because I'm doing a lot of pointer increments that way, as well as some 16-bit collision detection that I've decided not to use, as my game has no scrolling. :)

Am I right to believe that the carry from the first adc gets added onto the second one and then cleared?

I've been doing a lot of refactoring today and I'm soon onto testing replacing all my:

Code: Select all

ldy #0
lda (some_pointer_lo), y
... do stuff with accumulator

lda some_pointer_lo    ; increment pointers
clc
adc #1
sta some_pointer_lo
lda some_pointer_hi
adc #0
sta some_pointer_hi

lda (some_pointer_lo), y
... do stuff again with accumulator
with:

Code: Select all

ldy #0
lda (some_pointer_lo), y
... do stuff with accumulator

iny
lda (some_pointer_lo), y
... do stuff again with accumulator
If I've understood correctly, this should be possible as long as both pointer variables (in this case some_pointer_lo and some_pointer_hi) are on ZP and y never goes above 255?

Again, thank you so much for all the detailed explanations, they are very much appreciated!

Cheers!
User avatar
koitsu
Posts: 4201
Joined: Sun Sep 19, 2004 9:28 pm
Location: A world gone mad

Re: Best practices for instancing?

Post by koitsu »

pwnskar wrote:That part I think I might understand. I should hope so, because I'm doing a lot of pointer increments that way, as well as some 16-bit collision detection that I've decided not to use, as my game has no scrolling. :)

Am I right to believe that the carry from the first adc gets added onto the second one and then cleared?
Yup, correct! But it seems I made a catastrophic mistake in my previous code: I forgot an lda $15 before the adc #0. I've edited my post to fix that. Something easily overlooked when we get "used" to seeing a particular routine. Quite a major mistake though. :-)

Detailed explanation for future readers who aren't sure:

clc clears the carry. Next, the adc $14 gets executed. Here, a couple things happen. This is how I mentally envision it:

1. The CPU does: $fe (value in $14) + 4 (accumulator) + 0 (carry) == result of $102
2. Value $102 is too large for an 8-bit register, which is known as "unsigned overflow" in this particular case. Because of this, the carry flag c gets set
3. Likewise, in this situation, the two's complement math done does not contain an error, so the overflow flag v is clear. (We aren't using the overflow flag in this operation, so it's irrelevant, but I wanted to note that it does get cleared here -- two's complement math is one of the biggest struggling points there is on the 65xxx architecture, discussed heavily over the years on this forum)
4. The final result: the accumulator holds the value $02, and the carry flag is set

Next, sta $14 writes $02 to $14. Following that, we have lda $15, so the accumulator now holds $20. Next, we have adc #0. The same process as described above happens, except this is the resulting math:

1. The CPU does: 0 (value in operand) + $20 (value in accumulator) + 1 (carry) == result $21
2. Value $21 fits into an 8-bit register, so carry is clear. Likewise, overflow is also clear
3. The final result: accumulator holds the value $21, carry flag is clear, overflow flag is clear

Finally we do sta $15, which writes $21 to $15.

Thus, our 16-bit pointer at $14/$15 now contains the value $2102, which is exactly what we wanted: $20fe + 4 = $2102. In essence, we use the carry flag as a way to handle the "page wrap" (of our math) for us.
pwnskar wrote:If I've understood correctly, this should be possible as long as both pointer variables (in this case some_pointer_lo and some_pointer_hi) are on ZP and y never goes above 255?
The former part of your sentence is correct: basically, ensure that some_pointer_lo is not ever $ff, otherwise this would cause the CPU to read the high byte of the effective address from $00, not $0100 like your brain might think.

The latter part of your sentence is incorrect: Y can safely be any value (0-255). The CPU, when adding Y to the effective address (calculated from reading the low byte of the address from some_pointer_lo and the high byte of the address from some_pointer_lo+1), can handle wrapping (ex. $20ff->$2100) just fine.

For example: in my previous post's first code block, if you changed ldy #5 to ldy #$ff, it would still work fine (the final effective address would be $ee34 + $ff == $ef33).

The short of it is: when working with ZP, always remember that ZP addressing stays within page 0 (the $00xx region, or $0000-00FF) and can never "wrap" into page 1 ($01xx, or $0100-01FF). Absolute addressing can/will page wrap, for nice/clean/linear 16-bit addressing, but there's 1 CPU cycle penalty when a page wrap happens.
pwnskar
Posts: 119
Joined: Tue Oct 16, 2018 5:46 am
Location: Gothenburg, Sweden

Re: Best practices for instancing?

Post by pwnskar »

koitsu wrote:
pwnskar wrote:If I've understood correctly, this should be possible as long as both pointer variables (in this case some_pointer_lo and some_pointer_hi) are on ZP and y never goes above 255?
The former part of your sentence is correct: basically, ensure that some_pointer_lo is not ever $ff, otherwise this would cause the CPU to read the high byte of the effective address from $00, not $0100 like your brain might think.

The latter part of your sentence is incorrect: Y can safely be any value (0-255). The CPU, when adding Y to the effective address (calculated from reading the low byte of the address from some_pointer_lo and the high byte of the address from some_pointer_lo+1), can handle wrapping (ex. $20ff->$2100) just fine.

For example: in my previous post's first code block, if you changed ldy #5 to ldy #$ff, it would still work fine (the final effective address would be $ee34 + $ff == $ef33).

The short of it is: when working with ZP, always remember that ZP addressing stays within page 0 (the $00xx region, or $0000-00FF) and can never "wrap" into page 1 ($01xx, or $0100-01FF). Absolute addressing can/will page wrap, for nice/clean/linear 16-bit addressing, but there's 1 CPU cycle penalty when a page wrap happens.
Yes, what I meant was it should work as long as i don't increment y past 255, because that would have it wrap back to a lower number. So if I've already incremented y to 255 and my pointers point to say $9000, the next time I do iny it would wrap around to 0 and I would end up reading the value of$9000 instead of $9100? Or would the carry from my iny be used when I do lda (some_pointer_lo), y ?
User avatar
koitsu
Posts: 4201
Joined: Sun Sep 19, 2004 9:28 pm
Location: A world gone mad

Re: Best practices for instancing?

Post by koitsu »

pwnskar wrote:Yes, what I meant was it should work as long as i don't increment y past 255, because that would have it wrap back to a lower number. So if I've already incremented y to 255 and my pointers point to say $9000, the next time I do iny it would wrap around to 0 and I would end up reading the value of$9000 instead of $9100? Or would the carry from my iny be used when I do lda (some_pointer_lo), y ?
First question: yes.

Second question: no. There's no way in that situation the effective address could ever be $9100 because registers on the 6502 are only 8-bit; Y can only range from $00 to $ff (0-255), thus $9000+Y can only range from $9000-90FF. Here's some code/memory to make it more clear. I made two separate pointers to show you what happens:

Code: Select all

Memory contents:

$000a = $00
$000b = $90
$0010 = $50
$0011 = $90

$9000 = $aa
$9050 = $bb
$90ff = $cc
$914f = $dd

Code:

pointer1 = $0a
pointer2 = $10

ldy #$ff            ; Y = $ff (255)
lda (pointer1),y    ; Effective address is $9000+Y, hence $90ff, thus a value of $cc
iny                 ; Y = $00 (0)
lda (pointer1),y    ; Effective address is $9000+Y, hence $9000, thus a value of $aa

ldy #$ff            ; Y = $ff (255)
lda (pointer2),y    ; Effective address is $9050+Y, hence $914f, thus a value of $dd
iny                 ; Y = $00 (0)
lda (pointer2),y    ; Effective address is $9050+Y, hence $9050, thus a value of $bb
I don't think this is what you're asking, but: if you're asking if the iny opcode affects the carry flag, the answer is no -- it only affects the n (negative) and z (zero) CPU flags (a.k.a. P).

This opcode chart reports that "S" and "Z" get changed; for whatever reason, they call the negative flag (bit 7 of P) "S" for "sign bit", which is confusing because S usually refers to the stack pointer. *cringing*
Post Reply