Разработка и ромхакинг > Программирование
[NES] Изучаем Ассемблер 6502
(1/2) > >>
Arigato:
Решил углубиться в тему разработки игр для NES / Famicom / Dendy. Выбор пал на Assembler, хотя видел проекты и на Си, но Assembler ближе к железу, что позволит лучше понять суть работы приставки.

Для начала приведу список некоторых интернет-ресурсов, которые могут оказаться полезны тем, кто захочет освоить написание игр под NES:


* Nesdev Wiki - много полезной информации (англ).
* MOS Technology 6502/Система команд - описание команд процессора 6502 (рус). Учтите, что в NES используется урезанная версия процессора 6502, в которой вырезан блок двоично-десятичной арифметики BCD, процессор не умеет работать в этом режиме.
* cc65 - C compiler for 6502 - Компилятор Си под 6502 (англ). В составе имеется Ассемблер (ca65), именно его и использую для работы.
* Программирование процессора 6502 (Programming the 6502). Программирование Денди - про Ассемблер 6502 (рус). Про Денди по факту там ничего нет.
* Famicom Party. Making NES Games in Assembly - книга о программировании NES (англ). К сожалению, не дописана.
* Создание игр для NES на ассемблере 6502 - перевод указанной выше книги (рус). К сожалению, переведена не до конца, то есть на русском еще меньше глав, чем в самой книге, которая не дописана.
* Notepad++ - редактор для редактирования кода с подсветкой. Тема для подсветки синтаксиса Ассемблера 6502 прикреплена к сообщению (тему подсветки разработал самостоятельно, привязывается к файлам с расширениями asm, inc и mac). Для установки темы делаем следующее: Синтаксис - Пользовательский синтаксис - Задать свой синтаксис - Импорт, выбираем файл Asm6502.xml из прикрепленного архива.Редактор удобен тем, что позволяет настроить компиляцию и запуск программы нажатием одной кнопки. Для компиляции исходного кода написал небольшой CMD-файл:


--- Код: ---@echo off
title Compilation NES
%~d1
cd "%~p1"
if exist "%~n1.nes" del /q "%~n1.nes"
if exist "%~n1.o" del /q "%~n1.o"
ca65 "%~1"
if errorlevel 1 goto error
ld65 "%~n1.o" -t nes -o "%~n1.nes"
if errorlevel 1 goto error
del /q "%~n1.o"
echo OK
"%~n1.nes"
goto :end
:error
pause
:end

--- Конец кода ---

Код сохранил в файл compilation_nes.cmd и поместил в папку с Ассемблером (у меня это C:\cc65\bin). Для настройки Notepad++ на данный компилятор необходимо выполнить следующие действия:

* Папку с Ассемблером C:\cc65\bin добавить в список путей PATH операционной системы (речь за Windows).
* В Notepad++ нажать F5, ввести команду: compilation_nes.cmd "$(FULL_CURRENT_PATH)"
* Далее "Сохранить...", набрать название команды, например, "Compilation NES" и выбрать клавишу быстрого доступа. Я выставил запуск на клавишу F9.
Теперь при нажатии F9 программа компилируется и запускается получившийся .nes-файл (расширение файла должно быть привязано к одному из эмуляторов NES).

У этого способа есть одно ограничение: запускать программу надо только тогда, когда в редакторе открыта вкладка с главным файлом проекта, с которого подключены все остальные файлы. Например, у меня в одном из тестовых проектов это выглядит так:


Никаких .export и .import быть не должно, все файлы проекта надо подключать с помощью .include. Но это вообще не проблема, а даже наоборот, не надо думать о том, какие объекты экспортировать и импортировать, они все доступны из любого файла проекта. Меньше писанины и кучи лишних .include в каждом файле проекта (то есть все .include делаются один раз в главном файле).

В общем, инструмент достаточно удобный, можете пользоваться, но не навязываю.

--------------------------------------------------------------------------------------------------
Добавлено позже:

Ну и тема создана для вопросов и для всевозможных заметок и аккумуляции полезной информации. И первый вопрос будет от меня. А вопрос по указанной в ссылках книге, а именно по главе 10. Sprite Graphics

В книге приводят код обработчика прерывания NMI:


--- Код: ---.proc nmi_handler
  LDA #$00
  STA OAMADDR
  LDA #$02
  STA OAMDMA
  RTI
.endproc

--- Конец кода ---

В последующих главах обработчик прерывания NMI усложняется, в него добавляются новые инструкции. Вопрос возник на счет сохранения контекста, то есть регистров.

Смотрим описание команды RTI:



