Автор Тема: [NES] Изучаем Ассемблер 6502  (Прочитано 1445 раз)

0 Пользователей и 1 Гость просматривают эту тему.

Оффлайн Arigato

  • Пользователь
  • Сообщений: 47
    • Просмотр профиля
[NES] Изучаем Ассемблер 6502
« : 23 Май 2023, 14:45:56 »
Решил углубиться в тему разработки игр для 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 мы тут не работаем, то их сохранять необязательно.

Кто что думает по этому поводу?
« Последнее редактирование: 23 Май 2023, 15:12:34 от Arigato »

Оффлайн Sharpnull

  • Пользователь
  • Сообщений: 5050
    • Просмотр профиля
Re: [NES] Изучаем Ассемблер 6502
« Ответ #1 : 23 Май 2023, 16:59:55 »
Вопрос в том, где ошибка: либо в описании команды RTI, и она в реальности восстанавливает значения всех регистров, либо в книге, и заниматься сохранением регистров должен программист самостоятельно.
RTI восстанавливает только флаги, в большинстве игр в NMI делают PHA TXA PHA TYA PHA и обратные (по не знаю делают ещё PHP/PLP), можно делать STA $00 STX $01 STY $02 и обратные, если память свободная есть. Для простых обработчиков, которые не трогают X, Y, делают только PHA/PLA, совсем простые могут не сохранять (например, INC FrameCounter, RTI).
Ну и тема создана для вопросов и для всевозможных заметок и аккумуляции полезной информации
Из книг. 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.
Или с большим ожиданием, но короче на 3 байта:

Оффлайн Arigato

  • Пользователь
  • Сообщений: 47
    • Просмотр профиля
[NES] Изучаем Ассемблер 6502
« Ответ #2 : 24 Май 2023, 00:57:40 »
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

  • Пользователь
  • Сообщений: 47
    • Просмотр профиля
[NES] Изучаем Ассемблер 6502
« Ответ #3 : 24 Май 2023, 18:16:20 »
Для компиляции исходного кода написал небольшой 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

  • Пользователь
  • Сообщений: 47
    • Просмотр профиля
[NES] Изучаем Ассемблер 6502
« Ответ #4 : 27 Май 2023, 20:56:46 »
Ну и код обновленного командного файла 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

  • Пользователь
  • Сообщений: 4451
  • Пол: Мужской
  • Не тро-гай ме-ня
    • Steam
    • Просмотр профиля
[NES] Изучаем Ассемблер 6502
« Ответ #5 : 27 Май 2023, 23:50:20 »
Arigato,
интересно, когда ты узнаешь про "cd /d"?))

Оффлайн Arigato

  • Пользователь
  • Сообщений: 47
    • Просмотр профиля
[NES] Изучаем Ассемблер 6502
« Ответ #6 : 28 Май 2023, 12:42:10 »
"cd /d
%~d1
cd "%~p1"
Можно заменить на cd /d "%~dp1" но суть от этого не изменится.

Оффлайн Sharpnull

  • Пользователь
  • Сообщений: 5050
    • Просмотр профиля
[NES] Изучаем Ассемблер 6502
« Ответ #7 : 28 Май 2023, 13:26:04 »
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-й варианте инициализации от меня:
Добавлю, что даже с отключенным экраном запись палитры нужно делать в 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.
Ну то есть как бы вообще нежелательно использовать данный метод?
Желательно только в коде инициализации. Я сам до конца не понимаю, но если включен NMI, то точно нельзя из-за Race Condition. Поэтому после инициализации в начале и включения NMI ожидайте только через проверки переменной, которая изменяется в NMI. Только у вас NMI код может быть долгим и ожидание в основном коде закончится вне VBlank, но вы можете весь код сделать в NMI обработчике: https://www.nesdev.org/wiki/NMI_thread.
Я уже отвечал похожим образом на emu-land.

Оффлайн Arigato

  • Пользователь
  • Сообщений: 47
    • Просмотр профиля
