Laporan tersebut dapat dibagi menjadi tiga bagian:
- bagaimana prosesor 6502 bekerja dan bagaimana mengemulasinya menggunakan JavaScript,
- bagaimana perangkat keluaran grafis bekerja dan bagaimana permainan menyimpan sumber dayanya,
- bagaimana suara disintesis menggunakan audio web dan bagaimana itu diparalelkan menjadi dua aliran menggunakan orlet audio.
Saya mencoba memberikan tips tentang optimasi. Tetap saja, emulasi adalah masalahnya, pada 60 FPS hanya ada sedikit waktu tersisa untuk eksekusi kode.
- Halo semuanya, nama saya Zhenya. Sekarang akan ada pembicaraan yang tidak biasa, Sabtu, tentang proyek pada banyak hari Sabtu. Mari kita bicara tentang emulasi sistem komputer, yang dapat diterapkan di atas teknologi web yang ada. Faktanya, web sudah cukup kaya akan alat, dan Anda dapat melakukan hal-hal yang sangat menakjubkan. Lebih khusus lagi, kita akan berbicara tentang emulator ke semua, mungkin, konsol Dandy yang terkenal dari tahun 90-an, yang sebenarnya disebut Sistem Hiburan Nintendo.
Mari kita ingat sedikit sejarah. Ini dimulai pada tahun 1983 ketika Famicom keluar di Jepang. Itu dirilis oleh Nintendo. Pada tahun 1985, versi Amerika dirilis, yang disebut Sistem Hiburan Nintendo. Di tahun 90-an kami memiliki wilayah Taiwan yang sama bernama Dandy, tetapi diam-diam, ini adalah awalan tidak resmi. Dan hadiah besi terakhir dari Nintendo adalah pada tahun 2016, ketika mini NES keluar. Sayangnya, saya tidak memiliki mini NES. Ada SNES mini, Super Nintendo. Lihatlah betapa kecilnya hal itu, dan tepat di slide ini Anda dapat melihat Hukum Moore dalam segala kemuliaannya.
Jika kita melihat 1985 dan rasio konsol dengan joystick, dan pada 2016, kita bisa melihat betapa kecilnya semuanya, karena tangan orang tidak berubah, joystick tidak bisa dibuat lebih kecil, tetapi konsol itu sendiri menjadi kecil.
Seperti yang telah kita ketahui, ada banyak emulator. Kami tidak mengatakannya, tetapi setidaknya satu pejabat memperhatikan. Benda ini - SNES mini atau NES mini - sebenarnya bukan dekoder sungguhan. Ini adalah perangkat keras yang mengemulasi konsol. Sebenarnya, ini adalah emulator resmi, tetapi hadir dalam bentuk besi yang lucu.
Tapi seperti kita ketahui, sejak tahun 2000-an, ada program yang meniru NES, berkat itu kita masih bisa menikmati game dari era tersebut. Dan ada banyak emulator. Mengapa yang lain, terutama di JavaScript, Anda bertanya kepada saya? Ketika saya melakukan hal ini, saya menemukan tiga jawaban untuk pertanyaan ini untuk diri saya sendiri.
- , . - , . . , - , - . . . , , . , -.
- , , . , , , , NES β , , NTSC, 60 . 16 , . .
- . , . , , . , , β , . . , , .
Saya juga menyaksikan presentasi Matt Godbold, yang juga berbicara tentang meniru prosesor yang menjalankan NES. Dia berkata lucu bahwa kami meniru hal tingkat rendah seperti itu dalam bahasa tingkat tinggi. Kami tidak memiliki akses ke perangkat keras, kami bekerja secara tidak langsung.
Mari beralih ke pertimbangan tentang apa yang akan kita tiru, bagaimana kita akan meniru, dll. Kita akan mulai dengan prosesor. NES sendiri sangat ikonik. Untuk Rusia, bisa dimaklumi, ini adalah fenomena budaya. Tetapi di Barat, dan di Timur, di Jepang, itu juga merupakan fenomena budaya, karena konsol, nyatanya, menyelamatkan seluruh industri video game rumahan.
Prosesor juga dipasang di MOS6502 ikonik. Apa signifikansinya? Pada saat muncul, pesaingnya dihargai $ 180 dan MOS6502 dengan harga $ 25. Artinya, prosesor ini meluncurkan revolusi komputer pribadi. Dan di sini saya memiliki dua komputer. Yang pertama adalah Apple II, kita semua tahu dan membayangkan betapa pentingnya peristiwa ini bagi dunia komputer pribadi.
Ada juga komputer BBC Micro. Dia lebih populer di Inggris, BBC adalah perusahaan televisi Inggris. Artinya, prosesor ini membawa komputer ke massa, berkat itu kami sekarang menjadi programmer, pengembang front-end.
Mari kita lihat program minimumnya. Apa yang kita butuhkan untuk membuat sistem komputasi?
CPU itu sendiri adalah perangkat yang sangat tidak berguna. Seperti yang kita ketahui, CPU menjalankan program tersebut. Tapi setidaknya agar program ini bisa disimpan di suatu tempat, dibutuhkan memori. Dan tentunya sudah termasuk dalam program minimal. Dan memori kita terdiri dari sel delapan bit, yang disebut byte.
Dalam JavaScript, kita dapat menggunakan array Uint8Array yang diketik untuk meniru memori ini, artinya, kita dapat mengalokasikan array.
Untuk memori untuk berinteraksi dengan prosesor, ada bus. Bus memungkinkan prosesor untuk mengalamatkan memori melalui alamat. Alamat tidak lagi terdiri dari delapan bit, seperti data, tetapi 16, yang memungkinkan kita untuk mengatasi 64 kilobyte memori.
Ada keadaan tertentu dalam prosesor, ada tiga register - A, X, Y. Register seperti penyimpanan untuk nilai menengah. Ukuran register adalah satu byte atau delapan bit. Ini memberi tahu kita bahwa prosesor itu delapan-bit, beroperasi pada data delapan-bit.
Contoh penggunaan register. Kami ingin menambahkan dua angka, tetapi hanya ada satu bus di memori. Ternyata Anda perlu menyimpan nomor pertama di antara keduanya. Kita menyimpannya di register A, kita bisa mengambil nilai kedua dari memori, menambahkannya, dan hasilnya ditempatkan lagi di register A.
Secara fungsional, register ini cukup independen - bisa digunakan sebagai register umum. Tetapi mereka memiliki arti, seperti penjumlahan, hasil diperoleh di register A dan nilai operan pertama diambil.
Atau, misalnya, kami menangani data. Kita akan membicarakannya nanti. Kita dapat menentukan mode pengalamatan offset dan menggunakan register X untuk mendapatkan nilai akhir.
Apa lagi yang termasuk dalam status prosesor? Ada register PC yang menunjuk ke alamat dari perintah saat ini, karena alamatnya adalah dua byte.
Kami juga memiliki register Status, yang menunjukkan bendera status. Misalnya, jika kita mengurangi dua nilai dan mendapatkan negatif, maka bit tertentu dalam register flag menyala.
Terakhir, ada SP, penunjuk ke tumpukan. Tumpukan hanyalah memori biasa, tidak terpisah dari yang lainnya, dari semua program lain. Hanya ada instruksi dari prosesor yang mengontrol penunjuk SP ini. Beginilah cara tumpukan diterapkan. Kemudian kita akan melihat satu ide komputer hebat yang mengarah pada solusi yang begitu menarik.
Sekarang kita tahu bahwa ada prosesor, memori, status di prosesor. Mari kita lihat apa program kita. Ini adalah urutan byte. Bahkan tidak harus konsisten. Program itu sendiri dapat ditempatkan di berbagai bagian memori.
Kita dapat membayangkan sebuah program, saya memiliki sepotong kode di sini - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10. Ini adalah program nyata pada 6502. Setiap byte dari program ini, setiap digit dalam larik ini adalah seperti itu entitas sebagai opcode. Opcode - kode operasi. βKemudian, lagi, angka biasa.
Misalnya, ada opcode 169. Ini mengkodekan dua hal itu sendiri - pertama, instruksi. Ketika dijalankan, instruksi tersebut mengubah status prosesor, memori, dan seterusnya, yaitu status sistem. Misalnya kita tambahkan dua angka, hasilnya muncul di register A. Ini adalah contoh instruksi. Kami juga memiliki instruksi LDA, yang akan kami pertimbangkan secara lebih rinci. Ini memuat nilai dari memori ke register A.
Hal kedua yang dikodekan opcode adalah mode pengalamatan. Dia memberikan instruksi tentang di mana mendapatkan datanya. Misalnya, jika ini adalah mode pengalamatan IMM, maka dikatakan: ambil data yang ada di sel di sebelah penghitung program saat ini. Kami juga akan melihat bagaimana mode ini bekerja dan bagaimana itu dapat diimplementasikan dalam JavaScript.
Begitulah programnya. Terlepas dari byte ini, semuanya sangat mirip dengan JavaScript, hanya pada level yang lebih rendah.
Jika Anda ingat apa yang saya bicarakan, mungkin ada paradoks yang lucu. Kami, ternyata, menyimpan program di memori, dan datanya juga. Orang mungkin bertanya pertanyaan ini: dapatkah suatu program bertindak sebagai data? Jawabannya iya. Kita bisa merubah dari program itu sendiri pada saat menjalankan program ini.
Atau pertanyaan lain: dapatkah data menjadi program? Ya juga. Prosesor tidak masalah. Dia hanya, seperti penggilingan, menggiling byte yang diumpankan kepadanya dan mengikuti instruksi. Suatu hal yang paradoks. Jika Anda memikirkannya, itu sangat tidak aman. Anda dapat mulai menjalankan program yang hanya berupa data di tumpukan, dll. Tetapi keuntungannya adalah program ini sangat mudah. Tidak perlu melakukan sirkuit yang rumit.
Ini adalah ide bagus pertama yang kami temui hari ini. Itu disebut arsitektur von Neumann. Tapi sebenarnya ada banyak rekan penulis di sana.
Berikut diilustrasikan. Ada program 1, opcode 169, diikuti 10, beberapa data. Baik. Program ini juga dapat dilihat seperti ini: 169 adalah data, dan 10 adalah opcode. Ini akan menjadi program legal untuk 6502. Keseluruhan program ini, sekali lagi, dapat dianggap sebagai data.
Jika kita memiliki kompiler, kita dapat membuat sesuatu, memasukkannya ke dalam memori ini, dan itu akan menjadi hal yang lucu.
Mari kita lihat bagian pertama dari program kita - instruksi.
6502 menyediakan akses ke 73 instruksi, termasuk aritmatika: penambahan, pengurangan. Tidak ada perkalian dan pembagian, maaf. Ada operasi bit, yaitu tentang memanipulasi bit dalam kata delapan bit.
Ada lompatan yang dilarang di frontend kami: pernyataan jump, yang hanya mentransfer penghitung program ke beberapa bagian kode. Ini dilarang dalam pemrograman, tetapi jika Anda berurusan dengan level rendah, ini adalah satu-satunya cara untuk melakukan percabangan. Ada operasi untuk tumpukan, dll. Mereka dikelompokkan. Ya, kami memiliki 73 instruksi, tetapi jika Anda melihat pada grup dan apa yang mereka lakukan, sebenarnya tidak banyak dari mereka dan semuanya sangat mirip.
Mari kembali ke instruksi LDA. Seperti yang kami katakan, ini adalah "memuat nilai dari memori ke register A". Beginilah cara super sederhananya dalam JavaScript. Di pintu masuk adalah alamat yang disediakan oleh mode pengalamatan untuk kita. Kami mengubah keadaan di dalam, kami mengatakan bahwa this._a sama dengan nilai baca dari memori.
Kita masih perlu menyetel dua bidang bit ini di register status - bendera nol dan bendera negatif. Ada banyak hal yang bijak di sini. Tetapi jika Anda membuat emulator, itu akan menjadi kebiasaan Anda untuk berurusan dengan OR ini, negatif, dll. Satu-satunya hal lucu di sini adalah bahwa ada% 256 di cabang kedua. Ini merujuk kita, sekali lagi, pada sifat bahasa JavaScript kita yang kita cintai, pada fakta bahwa ia tidak memiliki nilai yang diketik. Nilai yang kami masukkan ke Status bisa melebihi 256, yang sesuai dengan satu byte. Kita harus menghadapi trik seperti itu.
Sekarang mari kita lihat bagian terakhir dari opcode kita, mode pengalamatan.
Kami memiliki 12 mode pengalamatan. Seperti yang kami katakan sebelumnya, mereka memungkinkan kami untuk mendapatkan dan menunjukkan instruksi dari mana mendapatkan data.
Mari kita lihat tiga hal. Yang terakhir adalah ABS, mode pengalamatan absolut, mari kita mulai dengan itu, saya minta maaf karena sedikit malu. Dia melakukan sesuatu seperti ini. Kami memberinya alamat lengkap, 16 bit, sebagai masukan. Dia memberi kita nilai dari sel memori ini. Di assembler, di kolom kedua, Anda dapat melihat seperti apa: LDA $ ccbb. ccbb adalah bilangan heksadesimal, bilangan biasa, cukup ditulis dengan notasi berbeda. Jika Anda merasa tidak nyaman di sini, ingatlah bahwa ini hanyalah angka.
Di kolom ketiga, Anda dapat melihat tampilannya di kode mesin. Di depan adalah opcode - 173, disorot dengan warna biru. Dan 187 dan 204 sudah merupakan data alamat. Tetapi karena kami beroperasi dengan nilai delapan-bit, kami memerlukan dua lokasi memori untuk menulis alamatnya.
Saya juga lupa mengatakan bahwa opcode dijalankan selama beberapa waktu di CPU, itu memiliki biaya tertentu. LDA dengan pengalamatan absolut membutuhkan empat siklus CPU.
Di sini Anda sudah dapat memahami mengapa begitu banyak mode pengalamatan dibutuhkan. Pertimbangkan mode pengalamatan berikutnya, ZP0. Ini adalah mode pengalamatan halaman nol. Dan halaman nol adalah 256 byte pertama yang dialokasikan dalam memori. Ini adalah alamat dari nol hingga 255.
Dalam assembler, sekali lagi, LDA * 10. Apa yang dilakukan mode pengalamatan ini? Dia berkata: pergi ke halaman nol, di sini, di 256 byte pertama ini, dengan offset ini dan itu. dalam hal ini 10, dan ambil nilainya dari sana. Di sini kita sudah melihat perbedaan yang signifikan antara mode pengalamatan.
Dalam kasus pengalamatan absolut, kami membutuhkan, pertama, tiga byte untuk menulis program semacam itu. Kedua, kami membutuhkan empat siklus CPU. Dan dalam mode pengalamatan ZP0, hanya dibutuhkan tiga siklus CPU dan dua byte. Tapi ya, kami kehilangan fleksibilitas. Artinya, kita hanya bisa meletakkan data kita di halaman pertama, yang ini.
Mode pengalamatan terakhir IMM mengatakan: ambil data dari sel di sebelah opcode. LDA # 10 di assembler ini melakukan itu. Dan ternyata programnya terlihat seperti [169, 10]. Ini sudah membutuhkan dua siklus CPU. Tetapi di sini jelas bahwa kami juga kehilangan fleksibilitas, dan kami memerlukan opcode untuk berada di samping data.
Menerapkan ini dalam JavaScript itu mudah. Berikut beberapa contoh kode. Ada sebuah alamat. Ini adalah pengalamatan IMM, yang mengambil data dari penghitung program. Kami hanya mengatakan bahwa alamat kami adalah penghitung program dan menambahnya satu sehingga saat program dijalankan, itu akan melompat ke instruksi berikutnya.
Ini hal yang lucu. Kami sekarang dapat membaca kode mesin sebagai pengembang frontend. Dan kami bahkan tahu bagaimana melihat apa yang tertulis di sana di assembler.
Pada prinsipnya kita sudah tahu semua yang kita butuhkan. Ada program yang terdiri dari byte. Setiap byte adalah opcode, setiap opcode adalah instruksi, dan seterusnya Mari kita lihat bagaimana program kita dijalankan. Dan itu dijalankan hanya dalam siklus CPU ini.
Bagaimana kode seperti itu dilakukan? Contoh. Kita perlu membaca opcode dari penghitung program, lalu menambahnya satu per satu. Sekarang kita perlu memecahkan kode opcode ini menjadi instruksi dan ke mode pengalamatan. Kalau dipikir-pikir, opcode-nya adalah bilangan prima, 169. Dan dalam satu byte kita hanya memiliki 256 angka. Kita bisa membuat array dengan 256 nilai. Setiap elemen dari array ini hanya akan mengarahkan kita ke instruksi mana yang akan digunakan, mode pengalamatan mana yang diperlukan dan berapa siklus yang dibutuhkan. Artinya, ini sangat sederhana. Dan larik yang saya miliki hanya dalam status prosesor.
Selanjutnya, kita hanya menjalankan fungsi mode pengalamatan pada baris 36, yang memberi kita alamat, dan memberinya instruksi.
Hal terakhir yang perlu kita lakukan adalah menangani loop. opcodeResolver mengembalikan jumlah siklus, kami menuliskannya ke variabel sisaCycles. Kami melihat setiap siklus prosesor: jika ada nol siklus tersisa, maka kami dapat menjalankan perintah berikutnya, jika lebih besar dari nol, kami cukup menguranginya satu per satu. Dan itu semua, sangat sederhana. Beginilah cara program dijalankan pada 6502.
Tetapi seperti yang telah kami katakan, program dapat berada di bagian memori yang berbeda, dalam rasio yang berbeda, dll. Bagaimana prosesor dapat memahami di mana harus mulai menjalankan program ini? Kita membutuhkan int utama seperti ini dari dunia C.
Padahal, semuanya sederhana. Prosesor memiliki prosedur untuk mengatur ulang statusnya. Dalam prosedur ini, kami mengambil alamat dari perintah awal dari alamat 0xfffxc. 0xfffxc lagi-lagi merupakan angka heksadesimal. Jika Anda merasa tidak nyaman, skor, ini adalah angka yang biasa. Beginilah cara mereka ditulis dalam JavaScript, melalui 0x.
Kita perlu membaca dua byte alamat, alamatnya 16 bit. Kami membaca byte rendah dari alamat ini, byte tinggi dari alamat berikutnya. Dan kemudian kami menambahkan kasus ini dengan operasi bit yang ajaib. Selain itu, mengatur ulang status prosesor juga mengatur ulang nilai dalam register - register A, X, Y, penunjuk ke tumpukan, status. Reset membutuhkan delapan siklus. Itulah masalahnya.
Kami sudah tahu segalanya sekarang. Sejujurnya, agak sulit bagi saya untuk menulis semua ini, karena saya sama sekali tidak mengerti bagaimana cara mengujinya. Kami menulis seluruh komputer yang dapat menjalankan program apa pun yang pernah dibuat untuknya. Bagaimana memahami bahwa kita bergerak dengan benar?
Ada cara yang luar biasa dan menakjubkan! Kami mengambil dua CPU. Yang pertama adalah yang kami buat, yang kedua adalah CPU referensi, kami tahu pasti bahwa itu berfungsi dengan baik. Misalnya, ada emulator untuk NES, nintendulator, yang dianggap sebagai CPU benchmark semacam itu.
Kami mengambil program uji tertentu, menjalankannya pada CPU referensi dan menulis status prosesor ke log status untuk setiap perintah. Kemudian kami mengambil program ini dan menjalankannya di CPU kami. Dan setiap status setelah setiap perintah dibandingkan dengan log ini. Ide super!
Tentu saja, kami tidak memerlukan referensi CPU. Kami hanya membutuhkan log eksekusi program. Log ini dapat ditemukan di Nesdev. Faktanya, emulator prosesor dapat dibuat, saya tidak tahu, dalam beberapa hari di akhir pekan - itu hanya hal yang super!
Dan itu saja. Kami mengambil log, membandingkan status, dan kami memiliki tes interaktif. Kami menjalankan perintah pertama, itu tidak diterapkan di prosesor yang kami kembangkan. Kami menerapkannya, pergi ke baris berikutnya dari log dan menerapkannya lagi. Sangat cepat! Memungkinkan Anda bergerak dengan cepat.
Arsitektur NES
Kami sekarang memiliki CPU, yang pada dasarnya adalah jantung dari komputer kami. Dan kita dapat melihat dari apa arsitektur SEN itu sendiri dibuat dan bagaimana sistem komputer komposit yang rumit itu dibuat. Karena kalau dipikir-pikir, ya, ada CPU, ada memori. Kami dapat menerima nilai, rekaman, dll.
Tetapi di NES, di set-top box mana pun, ada juga layar, perangkat suara, dll. Kami perlu belajar bekerja dengan periferal. Anda bahkan tidak perlu belajar sesuatu yang baru untuk ini, konsep bus kami sudah cukup. Ini mungkin ide cemerlang kedua, penemuan brilian yang saya buat untuk diri saya sendiri dalam proses menulis emulator.
Mari kita bayangkan bahwa kita mengambil memori kita, yaitu 64 kilobyte, dan membaginya menjadi dua kisaran 32 kilobita. Di rentang bawah akan ada perangkat tertentu, yaitu rangkaian lampu, seperti pada gambar di papan ini.
Misalkan saat menulis ke kisaran 32 kilobita junior ini, cahayanya akan menyala atau padam. Jika kita menulis di sana nilai 1, lampu akan menyala, jika 0 - padam. Pada saat yang sama, kita dapat membaca nilai dan memahami status sistem, memahami gambar mana yang ditampilkan di layar ini.
Sekali lagi, di kisaran atas alamat, kami menempatkan memori biasa di mana program berada, karena kami memerlukan alamat di kisaran atas selama prosedur reset.
Ini sebenarnya adalah ide yang sangat jenius. Untuk berinteraksi dengan periferal, tidak diperlukan perintah tambahan, dll. Kami hanya menulis ke memori lama yang baik, seperti sebelumnya. Tetapi pada saat yang sama, memori sudah dapat menjadi perangkat tambahan.
Kami sekarang sepenuhnya siap untuk melihat arsitektur NES. Kami memiliki CPU dan busnya, seperti biasa. Ada dua kilobyte memori tambahan. Ada APU - perangkat keluaran suara. Sayangnya, sekarang kami tidak akan mempertimbangkannya, tetapi semuanya super keren juga di sana. Dan ada kartrid. Itu ditempatkan di kisaran tinggi dan menyediakan data program. Dia juga menyediakan grafik ini, sekarang kita akan pertimbangkan. Hal terakhir di bus CPU adalah PPU, unit pemrosesan gambar, kartu proto-video seperti itu. Jika Anda ingin mempelajari cara bekerja dengan kartu video, kami sekarang bahkan akan belajar cara menerapkannya.
PPU juga memiliki busnya sendiri, tempat tabel nama, palet, dan data grafik dipindahkan. Tetapi data grafik berasal dari kartrid. Dan kemudian ada memori objek. Inilah arsitekturnya.
Mari kita lihat apa itu kartrid. Ini adalah ide yang jauh lebih keren daripada CD jika Anda menganggapnya dari masa lalu.
Kenapa dia keren? Di sebelah kiri kita bisa melihat kartrid wilayah Amerika, permainan terkenal Zelda, jika ada yang belum memainkan - mainkan, super. Dan jika kita membongkar kartrid ini, kita akan menemukan sirkuit mikro di dalamnya. Tidak ada laser disk, dll. Biasanya chip ini hanya berisi beberapa data. Juga, kartrid langsung memotong sistem komputer kita, ke bus CPU dan PPU. Ini memungkinkan Anda melakukan hal-hal luar biasa dan meningkatkan pengalaman pengguna.
Ada mapper di atas kartrid, itu mengisi dengan terjemahan alamat. Katakanlah kita memiliki pertandingan besar. Tetapi NES hanya memiliki 32 kilobyte memori yang dapat dialamatkan untuk program tersebut. Sebuah game, katakanlah, adalah 128 kilobyte. mapper dapat, dengan cepat, selama eksekusi program, mengganti kisaran memori tertentu dengan data yang benar-benar baru. Kita dapat mengatakan dalam program: memuat kita level 2, dan memori akan langsung diganti, hampir seketika.
Ditambah lagi ada hal-hal lucu. Misalnya, pembuat peta dapat menyediakan chip yang memperluas soundtrack, menambahkan yang baru, dll. Jika Anda telah memainkan Castlevania, dengarkan seperti apa suara Castlevania di wilayah Jepang. Ada suara tambahan, terdengar sangat berbeda. Dalam hal ini, semuanya dilakukan pada perangkat keras yang sama. Artinya, ide ini lebih mirip dengan saat Anda membeli kartu video, menghubungkannya ke komputer, dan Anda memiliki fungsi tambahan. Di sini juga sama. Itu keren. Tapi kami terjebak dengan CD.
Mari beralih ke bagian terakhir - mari kita lihat cara kerja perangkat keluaran gambar ini. Karena jika ingin membuat emulator, program minimalnya adalah membuat prosesor dan hal ini memperhatikan tampilan gambar dan video game.
Mari kita mulai dengan entitas tingkat atas - gambar itu sendiri. Ini memiliki dua rencana. Ada latar depan tempat entitas yang lebih dinamis ditempatkan dan latar belakang tempat lebih banyak entitas statis seperti pemandangan ditempatkan.
Anda dapat melihat perpecahan di sini. Di sebelah kiri adalah game Castlevania yang sama terkenalnya, jadi seluruh perjalanan kita ke PPU akan terjadi dengan Simon Belmont. Bersama dia, kami akan mempertimbangkan bagaimana semuanya bekerja.
Ada latar belakang, kolom, dll. Kita melihat bahwa semuanya digambar di latar belakang, tetapi pada saat yang sama semua karakter - Simon sendiri (kiri, coklat) dan hantu - sudah digambar di latar depan. Artinya, latar depan ada untuk entitas yang lebih dinamis, dan latar belakang ada untuk entitas yang lebih statis.
Gambar pada tampilan bitmap terdiri dari piksel. Piksel hanyalah titik-titik berwarna. Paling tidak, kita butuh warna. NES memiliki palet sistem. Ini terdiri dari 64 warna, yang sayangnya semua warna dapat direproduksi oleh NES. Tapi kita tidak bisa mengambil warna apapun dari palet. Untuk palet ubahsuaian, ada rentang tertentu dalam memori, yang, pada gilirannya, juga dibagi menjadi dua sub-rentang tersebut.
Ada berbagai latar belakang dan latar depan. Setiap rentang dibagi menjadi empat palet dengan empat warna. Misalnya background, zero palette terdiri dari putih, biru, merah. Dan warna keempat di setiap palet selalu mengacu pada warna transparan, yang memungkinkan kita membuat piksel transparan.
Rentang dengan palet ini tidak lagi terletak di bus CPU, tetapi di bus PPU. Mari kita lihat bagaimana kita dapat menulis data di sana, karena kita tidak memiliki akses ke bus PPU melalui bus CPU.
Di sini kita kembali lagi ke ide I / O yang dipetakan memori. Ada alamat 0x2006 dan 0x2007, ini adalah alamat heksadesimal, tetapi hanya berupa angka. Dan kami menulis seperti ini. Karena alamat kami 16-bit, kami menulis alamat ke register alamat ox2006 dalam dua pendekatan delapan bit dan kemudian kami dapat menulis data kami melalui alamat 0x2007. Hal yang lucu. Artinya, sebenarnya, kita perlu melakukan tiga operasi untuk setidaknya menulis sesuatu ke palet.
Luar biasa. Kami memiliki palet, tetapi kami membutuhkan struktur. Warna selalu bagus, tetapi bitmap terstruktur.
Untuk grafik, ada dua tabel masing-masing empat kilobyte yang berisi ubin. Dan semua ingatan ini adalah semacam atlas. Sebelumnya, ketika semua orang menggunakan gambar raster, mereka membuat atlas besar, kemudian mereka memilih gambar yang diperlukan melalui gambar latar belakang berdasarkan koordinat. Ini ide yang sama.
Setiap meja memiliki 256 ubin. Sekali lagi, numerologi lucu: tepatnya 256 memungkinkan Anda menentukan satu byte, 256 nilai berbeda. Artinya, dalam satu byte kita dapat menentukan ubin apa pun yang kita butuhkan. Ternyata dua tabel. Satu tabel untuk latar belakang, satu lagi untuk latar depan.
Mari kita lihat bagaimana ubin ini disimpan. Di sini juga lucu. Ingatlah bahwa kita memiliki empat warna dalam palet kita. Numerologi lagi: satu byte memiliki delapan bit, dan ubin delapan kali delapan. Ternyata dengan satu byte kita dapat merepresentasikan strip ubin, di mana setiap bit akan bertanggung jawab atas beberapa warna. Dan dengan delapan byte, kita dapat merepresentasikan ubin delapan kali delapan yang lengkap.
Tapi ada satu masalah disini. Seperti yang kami katakan, satu bit bertanggung jawab atas warna, tetapi hanya dapat mewakili dua nilai. Ubin disimpan dalam dua bidang. Ada bidang yang paling signifikan dan paling tidak signifikan. Untuk mendapatkan warna akhir, kami menggabungkan data dari kedua bidang.
Anda dapat mempertimbangkan - di sini, misalnya, huruf "I", bagian bawah, ada angka "3", yang ternyata seperti ini: kami mengambil bidang dari bit yang paling tidak signifikan dan paling signifikan dan mendapatkan angka biner 11, yang akan sama dengan desimal 3. Struktur data yang lucu.
Latar Belakang
Sekarang kita akhirnya bisa membuat latar belakang!
Ada tabel nama untuk itu. Kami memiliki dua di antaranya, masing-masing 960 byte, setiap byte merujuk kami ke ubin tertentu. Artinya, pengenal ubin ditunjukkan di tabel sebelumnya. Jika kita merepresentasikan 960 byte ini sebagai matriks, kita mendapatkan layar ubin 32 x 30. Resolusi NES akan menjadi 256 piksel kali 240 piksel.
Luar biasa. Kita bisa menulis ubin di sana. Tapi seperti yang mungkin Anda perhatikan, ubin tidak menunjukkan palet yang harus ditampilkan. Kami dapat menampilkan ubin berbeda dengan palet berbeda, dan kami juga perlu menyimpan informasi ini di suatu tempat. Sayangnya, kami hanya memiliki 64 byte per tabel nama untuk menyimpan informasi palet.
Dan di sinilah masalahnya muncul. Jika kita membagi tabel lebih jauh sehingga hanya ada 64 nilai, kita mendapatkan petak empat kali empat yang terlihat seperti kotak merah. Ini hanyalah sebagian besar dari layar. Dia akan tunduk pada satu palet, jika bukan untuk satu tapi.
Seperti yang kita ingat, ada empat palet di sub-palet, dan kita hanya perlu dua bit untuk menunjukkan yang kita butuhkan. Masing-masing 64 byte ini menyalin informasi palet untuk grid empat kali empat. Tetapi kisi ini masih terbagi menjadi subkisi dua per dua. Tentu saja, ada batasan: kisi dua kali dua terikat pada satu palet. Ini adalah batasan dalam dunia menampilkan background di Nintendo. Fakta menyenangkan, tetapi secara umum tidak terlalu mengganggu game.
Ada juga scrolling. Jika kita ingat, misalnya, "Mario" atau Castlevania, maka kita tahu: jika dalam permainan ini pahlawan bergerak ke kanan, maka dunia seolah terbuka di sepanjang layar. Ini dilakukan dengan menggulir.
Ingatlah bahwa kita memiliki dua tabel nama yang sudah menyandikan dua layar. Dan saat pahlawan kita bergerak, kita semacam menambahkan data ke tabel nama yang mengikuti. Dengan cepat, saat pahlawan kita bergerak, kita mengisi tabel nama. Ternyata kami dapat menunjukkan dari ubin mana dalam tabel nama yang kami perlukan untuk mulai menampilkan data, dan kami akan memperluasnya dalam bentuk strip. Seluruh trik menggulir membaca dari dua tabel nama.
Artinya, jika kita melampaui satu tabel nama secara horizontal, maka kita mulai membaca dari tabel lain secara otomatis, dll. Dan jangan lupa, sekali lagi, untuk mengisi datanya.
Ngomong-ngomong, scrolling adalah hal yang cukup besar saat itu. Prestasi pertama John Carmack adalah di bidang scrolling. Lihat cerita ini, ini sangat lucu.
Latar depan
Dan latar depan. Di latar depan, seperti yang kami katakan, ada entitas dinamis, dan mereka disimpan dalam memori objek dan atribut.
Ada 256 byte yang kita dapat menulis 64 objek, empat byte per objek. Setiap objek menyandikan X dan Y, yang merupakan offset piksel di layar. Ditambah alamat dan atribut ubin. Kita bisa memprioritaskan background, lihat gambar dibawah ini? Kita bisa menentukan paletnya. Prioritas di atas latar belakang memberi tahu PPU bahwa latar belakang harus digambar di atas sprite. Ini memungkinkan kami untuk menempatkan Simon di belakang patung.
Kita juga bisa membuat orientasi, memutarnya melewati sumbu apa saja, misalnya horizontal, vertikal, seperti huruf "I" pada gambar. Kami menulis kira-kira dengan cara yang sama seperti palet: melalui alamat 0x2003, 0x2004.
Akhirnya, final. Bagaimana kita merender objek latar depan?
Gambar terbentang di sepanjang garis yang disebut scanlines, ini adalah istilah televisi. Sebelum setiap scanline, kita cukup mengambil delapan sprite dari memori objek dan atribut. Tidak lebih dari delapan, hanya delapan yang didukung. Ada juga batasan seperti itu. Kami hanya menampilkannya baris demi baris, seperti di sini, misalnya. Pada garis pindai saat ini, dengan warna kuning, kami menampilkan awan, matahari, dan hati dalam garis. Kami tidak menampilkan smiley. Tapi dia tetap bahagia.
Lihat saluran super One Lone Coder . Ada proses pemrograman itu sendiri, khususnya - memprogram emulator NES. Dan Nesdev berisi semua informasi tentang emulasi - terdiri dari apa, dll. Tautan terakhir adalah kode emulator saya . Coba lihat jika tertarik. Ditulis dalam TypeScript.
Terima kasih. Semoga Anda menikmatinya.