Как видно, регистр флагов P восстанавливается из стека, после чего происходит возврат из прерывания. Однако никакие другие регистры не восстанавливаются (а в примере выше мы меняем значение аккумулятора A).

Вопрос в том, где ошибка: либо в описании команды RTI, и она в реальности восстанавливает значения всех регистров, либо в книге, и заниматься сохранением регистров должен программист самостоятельно. То есть переписать обработчик таким образом:


--- Код: ---.proc nmi_handler
  PHA ; сохраняем в стеке аккумулятор A
  LDA #$00
  STA OAMADDR
  LDA #$02
  STA OAMDMA
  PLA ; восстанавливаем аккумулятор перед возвратом
  RTI
.endproc
--- Конец кода ---

Так как с регистрами X и Y мы тут не работаем, то их сохранять необязательно.

Кто что думает по этому поводу?
Sharpnull:

--- Цитата: Arigato от 23 Май 2023, 14:45:56 ---Вопрос в том, где ошибка: либо в описании команды RTI, и она в реальности восстанавливает значения всех регистров, либо в книге, и заниматься сохранением регистров должен программист самостоятельно.
--- Конец цитаты ---
RTI восстанавливает только флаги, в большинстве игр в NMI делают PHA TXA PHA TYA PHA и обратные (по не знаю делают ещё PHP/PLP), можно делать STA $00 STX $01 STY $02 и обратные, если память свободная есть. Для простых обработчиков, которые не трогают X, Y, делают только PHA/PLA, совсем простые могут не сохранять (например, INC FrameCounter, RTI).

--- Цитата: Arigato от 23 Май 2023, 14:45:56 ---Ну и тема создана для вопросов и для всевозможных заметок и аккумуляции полезной информации
--- Конец цитаты ---
Из книг. Making Games for the NES (2019) - Steven Hugg (mega.nz). Dev Cart Issue 1 Vol.1 B07H72XDM3 (mega.nz) не помню где качал, там полезно про выбор цветов, много мусора из них.

Хотел поделиться макс. коротким кодом инициализации для освобождения места под хак, например. На основе https://www.nesdev.org/wiki/Init_code.

--- Код: ---Init:
  SEI
  CLD
  LDA #$40
  STA $4017  ; Disable APU frame IRQ
  LDA #$00
  STA $2000  ; Disable NMI
  STA $2001  ; Disable rendering
  STA $4010  ; Disable DMC IRQs

  BIT $2002
-:
  BIT $2002
  BPL -

; Очистка RAM - 13 bytes
  TAY
  LDX #$08
  ; 1-й проход обнулит $FF при любом начальном $FF, для RAM == 2KiB
--:
  STX $00
-:
  STA ($FF),Y
  INY
  BNE -
  DEX
  BPL --
; A = Y = 0, X = FF
  TXS        ; Set up stack

  ; Разный код

-:
  BIT $2002
  BPL -
--- Конец кода ---
Или с большим ожиданием, но короче на 3 байта:

--- Код: ---Init:
  SEI
  CLD
  LDA #$40
  STA $4017  ; Disable APU frame IRQ
  LDA #$00
  STA $2000  ; Disable NMI
  STA $2001  ; Disable rendering
  STA $4010  ; Disable DMC IRQs

  LDX #$02
-:
  BIT $2002
  BPL -
  DEX
  BNE -

; Очистка RAM - 13 bytes
  TAY
  LDX #$08
  ; 1-й проход обнулит $FF при любом начальном $FF, для RAM == 2KiB
--:
  STX $00
-:
  STA ($FF),Y
  INY
  BNE -
  DEX
  BPL --
; A = Y = 0, X = FF
  TXS        ; Set up stack
--- Конец кода ---
Arigato:
Sharpnull, благодарю за подсказку!

Возник еще какой вопрос. Хочу написать набор подпрограмм для работы с экраном (очистка экрана, вывод текста и т.д.). Столкнулся с не до конца понятной мне конструкцией, а именно:

--- Код: --- BIT PPUSTATUS
@vblankwait:
BIT PPUSTATUS
BPL @vblankwait
@vblankwait1:
BIT PPUSTATUS
BPL @vblankwait1
--- Конец кода ---
Да, она позволяет дождаться перерисовки экрана и что-то вывести без артефактов на экране. Но есть пара вопросов:

1. Нужно ли тут 2 цикла или хватит одного? Нужно ли делать лишний BIT PPUSTATUS до цикла?
2. Вот тут - https://www.nesdev.org/wiki/PPU_registers#PPUSTATUS сказано вот что:
Do not read this address to wait for exactly one vertical redraw! On NTSC it will sometimes give false negatives, and on Dendy on some reboots it will always give false negatives.
Ну то есть как бы вообще нежелательно использовать данный метод?
Arigato:

