Разработка и ромхакинг > Разработка игр

Хранение клона OAM таблицы в PRG-ROM NES

(1/3) > >>

Howard Phillips:
Здравствуйте. Разбираюсь в программировании Nes на си, пока все более менее понятно. Но один вопрос я сам не смог решил, хотя гуглил кране активно.
В документации и статьях в интернете утверждается, что есть возможность хранить копию  OAM таблицы в PRG-ROM для загрузки ее в OAM PPU, а не только в RAM.
Есть я выделяю память под таблицу в $0200-$0600 или в $6000-7F00, то все работает отлично.
 А если пытаюсь хранить таблицу на $8000-$BF00, то на экране артефакты и программа зависает.
Если создаю таблицу на $С000-$FF00 в свободной части, то просто черный экран без артефактов на экране.
И я понимаю, что это не самое оптимальное решение, но мне нужно нормальное осознание архитектуры процессора, поэтому стараюсь освоить все его основные фишки.

--- Код: ---// Так я записываю старшие разряды адреса таблицы (именно так сделал для наглядности того, что адрес точно верный передается)
// SPRITES - массив таблицы
#define OAM_DMA *((unsigned char*)0x4014)
a = (unsigned  int) SPRITES;
OAM_DMA     = a / 0x100;

--- Конец кода ---
Код программы у меня храните в $С000 блоке, $8000-$BFFF - не использую.
Возможно я как-то нарушаю разметку кода? Но я самые разные варианты перепробовал, результатов ноль.
Надеюсь, что вы мне поможете с этой проблемой, ибо дальше осваивать процессор не могу, пока не пойму этот момент. Спасибо.

Sharpnull:
При программировании на C под NES указываются сегменты, вы не показываете как у вас это сделано, лучше покажите весь проект и скажите, что за компилятор.

Howard Phillips:
Компилятор я использую cc65.
Архив прикрепляю (это рабочая версия с таблицей в ОЗУ). В nes.cfg описана разметка (комментарии я делал для себя, пока разбирался с архитектурой). Структуру разметки там видно, скорее всего я что-то не так размечаю.
Надеюсь, что вы подскажите как правильно настроить разметку, чтоб держать OAM в PRG-ROM.

Sharpnull:
Howard Phillips, PRG ROM нельзя менять, поэтому нельзя использовать разметку по аналогии с OAM в RAM (вы изменяете массив SPRITES). Хранить в PRG ROM можно только постоянную OAM, подойдёт разве что для статичной картинки. По идее нужно просто определить массив const unsigned char как у вас PALETTE, но с выравниванием до 256 байт. Не знаю как сделать в cc65 для отдельного массива на C. Наверно, можно добавить в nes.cfg к SEGMENTS:

--- Код: ---RODATA_OAM: load = PRG, type = ro, define = yes, align = $100;
--- Конец кода ---
В DEFINE.c:

--- Код: ---#pragma rodata-name(push, "RODATA_OAM") // Сегмент выравненный до 256 байт
const unsigned char SPRITES[256] = {0}; // Заполнить заранее данными OAM
#pragma rodata-name(pop) // Вернуть сегмент
--- Конец кода ---
Не проверял, не знаю как собирать ром.
Кстати, так не делают: #include "DEFINE.c". #include для .h файлов, но тогда там лучше не определять переменные, только объявлять.
UPD: Заменил rodataseg на rodata-name, смотрел старые доки, а новые здесь: https://www.cc65.org/snapshot-doc/.
UPD2: Проверил, работает. В nes.cfg убрал сегмент OAM, потому что его не использовал и была ошибка. В main.c убрал запись в SPRITES, иначе ошибка компиляции. В define.c в SPRITES указал байты из одного из состояний main.nes. Для компиляции я закинул в папку с проектом файлы от cc65: ca65.exe, cc65.exe, ld65.exe, longbranch.mac, nes.lib, zeropage.inc. Запускал команды по очереди:

