Сайт Влада

Как исправить баг со сдвигом очереди Pattern Load Cues

Автор гида: vladikcomper

Pattern Load Cues, или сокращенно PLC, система для подгрузки сжатого в Nemesis арта. PLC загружает арт в начале уровней, а также в некоторые моменты игры (например, подгрузка арта сигнпоста или боссов в конце уровней). Эта система устроена довольно сложно, и в Соник 1 и 2 в ней присутствуют баги, один из которых описан в гиде «Как исправить баг с загрузкой Pattern Load Cues».

Еще один баг связан с механизмом сдвига PLC очереди.

Вступление

У системы PLC есть буффер, или правильнее сказать, очередь (queue, тип структуры данных), которая занимает $60 байт в памяти. В очередь записываются и поочередно исполняются команды на загрузку арта, каждая команда занимает 6 байт: первые 4 байта - исходный оффсет загружаемого арта в РОМе, оставшиеся 2 - оффсет VRAM, куда следует загрузить арт. Посмотреть как выглядят эти команды можно в файле _inc\Pattern Load Cues.asm в Соник 1.

Очередь PLC рассчитана на $10 (16) команд. Выполнение команд в очереди начинается с ее начала, как только команда выполнена, вся очередь сдвигается: первая команда замещается второй, вторая - третьей, и так далее. Говоря другими словами, команды в очереди сдвигаются. Однако, из-за ошибке в коде, когда буффер заполнен всеми $10 командами, последняя команда не очищается при сдвиге, что приводит к тому, что вся очередь заполняется остатками последней команды и игра попадает в бесконечный цикл, обрабатывая одну и ту же команду снова и снова.

С этим можно столкнуться в Соник 1, если добавить в PLC_GHZ хотя бы две новые команды, очередь заполнится до максимума, и игра зависнет на Title Cards, хотя количество команд в очереди будет в рамках допустимых значений.

Причина бага

Давайте рассмотрим код сдвига очереди, чтобы понять суть программной ошибки. В качестве примера, я приведу код из Sonic 2 Xenowhirl's Disassembly, так как он лучше раскомментирован, чем код из Соник 1. Заметьте, что сам код и в Соник 1, и в Соник 2, абсолютно одинаков.

; ===========================================================================
; pop one request off the buffer so that the next one can be filled

; loc_177A:
ProcessDPLC_Pop:
	lea	(Plc_Buffer).w,a0
	moveq	#$15,d0

-	move.l	6(a0),(a0)+
	dbf	d0,-
	rts
 

Во-первых, этот код перемещает $16*4 = $58 байт, что даже не соответствует целому числу команд (не делится на 6). Он должен был перемещать $5A байт, т.е. $F команд. Поскольку код перемещает только $58 байт, недокопируются последние 2 байта последней ($10-ой команды), т.е. оффсет VRAM. Таким образом, оффсет этой команды собьется и останется с прошлой команды.

Во-вторых, последняя команда в очереди не очищается, что ведет к ее бесконечному копированию (или, "размазыванию" по очереди). В конечном счете, вся очередь будет заполнена остатками последней команды и игра будет бескончено обрабатывать одну и ту же команду.

Исправление бага

Sonic 1 (Hivenbrain's Disassembly)

Идите к loc_16DC и замените весь код на это:

loc_16DC:			; XREF: sub_165E
		lea	($FFFFF680).w,a0
		lea	6(a0),a1
		moveq	#$E,d0		; do $F cues

loc_16E2:				; XREF: sub_165E
		move.l	(a1)+,(a0)+
		move.w	(a1)+,(a0)+
		dbf	d0,loc_16E2
		
		moveq	#0,d0
		move.l	d0,(a0)+	; clear the last cue to avoid overcopying it
		move.w	d0,(a0)+	;
		rts
; End of function sub_165E

Sonic 2 (Xenowhirls's Disassembly)

Идите к ProcessDPLC_Pop и замените весь код на это:

ProcessDPLC_Pop:
	lea	(Plc_Buffer).w,a0
	lea	6(a0),a1
	moveq	#$E,d0		; do $F cues

-	move.l	(a1)+,(a0)+
	move.w	(a1)+,(a0)+
	dbf	d0,-

	moveq	#0,d0
	move.l	d0,(a0)+	; clear the last cue to avoid overcopying it
	move.w	d0,(a0)+	;
	rts

Новая версия кода правильно сдвинет все команды в очереди и предотвратит ее засорение. Баг исправлен! Однако не забывайте про переполнение очереди, это исправление исправит проблему, когда очередь заполнена до $10 команд, но на $11 команд она уже не рассчитана.