--- Цитата: Arigato от 23 Май 2023, 14:45:56 ---Для компиляции исходного кода написал небольшой CMD-файл
--- Конец цитаты ---
Усовершенствовал командный файл, теперь для компиляции проекта, его можно запускать для любого из файлов проекта, скрипт сам находит главный файл проекта двигаясь вверх по дереву каталогов. Важно чтобы название папки с проектом исовпадало с главным файлом проекта. К примеру, у меня папка с проектом называется Dictator, в ней лежит файл Dictator.asm, то есть имя файла совпадает с названием папки, только еще имеет расширение .asm. Именно по этому критерию скрипт и находит данный файл.

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


--- Код: ---.include "inc/charmap.inc"
.include "inc/constants.inc"
.include "inc/header.inc"
.include "inc/data.inc"
.include "mac/macros.mac"
.include "asm/vectors.asm"
.include "asm/controllers.asm"
.include "asm/screen.asm"
.include "asm/main.asm"

; ---------------------------------------------------------------------------

.segment "CHARS"
.incbin "res/charset.chr"

.segment "RODATA"
palettes:
.incbin "res/font.pal" ; спрайты
.incbin "res/font.pal" ; фон

--- Конец кода ---

Чем удобна такая методика?

* Не нужно прописывать директивы include в каждом файле, чтобы получить доступ к тем или иным ресурсам, они все доступны по умолчанию во всех файлах проекта.
* Не нужно использовать директивы import и export для расшаривания ресурсов. Опять же, все ресурсы доступны изначально, что избавляет от лишней писанины.
* Не нужно компилировать отдельные файлы проекта, чтобы потом собирать их все вместе. Проект компилируется каждый раз целиком и полностью.
* Компиляция и запуск программы осуществляется банальным нажатием клавиши F9 (у меня так настроено) в редакторе кода Notepad++. Причем не важно, какой файл проекта редактируется в данный момент. Нажали F9, программа откомпилировалась и запустилась через эмулятор NES в автоматическом режиме.
Ну и код обновленного командного файла compilation_nes.cmd


--- Код: ---@echo off
setlocal enabledelayedexpansion
title Compilation NES

rem Переходим в папку с файлом
%~d1
cd "%~p1"
set ext=.asm

:next
rem Ищем файл .asm, имя которого совпадает с именем папки
set "dir=%CD%"
for %%n in (%dir:\= %) do set "file=%%n"
if "%file:~-1%"==":" (
rem Дошли до корневой папки диска, останавливаем поиск
cd "%~p1"
set "file=%~dpn1"
set "ext=%~x1"
goto compil
)
set "file=%dir%\%file%"
if not exist "%file%%ext%" (
rem Файл не найден, ищем дальше...
cd ..
goto next
)

:compil
rem Начинаем компиляцию проекта
echo Compilation: %file%%ext%

rem Удаляем старые файлы .nes и .o
if exist "%file%.nes" del /q "%file%.nes"
if exist "%file%.o" del /q "%file%.o"

rem Компилируем .asm файл
ca65 "%file%%ext%"
if errorlevel 1 goto error

rem Компонуем в .nes файл
ld65 "%file%.o" -t nes -o "%file%.nes"
if errorlevel 1 goto error

rem Удаляем объектный файл
del /q "%file%.o"

rem Компиляция успешно выполнена
echo OK

rem Запускаем .nes файл в эмуляторе
"%file%.nes"
goto :EOF

:error
rem Сообщение об ошибке
echo ERROR
pause

--- Конец кода ---
Arigato:

--- Цитата: Arigato от 24 Май 2023, 18:16:20 ---Ну и код обновленного командного файла compilation_nes.cmd
--- Конец цитаты ---
Очередная доработка файла compilation_nes.cmd. Теперь в корневую папку проекта можно положить файл nes.cfg, данный конфиг будет использоваться компоновщиком вместо стандартного. Если файла конфига не будет в корневой папке проекта, то подключается стандартный конфиг nes из набора cc65.

Ну и код новой версии командного файла:


--- Код: ---@echo off
setlocal enabledelayedexpansion
title Compilation NES

rem Переходим в папку с файлом
%~d1
cd "%~p1"
set ext=.asm

:next
rem Ищем файл .asm, имя которого совпадает с именем папки
set "dir=%CD%"
for %%n in (%dir:\= %) do set "file=%%n"
if "%file:~-1%"==":" (
rem Дошли до корневой папки диска, останавливаем поиск
cd "%~p1"
set "file=%~dpn1"
set "ext=%~x1"
goto compil
)
set "file=%dir%\%file%"
if not exist "%file%%ext%" (
rem Файл не найден, ищем дальше...
cd ..
goto next
)