[NES] Изучаем Ассемблер 6502
« Ответ #8 : 28 Май 2023, 15:16:15 »
Добавлю, что даже с отключенным экраном запись палитры нужно делать в VBlank
Понятно, почему у меня при смене экрана (и перезаписи палитры) появлялась полоса на экране типа вспышки. Переместил загрузку палитры в начало, артефакт исчез.

Добавлено позже:
В коде попадаются длинные записи как STA $0012 вместо STA $12.
Не совсем понимаю с этими адресами. Вот что показывает отладчик:



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



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

« Последнее редактирование: 28 Май 2023, 17:35:35 от Arigato »

Оффлайн Arigato

  • Пользователь
  • Сообщений: 47
    • Просмотр профиля
[NES] Изучаем Ассемблер 6502
« Ответ #9 : 29 Май 2023, 16:51:45 »
Не совсем понимаю с этими адресами.
Эксперименты показали следующее:
  • Если переменная объявлена в .segment "ZEROPAGE" в этом же файле выше по коду или же в подключенных с помощью .include файлах выше по коду, то компилятор генерирует короткие команды доступа к памяти.
  • Если же переменная объявлена ниже по коду, либо мы к ней обращаемся из одного из инклудов, который стоял выше объявления переменной (по сути аналогично переменная получается объявлена ниже по коду), то компилятор генерирует длинные команды.

Вывод: важен порядок объявления, то есть и порядок инклудов, чтобы использование переменной всегда было ниже ее объявления, тогда будут короткие команды доступа к ZP.

Оффлайн Arigato

  • Пользователь
  • Сообщений: 47
    • Просмотр профиля
[NES] Изучаем Ассемблер 6502
« Ответ #10 : 01 Июнь 2023, 23:06:44 »
Возникла очередная проблема. Мне нужно для нескольких нижних строк экрана поменять палитру, то есть чтобы основной экран отрисовывался с одной палитрой, но низ уже с другой.

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

@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:

Добавлено позже:
Добавлю, что даже с отключенным экраном запись палитры нужно делать в VBlank
Я правильно понимаю, что не получится переключать палитру с помощью нулевого спрайта, чтобы у нас на экраны было как бы два набора палитр?
« Последнее редактирование: 01 Июнь 2023, 23:25:30 от Arigato »

Оффлайн Sharpnull

  • Пользователь
  • Сообщений: 5050
    • Просмотр профиля
[NES] Изучаем Ассемблер 6502
« Ответ #11 : 01 Июнь 2023, 23:43:57 »
чтобы основной экран отрисовывался с одной палитрой, но низ уже с другой
Лучше не пытаться так делать, но это возможно: 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, в других банках расположить такой же алфавит в тех же местах, но у фона сделать индекс цвета другой, тогда внизу у вас будет палитра из цветов "фон, серый (текст), синий, красный", вы переключаете банк с фоном из цвета синий, потом из цвета красный.
« Последнее редактирование: 02 Июнь 2023, 00:00:10 от Sharpnull »

Оффлайн Arigato

  • Пользователь
  • Сообщений: 47
    • Просмотр профиля
[NES] Изучаем Ассемблер 6502
« Ответ #12 : 02 Июнь 2023, 00:21:32 »
Не хватает цветов. Но тогда проще будет надпись вывести спрайтами, тем самым один цвет из палитры освободив для других целей.

Оффлайн Sharpnull

  • Пользователь
  • Сообщений: 5050
    • Просмотр профиля
[NES] Изучаем Ассемблер 6502
« Ответ #13 : 02 Июнь 2023, 03:55:33 »
Arigato, вы можете попробовать переключение цвета, а то я вас отговорил. Здесь пример с переключением: https://forums.nesdev.org/viewtopic.php?t=13264. Разберите его, в Mesen в Event Viewer можно увидеть в каком пикселе были чтение/запись. В конце нужно будет записать адрес в $2006 где начинается нижняя часть. Но для переключения без артефактов придётся выбирать место с 1-2 сканлайном цвета фон (PPU $3F00), когда отключен экран, я посмотрел экраны Dictator и там бывает текст сразу выше "клавиша", тогда нужно переключать выше.
UPD: При использовании спрайтов для слова "клавиша", без изменения цветов в палитре можно менять фон между красным и синим, переключая на другой экран nametable, где будут заранее заполнены тайлы другого сплошного цвета (индекса цвета).
« Последнее редактирование: 02 Июнь 2023, 05:07:45 от Sharpnull »