--- Код: ---cc65.exe -O -t nes main.c
ca65.exe main.s
ca65.exe reset.s
ld65.exe -o mainPRGOAM.nes -t nes main.o reset.o nes.lib
--- Конец кода ---
Знаю, что нужно писать Makefile :) Ох, там ещё есть cl65 для упрощения.

Howard Phillips:
Sharpnull, точно, я как-то не подумал, что в названии PRG-ROM слово ROM не просто так присутствует, причем еще и двигать пытался спрайт их таблицы. Просто фейспалм. Как-то я еще значит не привык к особенностям NES, 5 дней значит мало для формирования привычки :)
Спасибо, что заморочились с  подробным ответом. Все заработало сразу, когда последовал вашим советам. Работает даже без const, компилятор не ругается.
Структура проекта там вообще спорная у меня, ибо я за основу брал чужой скелет, а в него уже лепил свои эксперименты. Отсюда #include "DEFINE.c" и прочие приколы.
Разработкой я занимаю я в VS Code. Я прописал универсальный батник, которому я хоткеем передаю текущий .с файл на компиляцию и автоматически запускается эмулятор после сборки. Очень удобно. И студия приятная, хороший редактор.
И почти в эту же тему есть еще небольшой вопрос.
Вот я описываю сегмент кода PRG-ROM:

--- Код: ---PRG: start = $c000, size = $3ffa, file = %O ,fill = yes, define = yes;

--- Конец кода ---
Тут я храню PRG во второй половине доступной ROM. А если пытаюсь использовать всю память:

--- Код: ---PRG: start = $8000, size = $7f00, file = %O ,fill = yes, define = yes;

--- Конец кода ---
То серый экран и программа висит, при этом больше ничего не менял, только адреса сегмента.
Я предполагаю, что это нужно еще прописывать в сегменте HEADER. У меня он такой:

--- Код: ---.segment "HEADER"
    .byte $4e,$45,$53,$1a
.byte 01
.byte 01
.byte 00
.byte 00
.res 8,0

--- Конец кода ---
Но документацию на параметры HEADER я почему-то не смог найти. Как мне использовать всю доступную ROM в проекте?
Пробовал  разбивать на два сегмента PRG1 и PRG2, но тогда компилятор ругается на переопределение.

Sharpnull:

--- Цитата: Howard Phillips от 26 Декабрь 2022, 15:46:30 ---Но документацию на параметры HEADER я почему-то не смог найти.
--- Конец цитаты ---
У .nes ромов iNES 1.0 или NES 2.0 заголовок, вам нужно указать размер PRG ROM как 32КиБ. Можете в эмуляторах как Mesen открыть редактор заголовка и увидеть результат в HEX (байтах). Формат: https://www.nesdev.org/wiki/INES. Нужно заменить на:

--- Код: ---.segment "HEADER"
    .byte $4e,$45,$53,$1a
.byte 02
...
--- Конец кода ---
UPD: Ещё вы указали PRG: start = $8000, size = $7f00, а нужно size = $7ffa, размер не совпадёт.

Howard Phillips:
Sharpnull,  спасибо, все заработало.
Сколько тут все-таки тонкостей. Страшно представить разработку большой игры для NES, разработчикам прошлого большой респект.

nonamezerox:

--- Цитата: Howard Phillips от 25 Декабрь 2022, 23:46:11 ---Компилятор я использую cc65.

--- Конец цитаты ---

Переходите на шланг+LLVM-MOS, они сделали супергипервысокоэффективный компилятор отменяющий необходимость ассемблерных вставок.

Sharpnull:
nonamezerox, забавно, но на C не получится сделать синхронизацию с точностью до тактов, которая нужна для эффектов, смены банков графики вне vblank и т. д. Аsm всё равно придётся использовать для чего-то интересного.
UPD: Есть вариант с языками между asm и C как https://github.com/KarolS/millfork и https://github.com/wiz-lang/wiz (пример https://8bitworkshop.com/v3.10.0/?platform=nes&file=hello.wiz).