:compil
rem Начинаем компиляцию проекта
echo Compilation: %file%%ext%

rem Удаляем старые файлы .nes и .o
if exist "%file%.nes" del /q "%file%.nes"
if exist "%file%.o" del /q "%file%.o"

rem Компилируем .asm файл
ca65 "%file%%ext%"
if errorlevel 1 goto error

rem Компонуем в .nes файл
if exist nes.cfg (
ld65 "%file%.o" -C nes.cfg -o "%file%.nes"
) else (
ld65 "%file%.o" -t nes -o "%file%.nes"
)
if errorlevel 1 goto error

rem Удаляем объектный файл
del /q "%file%.o"

rem Компиляция успешно выполнена
echo OK

rem Запускаем .nes файл в эмуляторе
"%file%.nes"
goto :EOF

:error
rem Сообщение об ошибке
echo ERROR
pause


--- Конец кода ---
Yoti:
Arigato,
интересно, когда ты узнаешь про "cd /d"?))
Arigato:

--- Цитата: Yoti от 27 Май 2023, 23:50:20 ---"cd /d
--- Конец цитаты ---

--- Код: ---%~d1
cd "%~p1"
--- Конец кода ---
Можно заменить на cd /d "%~dp1" но суть от этого не изменится.
Sharpnull:

--- Цитата: Arigato от 24 Май 2023, 00:57:40 ---1. Нужно ли тут 2 цикла или хватит одного? Нужно ли делать лишний BIT PPUSTATUS до цикла?
--- Конец цитаты ---
В https://www.nesdev.org/wiki/Init_code написано зачем ждать 2 раз: чтобы дождаться разогрева PPU, поэтому можно вообще не ждать, если ваш код будет производить долгие вычисления (30 000 тактов) до использования PPU. В Init_code специально вставили код очистки памяти между ожиданиями VBlank, чтобы не терять лишнего времени. 1-й BIT PPUSTATUS, потому что не знаем начальное состояние VBlank флага. Часто в играх делают 3 итерации ожидания, поэтому всегда ожидается нужное кол-во времени, я так использовал во 2-й варианте инициализации от меня:

--- Код: ---  LDX #$02
-:
  BIT $2002
  BPL -
  DEX
  BNE -
