-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackground.s
More file actions
519 lines (448 loc) · 17.8 KB
/
Copy pathbackground.s
File metadata and controls
519 lines (448 loc) · 17.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
;; Reset the background cycle so a new column can be loaded even if the cycle
;; was stopped at the end of the loading of the previous screen.
.macro RESET_BACKGROUND_CYCLE
lda #$FF
sta Background::zp_background_cycle
.endmacro
;; All functions and related variables for keeping up with the background
;; elements. This is by far the most extensive and complex piece of code from
;; the scrolling examples, and it's the core of the render engine for them.
;;
;; Documentation has been added for it as much as possible, so follow along to
;; better grasp how to use and grok this piece of code.
.scope Background
;; Current value for the scroll on the X axis.
zp_scroll = $95
;; Instead of attempting to load a column every time the player movement
;; requires it, stall things a bit (as much as defined in
;; `BACKGROUND_CYCLE_MAX`). This allows for loading columns at a slower
;; pace. This cycle is diregarded if you set it to
;; `DISABLED_BACGROUND_CYCLE`, which it just means something like "no more
;; columns to load".
zp_background_cycle = $96
;; See `zp_background_cycle`.
DISABLED_BACKGROUND_CYCLE = $FE
BACKGROUND_CYCLE_MAX = $02
;; The offset for metatile rows. That is, from where should the engine start
;; counting rows of metatiles. By default it's 0, but it could be set to
;; something else to reserve top space for a HUD or something similar.
;;
;; NOTE: configurable.
.ifndef BACKGROUND_ROW_OFFSET
BACKGROUND_ROW_OFFSET = 0
.endif
;; The maximum row coordinate this engine is allowed to go. That is, until
;; which row each column is supposed to be rendered. By default it's $0F,
;; but it could be set to something else to allow a status element down the
;; screen.
;;
;; NOTE: configurable.
.ifndef BACKGROUND_ROW_MAX
BACKGROUND_ROW_MAX = $0F
.endif
;; Current column (or the next to be loaded by functions like
;; `load_column`).
zp_cur_column = $75
;; Current row (or the next to be loaded by functions like `load_column`).
zp_cur_row = $76
.proc init
;; Load the first level.
ldx #$00
JAL load_level_x
;; NOTE: returned value ignored as the level *must* have loaded at this
;; point.
.endproc
;; Resets all the variables which touch in any way the loading of columns,
;; levels, etc. This is already being called by `load_level_x`, so outside
;; of this usage you shouldn't be calling it.
.proc reset
RESET_BACKGROUND_CYCLE
lda #$00
sta zp_scroll
sta Buffer::zp_vram_idx
sta zp_cur_column
lda #BACKGROUND_ROW_OFFSET
sta zp_cur_row
rts
.endproc
;; Load the level indexed by the `x` register. This function requires that
;; the PPU is disabled. If this is not the case, then it sets PPU::zp_mask
;; in a way so to disable the PPU on the next `nmi` call, and finally sets
;; `a` to 0 (i.e. level could not be loaded). Otherwise you can expect the
;; `a` register to contain 1 after calling this function.
;;
;; Moreover, the screen pointer is going to be left at the start of the
;; second screen, so future `load_column` calls can rely on this pointer
;; being set properly.
.proc load_level_x
;; Is the PPU set to be disabled?
lda PPU::zp_mask
and #%00111100
beq @load_level
;; The PPU is not disabled, set the mask so the PPU is disabled.
lda #$00
sta PPU::zp_mask
;; Reset the nametable being used.
lda #%11111100
and PPU::zp_control
sta PPU::zp_control
;; Enable the `ppu` flag so these changes are not ignored.
lda Globals::zp_flags
ora #%01000000
and #%11011111
sta Globals::zp_flags
lda #0
rts
@load_level:
;; Start by resetting any variables that touch on the loading of columns
;; in any way.
jsr Background::reset
;; Reset all relevant flags.
lda #%11000011
and Globals::zp_flags
sta Globals::zp_flags
;; Initialize the screen pointer by using the argument as passed through
;; the `x` register.
lda levels_lo, x
sta Metatile::zp_screen_ptr
lda levels_hi, x
sta Metatile::zp_screen_ptr + 1
@loop:
jsr load_column
;; NOTE: returned value ignored.
;; Increase the column being used and loop if we are not at the end of
;; the screen yet.
lda #BACKGROUND_ROW_OFFSET
sta zp_cur_row
inc zp_cur_column
lda zp_cur_column
cmp #$10
bne @loop
;; Advance the screen pointer so it points to the second screen.
ADVANCE_SCREEN_PTR(1)
;; Enable back the PPU on the next NMI call.
lda #%00011110
sta PPU::zp_mask
;; And set the `ppu`, `level` and `nametable` flags.
lda #%01100100
ora Globals::zp_flags
sta Globals::zp_flags
;; Reset things again just in case.
jsr Background::reset
lda #1
rts
.endproc
;; Translates the given metatile coordinate byte as given on the `a`
;; register and sets the first four argument variables from it. A "metatile
;; coordinate byte" is the first byte of a metatile reference on the screen
;; definition. Hence, the high nibble contains the Y metatile coordinate,
;; and the low nibble contains the X metatile coordinate.
;;
;; After calling this function you will have the following:
;;
;; - zp_arg0: Y metatile coordinate (i.e. the high nibble as given on the
;; `a` register shifted 4 times right).
;; - zp_arg1: X metatile coordinate (i.e. the low nibble as given on the
;; `a` register and the high nibble zero'ed out).
;; - zp_arg2: low byte for the corresponding PPU base address.
;; - zp_arg3: high byte for the corresponding PPU base address.
.proc translate_mt_coordinates_to_arguments
;; Save the X metatile coordinate. This is also saved into a temporary
;; variable so it can be messed up when figuring out the PPU address.
tay
and #%00001111
sta Globals::zp_arg1
sta Globals::zp_tmp0
;; Save the Y metatile coordinate.
tya
and #%11110000
lsr
lsr
lsr
lsr
sta Globals::zp_arg0
;; Now that the metatile coordinates have been isolated, let's go for
;; the PPU address.
;; The initial value for the high byte of the PPU address depends on the
;; nametable being used. For this we can take advantage of the fact that
;; this information is on bit 2 of the global flags. If we mask this
;; out, and add to it the base value of `$20`, we get either `$20` or
;; `$24`, which is the base high byte for either nametable.
lda #%00000100
and Globals::zp_flags
clc
adc #$20
sta Globals::zp_arg3
;; Every 4 rows the high byte of the PPU address increases by one. This
;; means that with the Y metatile position, divide it by 4 (shift right
;; twice), and you can add that to the high byte directly.
lda Globals::zp_arg0
lsr
lsr
clc
adc Globals::zp_arg3
sta Globals::zp_arg3
;; After the high byte has been sorted out, notice that for the low byte
;; the base can only be the four values as defined in `ppu_offsets`,
;; as they repeat every four times.
lda Globals::zp_arg0
and #%00000011
tax
lda ppu_offsets, x
;; To that base we need to apply the X coordinate. Since each metatile
;; takes two tiles on the PPU, we can simply take the value on the X and
;; shift it left once. And finally add the base low byte from the row
;; with this shifted X value.
asl Globals::zp_tmp0
clc
adc Globals::zp_tmp0
sta Globals::zp_arg2
rts
ppu_offsets:
.byte $00, $40, $80, $C0
.endproc
;; Load the column as indexed by `Background::zp_cur_column` and
;; `Background:zp_cur_row`. This means that this function can be called at
;; any time, and so rendering columns in multiple iterations is indeed
;; possible. This is important to keep a small VRAM buffer.
;;
;; Returns 1 if the column has been completely loaded, 0 if there are rows
;; still to be loaded for the current column after calling this function.
;;
;; This function also sets the `ppu` and the `column` flags as expected in
;; each scenario, but it will *not* call functions such as
;; `prepare_next_column`, as that is to be done by the caller if necessary.
.proc load_column
@start:
;; First, compute the offset of the current metatile definition. That
;; is, what's left from the current row until the Y position of the next
;; metatile to push. First things first, check whether we are at the end
;; of the screen definition.
ldy #0
lda (Metatile::zp_screen_ptr), y
cmp #$FF
bne @fetch_offset
;; We are actually at the end of the screen definition. Then the offset
;; is simply the current row until the end. If that turns out to be
;; zero, then we are done.
lda #BACKGROUND_ROW_MAX
sec
sbc zp_cur_row
beq @done
sta Globals::zp_idx
jmp @default_loop
@fetch_offset:
;; Otherwise, if we weren't at the end of the screen definition, fetch
;; the offset now. Note that if the offset is zero, then we can simply
;; go and push the metatile definition.
jsr find_offset
beq @push
;; This is an inner loop, which pushes the default metatile
;; `Globals::zp_idx` - 1 times.
sta Globals::zp_idx
@default_loop:
;; Make up a fake metatile position at the currently evaluated pair of
;; row/column. Afterwards just push it. Doing all this dance is
;; certainly not too expensive, but at the same time it could be easily
;; optimized. This is not done out of simplicity.
lda zp_cur_row
asl
asl
asl
asl
ora zp_cur_column
jsr translate_mt_coordinates_to_arguments
lda #$00
jsr Buffer::push_metatile
beq @end
;; The metatile was successfully pushed, now increase the row and check
;; if we have to push another default metatile.
inc zp_cur_row
dec Globals::zp_idx
bne @default_loop
;; We are done pushing default metatiles. Check if we are done with the
;; column entirely.
lda zp_cur_row
cmp #BACKGROUND_ROW_MAX
beq @done
;; Push the metatile pointed by the screen pointer.
@push:
;; Pick up the current metatile position byte and set the arguments as
;; expected by `Buffer::push_metatile`,
;; `Collision::set_background_collision` and related functions.
ldy #0
lda (Metatile::zp_screen_ptr), y
jsr translate_mt_coordinates_to_arguments
;; And now call `push_metatile` with the proper metatile definition.
ldy #1
lda (Metatile::zp_screen_ptr), y
jsr Buffer::push_metatile
beq @end
;; The two bytes for the metatile have been consumed, now advance the
;; screen pointer two bytes to point to the next metatile for the
;; screen.
ADVANCE_SCREEN_PTR(2)
;; Increase the row being evaluated and go for another iteration if this
;; wasn't the last row for the column.
inc zp_cur_row
lda zp_cur_row
cmp #$10
bne @start
@done:
;; Set the `ppu` flag and unset the `column` one. That is, a column is
;; now fully buffered and the PPU needs to update things.
lda Globals::zp_flags
ora #%01000000
and #%11101111
sta Globals::zp_flags
lda #1
rts
@end:
;; Set the `ppu` and the `column` flags. Even if the column is not fully
;; buffered yet, we can already consume the VRAM buffer and empty it.
;; Setting the `column` flag will ask code elsewhere to beware of a
;; pending column to be loaded and act on it (e.g. by calling
;; `load_column` again to finish the task).
lda Globals::zp_flags
ora #%01010000
sta Globals::zp_flags
lda #0
rts
.endproc
;; Load a new column if the engine detects that it was needed. That is, it
;; will ask to load a column unless a column is already being rendered, or
;; the background cycle has not been consumed yet (or disabled entirely).
.proc load_column_if_needed
;; If the `column` or the `level` flags are set, then we can skip the
;; loading altogether.
lda #%00011000
and Globals::zp_flags
bne @end
;; Is the background cycle disabled? If so just go to the end.
lda Background::zp_background_cycle
cmp #DISABLED_BACKGROUND_CYCLE
beq @end
;; Instead of loading a column every time it can be loaded, we add a bit
;; of a hiccup (i.e. "background cycle") so an entire level is not
;; loaded right away after the player moves one bit.
inc Background::zp_background_cycle
beq @do_load
lda Background::zp_background_cycle
cmp #BACKGROUND_CYCLE_MAX
bne @end
lda #$00
sta Background::zp_background_cycle
@do_load:
;; Load the column and increase the current column if it was fully
;; buffered. Otherwise it will be picked up by the `update` function.
jsr load_column
beq @end
JAL prepare_next_column
@end:
rts
.endproc
;; Load the next background column. You want to call this function on the
;; outside whenever a new column might be needed. This function will
;; eventually call `load_column_if_needed`, and it will also handle
;; overflows on the scroll values, changing the nametable being used, etc.
;;
;; Because of the above, this is the most suitable function to call whenever
;; you want to load a new column from the outside code, even if you are
;; unsure whether it's really needed.
.proc load_next_background
;; Was there an overflow on the scroll's value?
lda Background::zp_scroll
bne @load_column_if_needed
;; Reset the background cycle so the next screen is picked up.
RESET_BACKGROUND_CYCLE
;; Flip the nametable being used so the scroll makes sense for the
;; current context. After that, skip the loading of the column because
;; otherwise it would start loading on the currently displayed nametable
;; and that would make for weird glitches.
lda PPU::zp_control
eor #%00000001
sta PPU::zp_control
jmp @update_ppu_and_quit
@load_column_if_needed:
jsr Background::load_column_if_needed
@update_ppu_and_quit:
;; Regardless of whether the loading of the column happened or not, the
;; scroll register has to change. Hence, set the `ppu` flag so at least
;; the PPU::SCROLL register is updated.
lda Globals::zp_flags
ora #%01000000
sta Globals::zp_flags
rts
.endproc
;; Reset all the variables in preparation for the loading of a new column.
;; This function assumes that a column has already been fully buffered; and
;; it will also handle the case where it was the last column of the
;; screen/level by poking the right flags.
.proc prepare_next_column
;; The row index always has to be set to zero.
lda #BACKGROUND_ROW_OFFSET
sta zp_cur_row
;; Is this the last column? If not, just quit.
lda zp_cur_column
cmp #$0F
bne @end
;; Flip the nametable to be used.
lda Globals::zp_flags
eor #%00000100
sta Globals::zp_flags
;; Mark the background cycle as over. This will make functions like
;; `load_column_if_needed` to refrain from loading a new column
;; altogether.
lda #DISABLED_BACKGROUND_CYCLE
sta zp_background_cycle
;; Reset the column to use. Note that the `@end` section will increase
;; the column to be used.
lda #$FF
sta zp_cur_column
;; If we were at the last column, we were out of metatiles for the
;; current screen. Hence, move to the next screen if possible.
ADVANCE_SCREEN_PTR(1)
;; Do we have another "$FF" byte? If so then we are at the end of the
;; level entirely.
ldy #0
lda (Metatile::zp_screen_ptr), y
cmp #$FF
bne @end
;; Mark the end of the level.
lda #%00001000
ora Globals::zp_flags
sta Globals::zp_flags
@end:
;; Increase the current column and quit.
inc zp_cur_column
rts
.endproc
;;;
;; Find the distance between the currently evaluated row and the one pointed
;; by the given argument. The argument is a byte where the Y value is
;; located on the high nibble, as expected from a metatile definition.
;;
;; NOTE: the argument and the returned value are on the `a` register.
.proc find_offset
;; Are we at the right column?
sta Globals::zp_arg0
and #%00001111
cmp zp_cur_column
bne @to_the_end
;; We are: go until the current Y index.
lda Globals::zp_arg0
lsr
lsr
lsr
lsr
jmp @compute
@to_the_end:
;; We aren't: go until the last row.
lda #BACKGROUND_ROW_MAX
@compute:
;; Subtract the current row with the one we are trying to reach.
sec
sbc zp_cur_row
rts
.endproc
.endscope