Оффлайн Arigato

  • Пользователь
  • Сообщений: 47
    • Просмотр профиля
[NES] Изучаем Ассемблер 6502
« Ответ #14 : 02 Июнь 2023, 20:29:28 »
Попробовал вариант с заменой палитры. По ссылке странно сказано - уложитесь в 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

  • Пользователь
  • Сообщений: 5050
    • Просмотр профиля
[NES] Изучаем Ассемблер 6502
« Ответ #15 : 02 Июнь 2023, 21:16:24 »
Как видим, работа очень сильно зависит от эмулятора
Пример 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 адаптировать. Не для всех это.
« Последнее редактирование: 03 Июнь 2023, 04:50:39 от Sharpnull »

Оффлайн Ti_

  • Пользователь
  • Сообщений: 3265
  • Пол: Мужской
    • ВКонтакте
    • Youtube
    • Просмотр профиля
[NES] Изучаем Ассемблер 6502
« Ответ #16 : 03 Июнь 2023, 09:30:27 »
UPD: Я попробовал для примера переключать внизу палитры то на красную, то на синюю и у меня получилось. Я растянул на 3 HBlank и использовал цвет из PPU $3F04 для полос при откл. экране, но в FCEUX и VirtuaNES не знают про
В Fceux config->ppu->new ppu выставляет более точную эмуляцию. Касаемо смены палитры, можно из Tom & Jerry ещё взять.


Добавлено позже:
В https://www.nesdev.org/wiki/Init_code написано зачем ждать 2 раз: чтобы дождаться разогрева PPU, поэтому можно вообще не ждать, если ваш код будет производить долгие вычисления (30 000 тактов) до использования PPU.
То, что ждать 2 раза это никак не связано с инит кодом, просто вначале идёт проверка на вне vblank, а потом vblank. Потому что если оставить только вторую строчку (ожидание vblank), а сам код ожидания ты запускаешь в произвольное время - ты можешь оказаться и в середине, и в самом конце vblank, а тебе надо оказываться именно в самом его начале, чтобы провести максимум операций с ппу.  Посмотрел ещё раз - там просто пример кривой (2 раза с BPL), не знаю откуда вы их берёте.

Добавлено позже:
Я сам до конца не понимаю, но если включен NMI, то точно нельзя из-за Race Condition.
Если NMI включен, то вообще не надо ничего делать с ppu вне него. Если он будет срабатывать где-то в середине твоего кода, и будет всё ломаться.
И как раз через NMI лучше всё и делать, он автоматически и срабатывает вначале vblank, никакие ожидания не нужны, везде так и делают.
« Последнее редактирование: 03 Июнь 2023, 09:55:22 от Ti_ »

Оффлайн Arigato

  • Пользователь
  • Сообщений: 47
    • Просмотр профиля
[NES] Изучаем Ассемблер 6502
« Ответ #17 : 03 Июнь 2023, 12:29:42 »
Там же нужен один BIT PPUDATA для возврата к цвету фона, а вторая запись в $2006 должна быть #$40.
Логично, что один. Видимо я там уже экспериментировал, потому что и один не возвращал фон в черный в эмуляторе Nintaco (NES). Самое интересное, что мне удалось изначально сделать черный фон в Nintaco (NES), потом что-то еще правил в коде, в итоге фон сбился, и я так и не смог восстановить исходную версию, где фон правильно отрабатывал...

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

о, что ждать 2 раза это никак не связано с инит кодом, просто вначале идёт проверка на вне vblank, а потом vblank.
В приведенном по ссылке коде сначала делают bit $2002 и утверждают, что он должен сбросить состояние, после чего ждут первый раз vblank, затем предлагают заняться разными манипуляциями с памятью (время есть) и ждут второй раз vblank.


Оффлайн Ti_

  • Пользователь
  • Сообщений: 3265
  • Пол: Мужской
    • ВКонтакте
    • Youtube
    • Просмотр профиля