--- Конец кода ---
Добавлю, что даже с отключенным экраном запись палитры нужно делать в VBlank, в Mesen заметно как записываются цвета в виде разноцветных полос на экране. Поэтому в коде инициализации с окончанием на ожидании VBlank можно сразу записать палитру, это нужно, потому что неизвестен начальный цвет фона (https://www.nesdev.org/wiki/PPU_power_up_state), а он используется для экрана даже с отключенным отображением фона и спрайтов (т. е. откл. экран или forced blanking) - https://www.nesdev.org/wiki/PPU_palettes#Backdrop_color_(palette_index_0)_uses.

--- Цитата: Arigato от 24 Май 2023, 00:57:40 ---Ну то есть как бы вообще нежелательно использовать данный метод?
--- Конец цитаты ---
Желательно только в коде инициализации. Я сам до конца не понимаю, но если включен NMI, то точно нельзя из-за Race Condition. Поэтому после инициализации в начале и включения NMI ожидайте только через проверки переменной, которая изменяется в NMI. Только у вас NMI код может быть долгим и ожидание в основном коде закончится вне VBlank, но вы можете весь код сделать в NMI обработчике: https://www.nesdev.org/wiki/NMI_thread.
Я уже отвечал похожим образом на emu-land.
Arigato:

--- Цитата: Sharpnull от 28 Май 2023, 13:26:04 ---Добавлю, что даже с отключенным экраном запись палитры нужно делать в VBlank
--- Конец цитаты ---
Понятно, почему у меня при смене экрана (и перезаписи палитры) появлялась полоса на экране типа вспышки. Переместил загрузку палитры в начало, артефакт исчез.

Добавлено позже:

--- Цитата: Sharpnull от 28 Май 2023, 15:19:56 ---В коде попадаются длинные записи как STA $0012 вместо STA $12.
--- Конец цитаты ---
Не совсем понимаю с этими адресами. Вот что показывает отладчик:



Вот это место в программе:



Определение pad1:

Arigato:

--- Цитата: Arigato от 28 Май 2023, 15:16:15 ---Не совсем понимаю с этими адресами.
--- Конец цитаты ---
Эксперименты показали следующее:

* Если переменная объявлена в .segment "ZEROPAGE" в этом же файле выше по коду или же в подключенных с помощью .include файлах выше по коду, то компилятор генерирует короткие команды доступа к памяти.
* Если же переменная объявлена ниже по коду, либо мы к ней обращаемся из одного из инклудов, который стоял выше объявления переменной (по сути аналогично переменная получается объявлена ниже по коду), то компилятор генерирует длинные команды.
Вывод: важен порядок объявления, то есть и порядок инклудов, чтобы использование переменной всегда было ниже ее объявления, тогда будут короткие команды доступа к ZP.
Arigato:
Возникла очередная проблема. Мне нужно для нескольких нижних строк экрана поменять палитру, то есть чтобы основной экран отрисовывался с одной палитрой, но низ уже с другой.

Пробую метод с нулевым спрайтом. Если менять набор тайлов, то практически работает, правда есть небольшой артефакт:


--- Код: ---@loop:

; ждем столкновениЯ спрайта 0
@Sprite0:
BIT PPUSTATUS
BVS @Sprite0

LDA #%10010000
STA PPUCTRL
LDA #0
STA PPUSCROLL
STA PPUSCROLL

@Sprite0b:
BIT PPUSTATUS
BVC @Sprite0b

LDA #%10000000
STA PPUCTRL
LDA #0
STA PPUSCROLL
STA PPUSCROLL

JMP @loop

--- Конец кода ---



То, что нет осмысленной картинки, это так и должно быть, так как мне не нужен нулевой набор тайлов, просто сделал для проверки работоспособности метода. А артефакт, это первая строка пикселей в самом начале (верхняя слева) частично обрезается.

Ну да ладно, мне и не надо менять наборы тайлов, мне надо менять цвета. Тогда такой код:


--- Код: ---@loop:

; ждем столкновениЯ спрайта 0
@Sprite0:
BIT PPUSTATUS
BVS @Sprite0

LDA PPUSTATUS
LDA #$3f
STA PPUADDR
LDA #$01
STA PPUADDR
LDA #$0f ; черный
STA PPUDATA
LDA #$3F
STA PPUADDR
LDA #$00
STA PPUADDR
STA PPUADDR
STA PPUADDR
STA PPUSCROLL
STA PPUSCROLL

@Sprite0b:
BIT PPUSTATUS
BVC @Sprite0b

LDA PPUSTATUS
LDA #$3f
STA PPUADDR
LDA #$01
STA PPUADDR
LDA #$01 ; синий
STA PPUDATA
LDA #$3F
STA PPUADDR
LDA #$00
STA PPUADDR
STA PPUADDR
STA PPUADDR
STA PPUSCROLL
STA PPUSCROLL
JMP @loop
--- Конец кода ---

И вот тут все намного хуже:



С одной стороны, цвет действительно поменялся (синий фон за текстом, так и задумано). С другой - этот текст "МЕСЯЦ 0" вообще не от сюда, он сверху экрана свалился вниз. Там внизу совсем другой текст должен быть, но экран куда-то съехал. Вот полный экран как выглядит:


"МЕСЯЦ 0" там внизу быть не должно, он вверху. А надпись, которая была внизу, вообще куда-то пропала.

И как это побороть?  :neznayu:

Добавлено позже:

--- Цитата: Sharpnull от 28 Май 2023, 13:26:04 ---Добавлю, что даже с отключенным экраном запись палитры нужно делать в VBlank
--- Конец цитаты ---
Я правильно понимаю, что не получится переключать палитру с помощью нулевого спрайта, чтобы у нас на экраны было как бы два набора палитр?
Sharpnull:

--- Цитата: Arigato от 01 Июнь 2023, 23:06:44 ---чтобы основной экран отрисовывался с одной палитрой, но низ уже с другой
--- Конец цитаты ---
Лучше не пытаться так делать, но это возможно: https://www.nesdev.org/wiki/Palette_change_mid_frame. У вас часть экрана сверху, потому что запись в $2006 вне VBlank меняет место откуда рисовать фон, так делают в играх иногда. Для меня эти эффекты сложные, нужно понимать как работает рисование кадра и тайминг соблюдать, хотя один раз я использовал Split X/Y scroll, взяв код https://www.nesdev.org/wiki/PPU_scrolling. Возможно проще будет вместо sprite 0 hit использовать прерывание, например от MMC3, но в обоих случаях срабатывание не происходит в одной и той же точке на экране. Там ещё спрайты можно сломать. А главная проблема, что в эмуляторах по-разному выглядят сложные эффекты и не всё эмулируется, поэтому придётся проверять на железе.
UPD: Если проблема в том, что у текста разный фон (в игре внизу красный и синий чередуются), то можно использовать маппер с переключением банков графики как MMC3, в других банках расположить такой же алфавит в тех же местах, но у фона сделать индекс цвета другой, тогда внизу у вас будет палитра из цветов "фон, серый (текст), синий, красный", вы переключаете банк с фоном из цвета синий, потом из цвета красный.
Arigato:
Не хватает цветов. Но тогда проще будет надпись вывести спрайтами, тем самым один цвет из палитры освободив для других целей.
Sharpnull:
Arigato, вы можете попробовать переключение цвета, а то я вас отговорил. Здесь пример с переключением: https://forums.nesdev.org/viewtopic.php?t=13264. Разберите его, в Mesen в Event Viewer можно увидеть в каком пикселе были чтение/запись. В конце нужно будет записать адрес в $2006 где начинается нижняя часть. Но для переключения без артефактов придётся выбирать место с 1-2 сканлайном цвета фон (PPU $3F00), когда отключен экран, я посмотрел экраны Dictator и там бывает текст сразу выше "клавиша", тогда нужно переключать выше.
UPD: При использовании спрайтов для слова "клавиша", без изменения цветов в палитре можно менять фон между красным и синим, переключая на другой экран nametable, где будут заранее заполнены тайлы другого сплошного цвета (индекса цвета).
Arigato:
Попробовал вариант с заменой палитры. По ссылке странно сказано - уложитесь в 21 цикл и при этом приведены шаги, которые никаким образом в 21 цикл не уложишь.

Получился следующий код (комментарии прямо в коде):


--- Код: ---
_GOTOXY #31, #25 ; установить курсор в позицию (31,25)
_PRINT_CHAR #$ff ; вывести символ $ff (он черный, но не фон)
LDA #200
STA _SPRITE_ADDR_Y(0) ; установить координату Y спрайта 0

@loop1:

; ждем столкновениЯ спрайта 0
@Sprite0:
BIT PPUSTATUS
BVS @Sprite0

; загружаю адрес палитры в регистры заранее
LDX #$3f ; старший адрес палитры
LDY #$0d ; младший адрес палитры
; восстанавливаю палитру длЯ всего экрана
STX PPUADDR
STY PPUADDR
LDA #$11 ; цвет фона текста длЯ всего экрана
STA PPUDATA
LDA #$29 ; цвет текста длЯ всего экрана
STA PPUDATA
LDA #$00
; ставим адрес на цвет фона
STX PPUADDR
STA PPUADDR
; сбрасываем адрес в ноль
STA PPUADDR
STA PPUADDR

@Sprite0b:
BIT PPUSTATUS ; эти такты тоже надо считать
BVC @Sprite0b ; ведь Sprite 0 hits уже случилсЯ

; отключаем рендеринг (в A у нас уже 0, см. выше)
STA PPUMASK
; менЯем палитру
STX PPUADDR ; старший адрес палитры (он уже в X)
STY PPUADDR ; младший адрес палитры (он уже в Y)
LDA #$27 ; цвет фона текста
STA PPUDATA ; устанавливаем цвет фона текста
LDA #$3d ; цвет текста
STA PPUDATA ; устанавливаем цвет текста
; пропускаю цвета, чтобы вернутьсЯ на цвет фона (иначе фон зальет этим цветом)
BIT PPUDATA
BIT PPUDATA
; возвращаем адрес на позицию экрана (тот знакомест, где у нас нулевой спрайт)
LDA #$23
STA PPUADDR
LDA #$3f
STA PPUADDR
; включаем рендеринг
LDA #%00011110
STA PPUMASK ; отключаем рендеринг (в A у нас уже 0, см. выше)

; сбрасываем прокрутку
; не знаю, нужно или нет, но в любом случае тут уже не надо спешить
LDA #$0
STA PPUSCROLL
STA PPUSCROLL
JMP @loop1
--- Конец кода ---

В эмуляторе FCEUX отработало идеально (внизу на оранжевом фоне серая надпись, это та же самая палитра, что и выше зеленая надпись на голубом фоне):


В VirtuaNES тоже все идеально. А вот в других эмуляторах начались проблемы.

Nintaco (NES):


Причем голубой фон как-то удалось побороть сдвигом в палитре на цвет фона, но потом я что-то еще менял, а позже уже не смог вернуть рабочий вариант. В любом случае полоса выше панели "КНОПКА" видна на черной фоне и постоянно дергается.

Mesen:


Такая же полоса и цвета уже не возвращаются на исходные.

Как видим, работа очень сильно зависит от эмулятора. Как оно будет на реальном железе - неизвестно (и проверить не на чем). Но даже если на железе оно бы заработало, все равно нынче надо ориентироваться и на эмуляторы, ведь большинство использует эмуляторы. И если мы применяем такие хаки, которые эмуляторы не понимают, то лучше так не делать.

Так что вернусь к варианту вывода надписи спрайтами...
Sharpnull:

--- Цитата: Arigato от 02 Июнь 2023, 20:29:28 ---Как видим, работа очень сильно зависит от эмулятора
--- Конец цитаты ---
Пример palette.nes, о котором писал выше, работает нормально и в ужасном VirtuaNES, и в среднеточном FCEUX и в Mesen. Судя по коду, вы не поняли, что важен тайминг, откл. и вкл. экрана нужно в HBlank (за пределами видимой области scanline), а т. к. успеть нельзя за один HBlank, нужно откл. в 1-м и дождаться следующего HBlank и там записать цвета и вкл. экран придётся уже на 3-м HBlank. Получается, что у вас должно быть 2 scanline сплошного цвета $3F00, пока отключен экран (можно попробовать его тоже изменить сразу на цвет от фона "клавиша").

UPD: Я попробовал для примера переключать внизу палитры то на красную, то на синюю и у меня получилось. Я растянул на 3 HBlank и использовал цвет из PPU $3F04 для полос при откл. экране, но в FCEUX и VirtuaNES не знают про The_background_palette_hack и 2 scanline не того цвета. Вообще, удивительно, что у вас почти правильно работает, но не понял:

--- Цитата ---   ; пропускаю цвета, чтобы вернутьсЯ на цвет фона (иначе фон зальет этим цветом)
   BIT PPUDATA
   BIT PPUDATA
   ; возвращаем адрес на позицию экрана (тот знакомест, где у нас нулевой спрайт)
   LDA #$23
   STA PPUADDR
   LDA #$3f
   STA PPUADDR
--- Конец цитаты ---
Там же нужен один BIT PPUDATA для возврата к цвету фона, а вторая запись в $2006 должна быть #$40.
Придётся ещё для PAL адаптировать. Не для всех это.
Ti_:

--- Цитата: Sharpnull от 02 Июнь 2023, 21:16:24 ---UPD: Я попробовал для примера переключать внизу палитры то на красную, то на синюю и у меня получилось. Я растянул на 3 HBlank и использовал цвет из PPU $3F04 для полос при откл. экране, но в FCEUX и VirtuaNES не знают про

--- Конец цитаты ---
В Fceux config->ppu->new ppu выставляет более точную эмуляцию. Касаемо смены палитры, можно из Tom & Jerry ещё взять.


Добавлено позже:

--- Цитата: Sharpnull от 28 Май 2023, 13:26:04 ---В https://www.nesdev.org/wiki/Init_code написано зачем ждать 2 раз: чтобы дождаться разогрева PPU, поэтому можно вообще не ждать, если ваш код будет производить долгие вычисления (30 000 тактов) до использования PPU.

--- Конец цитаты ---
То, что ждать 2 раза это никак не связано с инит кодом, просто вначале идёт проверка на вне vblank, а потом vblank. Потому что если оставить только вторую строчку (ожидание vblank), а сам код ожидания ты запускаешь в произвольное время - ты можешь оказаться и в середине, и в самом конце vblank, а тебе надо оказываться именно в самом его начале, чтобы провести максимум операций с ппу.  Посмотрел ещё раз - там просто пример кривой (2 раза с BPL), не знаю откуда вы их берёте.

Добавлено позже:

--- Цитата: Sharpnull от 28 Май 2023, 13:26:04 ---Я сам до конца не понимаю, но если включен NMI, то точно нельзя из-за Race Condition.

--- Конец цитаты ---
Если NMI включен, то вообще не надо ничего делать с ppu вне него. Если он будет срабатывать где-то в середине твоего кода, и будет всё ломаться.
И как раз через NMI лучше всё и делать, он автоматически и срабатывает вначале vblank, никакие ожидания не нужны, везде так и делают.
Arigato:

--- Цитата: Sharpnull от 02 Июнь 2023, 21:16:24 ---Там же нужен один BIT PPUDATA для возврата к цвету фона, а вторая запись в $2006 должна быть #$40.
--- Конец цитаты ---
Логично, что один. Видимо я там уже экспериментировал, потому что и один не возвращал фон в черный в эмуляторе Nintaco (NES). Самое интересное, что мне удалось изначально сделать черный фон в Nintaco (NES), потом что-то еще правил в коде, в итоге фон сбился, и я так и не смог восстановить исходную версию, где фон правильно отрабатывал...

Во втором, если писать $40, то рендер смещается на одно знакоместо вверх. Во всяком случае у меня такой был эффект.


--- Цитата: Ti_ от 03 Июнь 2023, 09:30:27 ---о, что ждать 2 раза это никак не связано с инит кодом, просто вначале идёт проверка на вне vblank, а потом vblank.
--- Конец цитаты ---
В приведенном по ссылке коде сначала делают bit $2002 и утверждают, что он должен сбросить состояние, после чего ждут первый раз vblank, затем предлагают заняться разными манипуляциями с памятью (время есть) и ждут второй раз vblank.

Ti_:

--- Цитата: Arigato от 03 Июнь 2023, 12:29:42 ---Во втором, если писать $40, то рендер смещается на одно знакоместо вверх. Во всяком случае у меня такой был эффект.
В приведенном по ссылке коде сначала делают bit $2002 и утверждают, что он должен сбросить состояние, после чего ждут первый раз vblank, затем предлагают заняться разными манипуляциями с памятью (время есть) и ждут второй раз vblank.

--- Конец цитаты ---
А, ну значит пример там именно для инит кода, а не для обычного ожидания vblank, то есть по 2 кадра не надо пропускать чтобы палитру записать.
Arigato:
Для облегчения жизни написал подпрограмму, которая считает адрес по координатам (X, Y) экрана и устанавливает туда "курсор". Получилось громоздко:


--- Код: ---; переместить курсор в позицию (X,Y)
; posx - #значение, переменнаЯ, регистры A, X
; posy - #значение, переменнаЯ, регистры A, Y
.macro _GOTOXY posx, posy
.local addr, px, py
; если оба параметра константы,
; то считаем адрес сразу без вызова процедуры
.if .xmatch (.left (1, {posx}), #) .and .xmatch (.left (1, {posy}), #)
LDA PPUSTATUS
px = .right (.tcount ({posx}) - 1, {posx})
py = .right (.tcount ({posy}) - 1, {posy})
addr = py * 32 + px + SCR_BASE
LDA #>addr
STA PPUADDR
; если старший и младший байты адреса совпадают, то втораЯ запись в A не нужна
.if .not >addr = <addr
LDA #<addr
.endif
STA PPUADDR
.else
.if .xmatch ({posx}, A)
TAX
.elseif .not .xmatch ({posx}, X)
LDX posx
.endif
; если posy константа, то считаем сразу Y * 32
.if .xmatch (.left (1, {posy}), #)
addr = .right (.tcount ({posy}) - 1, {posy}) * 32
_LD16 sreg, #addr
JSR gotoxy::offset
.else
.if .xmatch ({posy}, A)
TAY
.elseif .not .xmatch ({posy}, Y)
LDY posy
.endif
JSR gotoxy
.endif
.endif
.endmacro

; переместить курсор в позицию (X,Y)
; X - новер столбца 0-31, Y - номер строки 0-29
; addr = Y * 32 + X + SCR_BASE
.proc gotoxy

; Y * 32
TYA
_SHL A, 4 ; * 16
STA sreg
LDA #0
ROL A
ASL sreg ; * 32
ROL A
STA sreg + 1

offset:
; + X + SCR_BASE
TXA
ADC sreg
TAX
LDA #>SCR_BASE ; SCR_BASE = $2000
ADC sreg + 1

BIT PPUSTATUS
STA PPUADDR
STX PPUADDR

RTS
.endproc

--- Конец кода ---

Пример вызова:


--- Код: --- _GOTOXY #3, #2
_PRINT str_hello0
--- Конец кода ---

То есть указываем координаты либо константами (с символом # в начале), либо переменная (то есть память), либо один из допустимых регистров. _PRINT печатает строку в установленную позицию курсора:


--- Код: ---; вывод строки на экран
; str - строка
; offset - смещение (не обЯзательно): #значение, переменнаЯ, регистр
; Result: X - длина строки
.macro _PRINT str, offset
.ifnblank offset
_LDX offset
.else
LDX #0
.endif
:
LDA str,X
BEQ :+ ; конец строки
STA PPUDATA ; вывод очередного символа
INX
JMP :-
:
.endmacro
--- Конец кода ---

Макрос _GOTOXY пытается построить оптимальный код. Если переданы константы, то адрес сразу вычисляется и записывается уже готовое значение. Если же переданы переменные значения, то адрес считается программно в процедуре gotoxy.

В общем, как такое решение? Можно ли что-то улучшить или оптимизировать?
Навигация
Главная страница сообщений
Следующая страница

Перейти к полной версии