A.I

Zapytałem Claude’a, czy… napisze mi emulator NES

Mam w głowie takie projekty, które robię nie dlatego, że muszę, nie dlatego, że ktoś płaci, i nie dlatego, że skończy się czymś użytecznym. Robię je, bo jestem ciekaw, gdzie leży granica. Tym razem granica brzmiała: czy współczesny model językowy jest w stanie napisać działający emulator konsoli NES, od zera, w C++ z Qt, jeśli go o to po prostu poprosić?

TL;DR: częściowo tak. Ale zanim dojdziemy do końca, przejdźmy przez całą drogę.

Skąd ten pomysł

Emulatory NES to klasyczny projekt „poważnego” programowania. Nie chodzi tu o to, żeby sklecić coś w kilka godzin. Prawidłowa emulacja procesora MOS 6502, układu graficznego PPU 2C02, kanałów audio APU, systemu mapperów kartridży, synchronizacji taktowania między podsystemami — to są rzeczy, przy których niejedna osoba spędza miesiące. Istnieją całe wiki dokumentujące nieudokumentowane zachowania sprzętu. Emulatory takie jak FCEUX czy Mesen powstawały latami.

Pomyślałem: a co jeśli po prostu zapytam Claude’a? Nie „wyjaśnij mi jak działa PPU”, ale dosłownie „napisz mi to”. Cały projekt. Wszystkie pliki. Gotowy do skompilowania.

Jak to rozplanowałem

Nie wysłałem jednej wiadomości „napisz emulator NES”. Podszedłem do tego jak do prawdziwego projektu — z architekturą, wymaganiami i etapami. Powiedziałem Claude’owi, że chcę:

  • C++20 z Qt6, kompilowany przez qmake w Qt Creator na Windows — Modularną strukturę: CPU, PPU, APU, MemoryBus, Cartridge, Mapper, Input, SaveState jako oddzielne klasy — Obsługę ponad 30 mapperów (NROM, MMC1, MMC3, MMC5, VRC4, VRC7 i inne) — Pełne GUI: główne okno, renderer obrazu, okno debuggera z disassemblerem, konfiguracja klawiszy — System save state z rewindem — Nazwę aplikacji VariaNES i obsługę ikony .ico
  • Claude zaczął generować pliki falami. Najpierw core — typy, CPU, PPU, APU, MemoryBus, Cartridge. Potem mappery jeden po drugim, dziesiątki plików nagłówkowych. Potem GUI — MainWindow, RenderWidget, AudioOutput, DebugWindow, InputConfig. Potem Emulator jako koordynator całości, SaveState, InputManager. Na końcu main.cpp, plik .pro dla qmake, CMakeLists.txt jako alternatywa.
  • Jedna sesja to za mało na taki projekt — w pewnym momencie okno kontekstu się zapełnia i Claude zaczyna tracić wątek. Trzeba było robić podsumowania i kontynuować w kolejnych sesjach, pilnując spójności interfejsów.

Co poszło gładko

Zaskakująco dużo. Architektura jest sensowna — każdy podsystem ma swoją klasę, komunikacja idzie przez MemoryBus, zależności są rozplanowane logicznie. CPU ma poprawną tablicę 256 opkodów z obsługą trybów adresowania, illegal opcodes, cyklami taktowania. PPU ma rejestry Loopy, shift registers dla tła, ewaluację sprite’ów. APU ma wszystkie pięć kanałów z envelopami, length counterami, sweep units. Mappery są zaimplementowane konkretnie — MMC3 ma IRQ counter, MMC5 ma ExRAM i tryby PRG/CHR, VRC4 ma prawidłowy pinout adresów.

Plik .pro dla qmake, struktura katalogów, CMakeLists — to wszystko wyszło z głowy bez specjalnego prowadzenia za rączkę.

Gdzie zaczęły się problemy

Tutaj zaczyna się właściwa historia. Kod wygenerowany przez Claude’a jest poprawny koncepcyjnie, ale nie jest spójny implementacyjnie. Interfejsy między klasami są pisane „z pamięci” przy każdym kolejnym pliku i nie zawsze pasują do siebie.

Pierwsza kompilacja dała kilkanaście błędów. Zebrałem je i wrzuciłem z powrotem do Claude’a, który je naprawiał — po czym kompilacja dawała kolejne błędy. Tak kilka rund.

Konkretne problemy, które się pojawiły:

  • Ścieżki include były złe — pliki wewnątrz src/core includowały się jako „ppu/PPU.h” zamiast po prostu „PPU.h”, bo INCLUDEPATH w pliku .pro nie zawierał src/core. Jedno dodanie linii naprawiło kilkanaście błędów naraz.
  • CPU używało forward declaration „class Bus;” a MemoryBus był osobną klasą — konflikt nazw przy próbie przekazania wskaźnika. Rozwiązanie: „using Bus = MemoryBus” w odpowiednim miejscu.
  • PPU miało zduplikowaną metodę getFrameBuffer() — raz zwracającą referencję do std::array, raz wskaźnik const u32*. Kompilator słusznie krzyczał.
  • InputManager nie miał metod readController() i writeStrobe(), bo Claude podczas pisania MemoryBus.cpp użył nieistniejącego API — właściwe metody to read() i write() z adresem jako parametrem.
  • SaveState miał tag jako „const char*” w strukturze SubSystem, co uniemożliwiało memcpy do tego pola. Zamiana na char[4] naprawiła sprawę.
  • RenderWidget.h używał typu u32 bez includowania Types.h — GUI nie wiedziało nic o typach core’u.
  • DebugWindow próbował wywołać std::string::operator const QString& który oczywiście nie istnieje — trzeba było owinąć w QString::fromStdString().
  • QMenu::addAction z Qt6.11 zmieniło kolejność parametrów i stara forma (tekst, obiekt, slot, skrót) jest deprecated — rozwiązane pragmą diagnostyczną, bo przepisywanie wszystkich wywołań byłoby żmudne.