[NES] Изучаем Ассемблер 6502
« Ответ #18 : 03 Июнь 2023, 13:31:12 »
Во втором, если писать $40, то рендер смещается на одно знакоместо вверх. Во всяком случае у меня такой был эффект.
В приведенном по ссылке коде сначала делают bit $2002 и утверждают, что он должен сбросить состояние, после чего ждут первый раз vblank, затем предлагают заняться разными манипуляциями с памятью (время есть) и ждут второй раз vblank.
А, ну значит пример там именно для инит кода, а не для обычного ожидания vblank, то есть по 2 кадра не надо пропускать чтобы палитру записать.

Оффлайн Arigato

  • Пользователь
  • Сообщений: 47
    • Просмотр профиля
[NES] Изучаем Ассемблер 6502
« Ответ #19 : 03 Июнь 2023, 17:09:38 »
Для облегчения жизни написал подпрограмму, которая считает адрес по координатам (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.

В общем, как такое решение? Можно ли что-то улучшить или оптимизировать?
« Последнее редактирование: 03 Июнь 2023, 17:39:12 от Arigato »

Оффлайн Ti_

  • Пользователь
  • Сообщений: 3265
  • Пол: Мужской
    • ВКонтакте
    • Youtube
    • Просмотр профиля
[NES] Изучаем Ассемблер 6502
« Ответ #20 : 03 Июнь 2023, 21:59:12 »
В общем, как такое решение? Можно ли что-то улучшить или оптимизировать?
Не совсем понял, но отдельный код для вывода каждой строки нигде не делают, обычно везде одна функция печати строки, и для нее параметры передаются. Это может быть даже просто её номер, а всё остальное в массиве данных быть. Вычисления как раз можно через макрос сделать.  LDA PPU_STATUS тоже нет никакого смысла каждый раз перед установкой адреса делать, это нужно только в 1 случае - если ты где-то записал только 1 раз в $2006 или $2005. Сам по себе регистр на 'нечетный' никак не слетит.

Вот под твой случай:
; 2 переменные обязательно в zeropage!
tmp_ptr_l equ $20
tmp_ptr_h equ $21

; макрос для печати строки (не знаю подойдёт ли под твой asm)
MACRO print_str str_x,str_y,str_ptr,nt
LDX #>((str_y*32)+$2000 + nt*$400)
LDY #<(str_y*32 + str_x)
LDA #<str_ptr
STA tmp_ptr_l
LDA #>str_ptr
JSR print_str_
ENDM

; функция печати
print_str_:
STA tmp_ptr_h
STX PPU_ADDRESS
STY PPU_ADDRESS
LDY #0
LDA (tmp_ptr_l),Y
@print_l
INY
STA PPU_DATA
LDA (tmp_ptr_l),Y
BNE @print_l
RTS

stroka1: .BYTE 'TEST STROKA 1        ',0
stroka2: .BYTE '   STROKA 2 TEST     ',0


; пример использования в коде:
print_str 4,5,stroka1,0    ; печать строки str0 по x=4,y=5, экран0
print_str 10,15,stroka2,0  ; печать строки str1 по x=10,y=15, экран0

Оффлайн Arigato

  • Пользователь
  • Сообщений: 47
    • Просмотр профиля
[NES] Изучаем Ассемблер 6502
« Ответ #21 : 03 Июнь 2023, 23:04:00 »
LDA PPU_STATUS тоже нет никакого смысла каждый раз перед установкой адреса делать, это нужно только в 1 случае - если ты где-то записал только 1 раз в $2006 или $2005.
Ну по идеи чисто в теории такая ситуация может возникнуть, если между парой записей произойдет вызов nmi. Хотя это весьма странно, так как к этому моменту работу с PPU уже надо бы завершить. Но читал рекомендацию делать LDA PPU_STATUS, видимо, как раз на такой случай (вариант что просто в коде где-то забыли второй раз записать маловероятен, да и в принципе ошибочен). В общем убрал.

По поводу печати, то у меня две отдельные подпрограммы: одна устанавливает "курсор" в нужную клетку экрана, а другая выводит текст, перемещая курсор в конец строки (ну это автоматически происходит при обращении к PPUDATA). Это позволяет вызывать подпрограмму печати одну за другой, тогда текст будет выводиться дальше, за предыдущим. По логике это как в обычном BASIC командой PRINT происходит.

Например:

_GOTOXY #1, #11 ; это координаты курсора
_PRINT str_hello4 ; это вывод строки на экран
_PRINT16 game_hst ; тут вывод 16-битного числа
_PRINT_CHAR #'.' ; и символ "." в конце предложения

На экране это выглядит так:



При этом в _GOTOXY можно передавать не только числа (константы), но и регистры или переменные. Скажем: _GOTOXY #10, Y - установить курсор в строку с номером Y (регистр) в позицию 10.

Выбор экранов пока не сделал, в данный момент мне это не нужно. Может позже добавлю.

Оффлайн Ti_

  • Пользователь
  • Сообщений: 3265
  • Пол: Мужской
    • ВКонтакте
    • Youtube
    • Просмотр профиля
[NES] Изучаем Ассемблер 6502
« Ответ #22 : 04 Июнь 2023, 08:00:20 »
Ну по идеи чисто в теории такая ситуация может возникнуть, если между парой записей произойдет вызов nmi. Хотя это весьма странно, так как к этому моменту работу с PPU уже надо бы завершить.
Само по себе NMI не сбрасывает адрес тоже. Просто там может быть, код который тоже делает запись в vram, или даже просто строчка BIT PPU_STATUS.
Только чтение PPU_STATUS в основном цикле в этом случае не поможет, код всё равно 'посыпется'. Потому что тогда nmi может проскочить и 'между строк' установки адреса, или просто в цикле записи в PPU_DATA.
Поэтому, если у тебя много данных для записи (более 1 фрейма по времени) то NMI отключают вместе с дисплеем (или можно в самом nmi вначале добавить 'флаг'),  а если у тебя записи в пределах vblank или даже экрана, то случайно/повторно в текущем кадре nmi сработать не может.

Добавлено позже:
При этом в _GOTOXY можно передавать не только числа (константы), но и регистры или переменные. Скажем: _GOTOXY #10, Y - установить курсор в строку с номером Y (регистр) в позицию 10.
Понятно, ну минимально можно сделать функцию вместо макроса .macro _PRINT ,  чтобы не забивать ром однотипным кодом (передавать адрес строки).  Если скорость не важна, можно даже сделать и отдельную функцию для установки адреса сделать вместо макроса, а в макросе оставить только вычисление A и Y.
proc_set_ppu_addr:
 STA PPU_ADDRESS
 STY PPU_ADDRESS
 RTS

Если наоборот нужна скорость , можно и в столбик строчки писать (такое тоже встречается):
; 'PRESS START'
   LDX #'P'
   STX PPU_DATA
   INX  ; R'
   STX PPU_DATA
   LDA #'E'
   STA PPU_DATA
   LDY #'S'
   STY PPU_DATA
   STY PPU_DATA
   LDA #'  '
   STA PPU_DATA
   STY PPU_DATA ; 'S'
   INY ; 'T'
   STY PPU_DATA ; 'T'
   LDA #'A'
   STA PPU_DATA  ; 'A'
   STX PPU_DATA  ; 'R'
   STY PPU_DATA ; 'T'

Касаемо вычислений, можно так попробовать:
offset:
LDA #>SCR_BASE ; SCR_BASE = $2000
ORA sreg + 1
        STA PPU_ADDRESS
        TXA
        ORA sreg
        STA PPU_ADDRESS
        RTS
« Последнее редактирование: 04 Июнь 2023, 08:24:59 от Ti_ »

Оффлайн Arigato

  • Пользователь
  • Сообщений: 47
    • Просмотр профиля
[NES] Изучаем Ассемблер 6502
« Ответ #23 : 04 Июнь 2023, 10:57:08 »
Поэтому, если у тебя много данных для записи (более 1 фрейма по времени) то NMI отключают вместе с дисплеем (или можно в самом nmi вначале добавить 'флаг'),
Или можно разбить вывод на несколько кадров, если скорость неважна. Я так делаю, разбиваю информацию на части и вывожу порциями в разных кадрах.