Howard Phillips:
nonamezerox, спасибо за наводку.
Посмотрел презентацию от разработчика, звучит грандиозно. Особенно забавно, если там реально кодогенерация с С++11 будет действительно на уровне рукопашного ассемблера.
Но пока переходить не буду, шас цель в оптимизации не стоит, пока я осваиваю архитектуру. А LLVM-MOS про уход на более высокий уровень абстракции. Если начну делать свой большой проект, то мб и перейду.

Sharpnull,  разработчики LLVM-MOS как раз утверждают, что код генерируется с точностью до такта и приводят демку, где необходим точный расчет тактов. Но я не вникал пока, может и врут. Но даже если там кодогененератор будет работать на уровне сс65, то использование  оправдано, ибо современный С++ сильно упрощает разработку. Хотя фишками С++11 и старше я особо так и не начал пользоваться.

Howard Phillips:
Здравствуйте. Я продолжаю делать свою игру под NES.
У меня есть функция опроса кнопок, она написана на асме и работает нормально. Но я решил переписать ее на Си. Вышло вот так:

--- Код: ---void Get_Input2 () {
// Сторобируем геймпад
// Адрес геймпада - 0x4016
JOYPAD1 = 1;
JOYPAD1 = 0;
    temp = 0;
// Считываем значения всех кнопок
   
    temp |= JOYPAD1 <<  7; // Чтение А
    temp |= JOYPAD1 <<  6; // Чтение В
    temp |= JOYPAD1 <<  5; // Чтение Select
    temp |= JOYPAD1 <<  4; // Чтение Start
    temp |= JOYPAD1 <<  3; // Чтение Вверх
    temp |= JOYPAD1 <<  2; // Чтение Вниз
    temp |= JOYPAD1 <<  1; // Чтение Влево
    temp |= JOYPAD1 <<  0; // Чтение Вправо

    // Сохраняем предыдущие нажатия кнопок
    joypad1old = joypad1;
    // Сохравняем новое нажатие
joypad1 = temp;
}

--- Конец кода ---
Старший бит temp - это А, следующий это В и тд.
JOYPAD1 - дефайн 0x4016
Все переменные unsigned char
Логика чтения  простейшая, но почему-то кнопки А и Б не считываются, а остальные считываются нормально. У меня вообще нет идей почему так. Явно какая-то глупая ошибка или я упускаю какой-то важный момент механики работы геймпадов.
Может слишком быстро я пытаюсь считать все кнопки? Что я делаю не так?
Компилятор все тот же сс65.
Спасибо.

Sharpnull:

