| Разработка и ромхакинг > Разработка игр |
| Хранение клона 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 кадр не нужно). |
| Навигация |
| Главная страница сообщений |
| Следующая страница |