Jak to działało w praktyce

Każda runda błędów wyglądała podobnie: wrzucam logi kompilatora, Claude analizuje, generuje poprawki, skadam je ręcznie do projektu, kompiluję, dostaję nowe błędy. Po kilku takich iteracjach projekt w końcu się skompilował.

Cały projekt to około 7000 linii kodu w 70+ plikach. CPU, PPU, APU, MemoryBus, Cartridge, 27 mapperów z pliki .cpp i kilkanaście header-only, Input, SaveState, pełne GUI z debuggerem.

Co działa, co nie działa

variaNES smb

Emulator się uruchamia. Można wrzucić ROM w formacie *.nes. Coś na ekranie się pojawia — widać, że kod się wykonuje, pojawiają się kolory, jakieś kształty. Nie zawsze jest to jednak prawidłowy obraz w sensie „gra działa poprawnie”.

Dźwięku nie ma, bo Qt Multimedia nie było zainstalowane w mojej konfiguracji Qt — kod jest napisany z obsługą tego modułu jako opcjonalną, więc kompiluje się bez niego, ale APU nie ma gdzie wysłać próbek audio.

Mappery są zaimplementowane, ale bez solidnych testów trudno powiedzieć ile z nich działa prawidłowo. Mapper 0 (NROM, najprostsze gry jak Donkey Kong czy Balloon Fight) teoretycznie powinien działać — to tylko fixowane banki ROM bez żadnego przełączania. Bardziej złożone mappery jak MMC3 wymagają precyzyjnego taktowania IRQ zsynchronizowanego z PPU, co jest trudne do przetestowania bez działającego obrazu.

Debugger otwiera się, pokazuje rejestry CPU, ma hex viewer pamięci. Nie sprawdzałem czy disassembler generuje poprawne mnemoniki, ale interfejs działa.

Co o tym myślę

Byłem ciekaw, czy da się zagnąć AI w ten sposób. Połowicznie mi się udało.

Po stronie „się udało”: Claude napisał architekturę. Stworzył sensowny kod dla każdego podsystemu z osobna. Zna dokumentację NES — NESDev wiki, Loopy’ego rejestry PPU, nieliniowe miksowanie APU, pinouty VRC4. Obsłużył kilkadziesiąt plików bez gubienia struktury projektu przez większość sesji.

Po stronie „nie do końca”: spójność między plikami jest słaba, dopóki człowiek tego nie pilnuje. Interfejsy napisane w jednym pliku nie zawsze zgadzają się z tym co napisano w innym. Kompilator wychwytuje to bezlitośnie, a Claude naprawiając jeden błąd potrafi wprowadzić inny. Bez kogoś kto rozumie co się dzieje i może interpretować błędy kompilatora, ten projekt by nie powstał.

Czy to znaczy, że AI „pisze programy”? Zależy co rozumiemy przez pisanie. Generuje kod który wygląda jak napisany przez programistę — i w dużej mierze jest funkcjonalnie poprawny. Ale sam nie skompiluje, nie uruchomi, nie sprawdzi czy renderer trafnie mapuje paletę kolorów 2C02 na ARGB32. Potrzebuje człowieka jako pętli zwrotnej.

Dla mnie to eksperyment zakończony sukcesem w tym sensie, że dowiedziałem się co chciałem wiedzieć: granica jest mniej więcej tu, gdzie się spodziewałem. AI radzi sobie z generowaniem dużych ilości technicznie złożonego kodu, ale złożoność systemowa — czyli spójność dziesiątek plików pisanych w różnych momentach różnych sesji — to wciąż obszar gdzie człowiek musi trzymać ster.

A emulator? Leży na dysku, coś tam emuluje, bez dźwięku i z nieznanymi problemami z mapperami. Może kiedyś wrócę i to dokończę. Albo nie. Projekt był pretekstem, nie celem.

Dla ciekawskich udostępniam pełny projekt do własnego skompilowania.

📎 Załączniki do pobrania:

📦
VariaNES.zip99 KB⬇ 0

Może cię zainteresować:

P.O.W. – Prisoners of War (1988)

Wściekłe Piksele

DuckStation – doskonała alternatywa dla emulatora ePSXe

Kabson

Kompresujemy obrazy płyt do formatu *.chd

Kabson

Variatkowy przegląd sportowy – gry kopane na Pegasusa

iPOD

Kompresujemy obrazy płyt GameCube/Wii do formatu RVZ

Kabson

Pegasus i erotyka dzieciństwa, czyli jak 8 bitów uczyło nas dobrze dmuchać ;-)

Emumaniak

Zostaw komentarz