--- Цитата: Howard Phillips от 24 Февраль 2023, 11:31:04 ---У меня есть функция опроса кнопок, она написана на асме и работает нормально. Но я решил переписать ее на Си.
--- Конец цитаты ---
Зря, в этом нет смысла и медленнее, здесь быстрый код - https://www.nesdev.org/wiki/Controller_reading_code и ещё быстрее, если раскрыть цикл, там есть код с учётом подключения контроллера в Famicom's DA15 expansion port.
Ошибка в том, что нужно считывать только 0-й бит (младший), а у вас происходит чтение вместе с open bus - 0x40 (https://www.nesdev.org/wiki/Controller_reading#Unconnected_data_lines_and_open_bus), поэтому в конце у вас: 0x40 | (0x40<<1).
UPD: Я боюсь представить как будет выглядеть ASM после JOYPAD1 <<  7, лучше сдвигать по одному биту, где возможно. Такой код должен быть нормальным (насколько возможно, без Famicom's DA15 expansion port):

--- Код: ---void Get_Input2 () {
    // Сторобируем геймпад
    // Адрес геймпада - 0x4016
    JOYPAD1 = 1;
    JOYPAD1 = 0;

    temp = JOYPAD1;      // A
    temp <<= 1;
    temp |= JOYPAD1 & 1; // B
    temp <<= 1;
    temp |= JOYPAD1 & 1; // Select
    temp <<= 1;
    temp |= JOYPAD1 & 1; // Start
    temp <<= 1;
    temp |= JOYPAD1 & 1; // Up
    temp <<= 1;
    temp |= JOYPAD1 & 1; // Down
    temp <<= 1;
    temp |= JOYPAD1 & 1; // Left
    temp <<= 1;
    temp |= JOYPAD1 & 1; // Right

    // Сохраняем предыдущие нажатия кнопок
    joypad1old = joypad1;
    // Сохравняем новое нажатие
    joypad1 = temp;
}
--- Конец кода ---

Howard Phillips:
Sharpnull,  точно. Спасибо за быстрый ответ. Я забыл, что в 0x4016 не только D0 хранится и почему-то думал, что сдвиг до 7 делается за одну команду. Ваш код заработал.
Я решил переписать на Си функцию чтения для большего единообразия кода и лучшей читаемости. Стараюсь минимизировать ассемблер. И у меня игра пошаговая, там нет борьбы за каждый такт. У меня основа движка написана уже, почти все написано на Си (Распаковка RLE только на асме теперь осталась), скорости пока хватает.
А по поводу оптимизации. Со сдвигами на один бит, получается вполне нормальный код. Вот начало функции после компиляции:

--- Код: ---;
; JOYPAD1 = 1;
;
lda     #$01
sta     $4016
;
; JOYPAD1 = 0;
;
lda     #$00
sta     $4016
;
; temp = JOYPAD1;      // A
;
sta     _temp
;
; temp <<= 1;
;
asl     a
sta     _temp

--- Конец кода ---
Разве плохо? Руками было бы тоже самое. Что тут можно оптимизировать?

Sharpnull:

--- Цитата: Howard Phillips от 24 Февраль 2023, 14:46:16 ---Разве плохо? Руками было бы тоже самое. Что тут можно оптимизировать?
--- Конец цитаты ---
Вы не показали самое интересное, что будет дальше с temp |= JOYPAD1 & 1;, каждый раз будет типа LDA $4016 | AND #$01 | ORA _temp | STA _temp | ASL _temp. Это при том, что вы забили на Famicom's DA15 expansion port, с ним будет заметно хуже. Если без Famicom, то можно записать:

--- Код: ---    LDA $4016
    LSR A
    ROL _temp
    ... повторить ещё 7 раз
--- Конец кода ---
UPD: Не понял как у вас работают кнопки, он здесь бред сгенерировал:

--- Код: ---; JOYPAD1 = 0;
;
lda     #$00
sta     $4016
;
; temp = JOYPAD1;      // A
;
sta     _temp
--- Конец кода ---
Нет чтения $4016, он типа оптимизировал до temp = JOYPAD1 = 0. Там нужно volatile, например, но не уверен:

--- Код: ---#define JOYPAD1 (*(volatile unsigned char*)0x4016)
--- Конец кода ---
Или в общем виде:

--- Код: ---#define IO8(addr) (*(volatile unsigned char*)(addr))
#define JOYPAD1 IO8(0x5100)
--- Конец кода ---
Хотя у вас в проекте уже должно было определено правильно, иначе не работало бы ничего.
UPD2: Забыл сам же сделать оптимизацию 1-го вызова, должно быть так полностью:

--- Код: ---  LDA #$01
  STA $4016
  LSR A ; A = 0
  STA $4016

  LDA $4016
  STA _temp
REPEAT 7 ; Макрос повторяющий N раз
  LDA $4016
  LSR A
  ROL _temp
ENDREPEAT
--- Конец кода ---
UPD3: Для чтения с расширением Famicom я давал ссылку (https://www.nesdev.org/wiki/Controller_reading_code). Можно записать с раскрытым циклом так:

--- Код: ---  LDA #$01
  STA $4016
  LSR A ; A = 0
  STA $4016

REPEAT 8 ; Макрос повторяющий N раз
  LDA $4016
  AND #$03
  CMP #$01
  ROL _temp
ENDREPEAT
--- Конец кода ---
В игре Kage (Shadow of the Ninja в США, Blue Shadow в Европе) не догадались (как многие) и сделали с использованием лишней RAM (потом делают $90 | $92) и дольше выполняется:

--- Код: ---  LDA #$01
  STA Ctrl1_4016
  LDA #$00
  STA Ctrl1_4016
Повторить 8 раз
  LDA Ctrl1_4016
  LSR A
  ROL $90
  LSR A
  ROL $92
КонецПовтора
--- Конец кода ---

Howard Phillips:
Sharpnull, вот полный код функции сгенерированный:

--- Код: ---.segment "CODE"

.proc _Get_Input2: near

.segment "CODE"
;
; JOYPAD1 = 1;
;
lda     #$01
sta     $4016
;
; JOYPAD1 = 0;
;
lda     #$00
sta     $4016
;
; temp = JOYPAD1;      // A
;
sta     _temp
;
; temp <<= 1;
;
asl     a
sta     _temp
;
; temp |= JOYPAD1 & 1; // B
;
lda     $4016
and     #$01
ora     _temp
sta     _temp
;
; temp <<= 1;
;
asl     a
sta     _temp
;
; temp |= JOYPAD1 & 1; // Select
;
lda     $4016
and     #$01
ora     _temp
sta     _temp
;
; temp <<= 1;
;
asl     a
sta     _temp
;
; temp |= JOYPAD1 & 1; // Start
;
lda     $4016
and     #$01
ora     _temp
sta     _temp
;
; temp <<= 1;
;
asl     a
sta     _temp
;
; temp |= JOYPAD1 & 1; // Up
;
lda     $4016
and     #$01
ora     _temp
sta     _temp
;
; temp <<= 1;
;
asl     a
sta     _temp
;
; temp |= JOYPAD1 & 1; // Down
;
lda     $4016
and     #$01
ora     _temp
sta     _temp
;
; temp <<= 1;
;
asl     a
sta     _temp
;
; temp |= JOYPAD1 & 1; // Left
;
lda     $4016
and     #$01
ora     _temp
sta     _temp
;
; temp <<= 1;
;
asl     a
sta     _temp
;
; temp |= JOYPAD1 & 1; // Right
;
lda     $4016
and     #$01
ora     _temp
sta     _temp
;
; joypad1old = joypad1;
;
lda     _joypad1
sta     _joypad1old
;
; joypad1 = temp;
;
lda     _temp
sta     _joypad1
;
; }
;
rts

--- Конец кода ---
Офтоп. А почему у меня не растягивается поле для написания сообщения? Хотя двустороння стрелка появляется. Это у меня проблема или глюк форума?

Sharpnull:

--- Цитата: Howard Phillips от 24 Февраль 2023, 17:51:28 ---полный код функции сгенерированный:
--- Конец цитаты ---
Почти как думал, только asl a + sta _temp вместо asl _temp, что по скорости также, но занимает больше кода. Не читается кнопка A, неправильная генерация ASM, смотрите как определён JOYPAD1, я выше писал про правильное определение.
Можно написать короче, если в начале joypad1old = joypad1; и вместо temp использовать joypad1.

--- Цитата: Howard Phillips от 24 Февраль 2023, 17:51:28 ---А почему у меня не растягивается поле для написания сообщения?
--- Конец цитаты ---
У меня работает в Firefox растягивание по вертикали.

Howard Phillips:
Sharpnull, хорошая мысль с выкидываем Temp. Вот сгенерированный код без volatile:

--- Код: ---; void __near__ Get_Input2 (void)
; ---------------------------------------------------------------

.segment "CODE"

.proc _Get_Input2: near

.segment "CODE"

;
; JOYPAD1 = 1;
;
lda     #$01
sta     $4016
;
; JOYPAD1 = 0;
;
lda     #$00
sta     $4016
;
; joypad1old = joypad1;
;
lda     _joypad1
sta     _joypad1old
;
; joypad1 = JOYPAD1;      // A
;
lda     $4016
sta     _joypad1
;
; joypad1 <<= 1;
;
asl     a
sta     _joypad1
;
; joypad1 |= JOYPAD1 & 1; // B
;
lda     $4016
and     #$01
ora     _joypad1
sta     _joypad1
;
; joypad1 <<= 1;
;
asl     a
sta     _joypad1
;
; joypad1 |= JOYPAD1 & 1; // Select
;
lda     $4016
and     #$01
ora     _joypad1
sta     _joypad1
;
; joypad1 <<= 1;
;
asl     a
sta     _joypad1
;
; joypad1 |= JOYPAD1 & 1; // Start
;
lda     $4016
and     #$01
ora     _joypad1
sta     _joypad1
;
; joypad1 <<= 1;
;
asl     a
sta     _joypad1
;
; joypad1 |= JOYPAD1 & 1; // Up
;
lda     $4016
and     #$01
ora     _joypad1
sta     _joypad1
;
; joypad1 <<= 1;
;
asl     a
sta     _joypad1
;
; joypad1 |= JOYPAD1 & 1; // Down
;
lda     $4016
and     #$01
ora     _joypad1
sta     _joypad1
;
; joypad1 <<= 1;
;
asl     a
sta     _joypad1
;
; joypad1 |= JOYPAD1 & 1; // Left
;
lda     $4016
and     #$01
ora     _joypad1
sta     _joypad1
;
; joypad1 <<= 1;
;
asl     a
sta     _joypad1
;
; joypad1 |= JOYPAD1 & 1; // Right
;
lda     $4016
and     #$01
ora     _joypad1
sta     _joypad1
;
; }
;
rts

--- Конец кода ---
Работает одинаково и с volatile и без него.
Офтоп: У меня гугл хром, странно, что у меня не работает растягивание окна. Очень неудобно так оформлять ответ.

Sharpnull:

--- Цитата: Howard Phillips от 24 Февраль 2023, 18:40:05 ---Работает одинаково и с volatile и без него.
--- Конец цитаты ---
Теперь вы изменили код (убрали temp) и конечно нет смысла сравнивать, он не будет делать оптимизации в любом случае и работает правильно всегда, нужно сравнить прошлый код, где у вас не читалась одна кнопка (не кнопка A, я ошибся, там вообще должно было быть по-другому из-за смещения кнопок, не знаю как вы не заметили). Если вы определяете JOYPAD1, то volatile обязан быть и ваш косяк.

DGanger:
Sharpnull, а в случае с разработкой для NES вообще есть какие-нибудь преимущества у Си перед Ассемблером, как считаете?

Sharpnull:
DGanger, не нужно быть экспертом для ответа: на C (Си) проще писать, а это экономит время разработки, включая исправление ошибок. Так и в современной разработке используют новые тяжёлые тормозящие технологии, но позволяющие быстро писать. Только, в любом случае, когда проект пишется "хорошо" (профессионально), приходится знать что под капотом, а в отношении NES это C для всего, кроме критичного кода где уже ASM. Кто хочет выжать максимум или любит копаться, тот выбирает ASM.
Поэтому я не понял зачем переписывать чтение контроллера, это просто написать ASM и в некоторых (я знаю только один, мало опыта) каркасах для разработки на C уже есть функция получения ввода с контроллера, которая возвращает заранее прочитанный ввод (чтобы не тратить время на чтение каждый раз, больше 1 раза за 1 кадр не нужно).

Навигация

[0] Главная страница сообщений

[#] Следующая страница

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