← Tüm yazılar

MİMARİ

Tarayıcıda NES: 60 kare değil, 60,0988

NES donanımı saniyede 60,0988 kare üretir; tarayıcı ise ekranın temposuyla çizer. Kaset’i kurarken bu iki saatin arasında verdiğimiz kararlar: çekirdeği değiştirilebilir tutmak, çizimi ayrı iş parçacığına taşımak ve kompozit görüntüyü bir tercih değil, oyunun tasarlandığı ortam olarak ele almak.

Tarayıcıda bir NES oynatıcı kurmaya oturduğunuzda karşınıza çıkan ilk sayı yuvarlak değildir. Konsol saniyede 60 kare üretmez; NTSC NES’in kare hızı 60,0988 Hz’dir. Ekran ise kendi temposunda çizer.

Kaset tarayıcıda çalışan bir NES oynatıcısı: .nes dosyasını pencereye bırakıyorsunuz, oyun cihazınızda çalışıyor, dosya hiçbir sunucuya gitmiyor. Bu yazı onu kurarken verdiğimiz kararların notu — çekirdeği değiştirilebilir tutmak, çizimi ayrı bir iş parçacığına taşımak ve kompozit görüntüyü bir görünüm tercihi değil, oyunların tasarlandığı ortam olarak ele almak. Zincirin başı o virgülden sonraki dört basamak.

Bir sayı: 60,0988

Farkı somutlaştırmak kolay. Saniyede 0,0988 kare, saatte yaklaşık 356 kare eder; oran olarak binde 1,65. Başka bir deyişle NES, her on saniyede bir, 60 Hz’lik bir sunumun gösterebileceğinden bir kare fazla üretir.

Bu fazlalık bir yere yazılmak zorunda. Kareyi atarsanız hareket bir an duraklar; ses tarafında biriktirirseniz görüntüyle arası açılır. Aşağıdaki kararların hepsi, bu farkın nereye yazılacağı sorusunun etrafında dönüyor.

Saat nereden geliyor

Sayı seçilmiş değil, bir bölme işleminin sonucu. NTSC’de ana saatin renk alt taşıyıcısının altı katı olması gerekiyor ve bu iki tuhaf sayıyı doğuruyor: ana saat tanım gereği 236,25 MHz’in on birde biri, yani 21,477272 MHz. PPU her noktası için bu saatten dört vuruş harcıyor, CPU ise on iki — NTSC’de bir CPU çevrimine tam üç PPU noktası düşmesinin sebebi bu.

text
ana saat   21.477272 MHz  (236,25 / 11)
nokta      21.477272 / 4  = 5,369318 MHz
CPU        21.477272 / 12 = 1,789773 MHz
kare       341 nokta x 262 satır = 89.342 vuruş

Kare geometrisi de bir tasarım tercihi değil, sinyalin kendisinden çıkıyor. 262 satırın 240’ı görünür, biri son çizim satırı, yirmisi dikey boşluk, biri ön çizim. PAL tarafında aynı zincir 26,6017125 MHz’lik bir ana saatten ve 312 satırdan geçiyor; orada CPU çevrimi başına 3,2 PPU noktası düşüyor.

Tek karede bir nokta kısa

Zincirin sonundaki basamakları belirleyen ayrıntı burada. Çizim açıkken tek numaralı her PPU karesi normalden bir PPU saat vuruşu kısa sürüyor; atlama, (339,261) noktasından doğrudan (0,0) noktasına geçilerek yapılıyor ve atlanan nokta (340,261) oluyor. Çizim kapalıyken böyle bir atlama yok, her kare 89.342 vuruş.

Hesap buradan çıkıyor. Kareler dönüşümlü olarak 89.342 ve 89.341 vuruş sürdüğünde ortalama 89.341,5 oluyor ve nokta hızına bölündüğünde 60,09881 Hz veriyor; atlama hiç olmasaydı 60,09848 çıkardı. NESdev tabloda tek bir değer yayımlıyor: 60,0988. Aradaki dördüncü basamağı belirleyen şey, iki karede bir atlanan tek bir saat vuruşu.

Kaset tarafında kare temposu ölçülüyor: frameTiming nesnesindeki effectiveFps değeri 60’a göre değerlendiriliyor ve sapma için iki ayrı eşik kullanılıyor. Konsolun 0,0988’lik farkı bu eşiklerin çok altında kalıyor — yani ölçüm, donanımın kendi sapmasını gürültü saymadan taşıyabiliyor.

Hangi saat tempoyu sürdürür

Tarayıcıda iki ayrı zaman kaynağı var ve ikisi de konsolun saatinden bağımsız. Ekran tarafında requestAnimationFrame geri çağrıyı bir sonraki boyamadan önce çalıştırıyor; çağrı sıklığı genellikle ekranın tazeleme hızıyla eşleşiyor ve arka plandaki sekmelerde çoğu tarayıcıda duraklıyor. Ses tarafında Web Audio spesifikasyonu açık konuşuyor: currentTime’daki geçen zaman ses akışının kendi zamanı ve sistemdeki diğer saatlerle eş olmayabiliyor.

Ses saati cihazdan cihaza da kayıyor. Örnekleme hızı seçenek verilmediğinde çıkış cihazının tercih ettiği değer oluyor — tipik aralık 8.000 ile 96.000 Hz, en yaygını 44.100. baseLatency ve outputLatency platforma göre değişiyor, latencyHint ise bir talep; tarayıcı dikkate almayabiliyor.

Ses işleme tarafında platform da yer değiştirdi. AudioWorklet, işleme kodunu ayrı bir Web Audio iş parçacığında çalıştırıyor ve process() her ses bloğu için bir kez çağrılıyor; bloklar şu an her zaman 128 kare uzunluğunda, ama boyutun her seferinde okunması gerekiyor. Ana iş parçacığında yürüyen ScriptProcessorNode’un yerini bu aldı.

Sonuç şu: ne ekran saati ne ses saati 60,0988’i veriyor. Hangisini tempo kaynağı seçerseniz seçin, diğeriyle aranızda bir muhasebe farkı kalıyor ve o farkı taşımanın bir yolunu kurmak gerekiyor.

Çekirdeği değiştirilebilir kılmak

Kaset’te iki çekirdek ailesi seçilebiliyor: TetaNES (Yerel) ve Libretro. Seçim localStorage’daki kaset.core anahtarında ve ?core= parametresinde taşınıyor, arayüz de değişikliğin sonraki kasette geçerli olacağını söylüyor.

O “sonraki kasette” kaydı bir kullanım tercihi değil, sözleşmenin şekli. libretro, ses, görüntü ve girdi geri çağrılarını genel biçimde dışa açan hafif ve C tabanlı bir arayüz; API sürümü hâlâ 1. İçeriği retro_load_game yüklüyor, retro_run her çağrıldığında tam olarak bir görüntü karesi üretiyor ve ön yüz ses ile görüntü özelliklerini retro_get_system_av_info üzerinden öğreniyor. Çekirdek değiştirmek, çalışan bir sürecin içinde bayrak çevirmek değil; yeni bir sözleşmeyle baştan kurmak.

Yerel yol Rust tarafında. TetaNES çapraz platform bir NES emülatörü ve WebAssembly ile tarayıcıda da çalışıyor; iki crate’e bölünmüş durumda ve tetanes-core arayüzden bağımsız bir emülasyon kütüphanesi. Bu ayrım, kendi çizim katmanımızı üstüne koyabilmemizi mümkün kılan şey. Derleme hedefi wasm32-unknown-unknown: konaktan hiçbir fonksiyon ithal etmeyen en yalın WebAssembly hedefi, Tier 2. tetanes-core’un yayındaki en yeni sürümü 11 Ağustos 2026 itibarıyla 0.15.0 (7 Ağustos 2026), lisansı MIT ya da Apache-2.0.

Libretro yolunda ise Nostalgist.js var. Kütüphane kendi içinde emülatör taşımıyor; RetroArch’ın Emscripten ile derlenmiş çekirdeklerini sürüyor ve tek giriş noktası launch({ core, rom }). core seçeneği ya hazır bir ad ya da { name, js, wasm } nesnesi alıyor — çekirdek listesinin açık ucu burada. Üst barda NESTOPIA ÇEKİRDEĞİ yazmasının künyesi de şu: Nestopia çevrim doğruluğunda bir NES/Famicom emülatörü, libretro portu yukarı akıştaki Nestopia JG çatallamasını temel alıyor, lisansı GPLv2.

İki yol bundle’da ayrı ayrı duruyor: nativeWorkerEngine, nativeEngine, nostalgistEngine, crtParams ve pacing kendi parçaları olarak yükleniyor. Aynı ayrımın bir benzeri “Yerel LLM sunum katmanı: vLLM, SGLang, llama.cpp ve Ollama” yazısında da vardı — orada da bir çalışma zamanını seçmek ile seçimin taşınabilirliğini korumak ayrı işlerdi.

İş parçacığını ayırmak ve izolasyonun bedeli

Çizimi ana iş parçacığından çıkarmak isteyince kapıda bir yetenek testi duruyor. Kaset beş koşulun tamamını arıyor; hepsi varsa worker tabanlı motoru, değilse ana iş parçacığındaki motoru yüklüyor.

js
Worker
OffscreenCanvas
HTMLCanvasElement.prototype.transferControlToOffscreen
self.crossOriginIsolated === true
new SharedArrayBuffer(4)

Beş koşul aslında iki zincir. Canvas zincirinde transferControlToOffscreen çizim denetimini bir OffscreenCanvas nesnesine devrediyor; sayfadaki öge yer tutucuya dönüşüyor, doğal boyutu sabitleniyor ve kendisi artık bir çizim bağlamı alamıyor. Devir tek yönlü ve tek seferlik — bağlamı alınmış ya da daha önce devredilmiş bir canvas için çağrı InvalidStateError veriyor.

Paylaşılan bellek zinciri daha talepkâr. SharedArrayBuffer için belgenin güvenli bağlamda ve cross-origin isolated olması gerekiyor; izolasyon sunucudan gelen iki başlıkla açılıyor — Cross-Origin-Opener-Policy: same-origin ve Cross-Origin-Embedder-Policy: require-corp. Sonuç kodda self.crossOriginIsolated olarak okunuyor. Karşılığında SharedArrayBuffer postMessage ile taşınabiliyor ve Performance.now daha hassas çalışıyor.

Bedeli de açık. COOP same-origin, belgenin tarama bağlamı grubunu yalnız aynı kökenli belgelerle paylaşması demek; require-corp altında no-cors modundaki kaynaklar ya aynı kökenden gelmeli ya da Cross-Origin-Resource-Policy ile açıkça izin vermeli. Üçüncü taraf betikleri sayfaya serbestçe gömme alışkanlığınız varsa bu tercih sizden bir düzenleme istiyor.

Yaygın bir varsayımı düzeltmekte fayda var: worker’a taşımak ekran saatinden vazgeçmek anlamına gelmiyor. requestAnimationFrame dedicated worker içinde de mevcut (Mart 2023’ten beri Baseline), yalnızca worker’ın bir sahip window’u olması gerekiyor. Öte yandan Atomics.wait ana iş parçacığında kullanılamıyor ve yalnızca SharedArrayBuffer üzerine kurulu dizilerle çalışıyor — iş parçacığını bekleterek tempo tutmak yalnızca worker tarafında mümkün.

Kompozit bir görünüm tercihi değil

Görüntü katmanındaki asıl karar teknik değil, kavramsal. NES’in PPU’su RGB üretip sonra kompozite çevirmiyor; NTSC videoyu doğrudan kompozit alanda kuruyor. Yani kompozit görüntü sonradan eklenen bir filtre değil, sinyalin kendisi.

Palet mekanizması bunu somutlaştırıyor. Altı bitlik değer 64 çıkıştan birine karşılık geliyor; üst iki bit parlaklığı, alt dört bit büyük ölçüde tonu belirliyor. Ton dediğimiz şey bir alt taşıyıcı fazı — $x1 ile $xC arası iki voltaj seviyesi arasında gidip gelen bir kare dalga. Renk, sayı olarak değil zamanlama olarak taşınıyor.

Bunun doğrudan bir sonucu var: tek doğru palet diye bir şey yok. Gerçek donanımda paletin en az dört değişkenlik kaynağı bulunuyor — empedans uyumu, televizyonun kullanıcı ayarları, kompoziti RGB’ye çözme biçimi ve televizyonun kendi renk uzayı. NESdev’in kendi ifadesiyle hiçbir tek kompozit palet her oyunun amaçlanan görüntüsünü aynı anda karşılamıyor, üstelik Nintendo lisanslı geliştiricilere bir referans monitör de tarif etmemiş. Dört ayrı profil sunmamızın gerekçesi tam olarak bu.

Renk çözünürlüğünün piksel çözünürlüğünün altında kalması da aynı yerden geliyor. Bir renk çevrimi on iki saat sürerken NTSC pikseli sekiz saat genişliğinde, yani renk bilgisinin bir kısmı komşu pikselle paylaşılıyor. Tarama satırı 227⅓ renk çevrimi taşıdığı için hizalama her satırda kayıyor ve üç satırda bir tekrarlayan bir desen oluşuyor; yavaş kaydırmalarda parıldama olarak fark ediliyor.

Dört profilin zemini bu olguların üstünde duruyor. Kompozitin çözülme yöntemi televizyondan televizyona değişiyor ve bazı setler hiç filtrelemiyor — Salon Televizyonu buradan çıkıyor. Trinitron tüpü tek elektron tabancası, şerit fosfor ve renk ayırıcı olarak apertür ızgarası kullanıyor; ızgara, ince bir levhadaki dikey açıklıklardan oluşan şeritler. Gölge maskesi ise delikli bir levha ve fosfor üçlülerini gölgeliyor. İki yapının görünür izi farklı: biri nokta üçlüsü, diğeri sürekli bir dikey çizgi. Parametrelerimizdeki mask alanının bir kind değeri taşıması bu yüzden.

RF profilinin zemini de bant genişliği. Televizyon kanalının tamamı 6 MHz iken renk farkı kanalları birkaç yüz kHz ile 1,3 MHz arasında taşınıyor; renk bilgisi parlaklıktan çok daha dar bir bantta yolculuk ediyor. Tarama çizgilerinin görünür olması ise NES’in daima 262 satır üretmesinden: televizyon alanları üst üste çiziyor, geçmeli video oluşmuyor.

Profillerin arkasındaki sayılar bizim seçtiğimiz değerler — bir kalibrasyon ölçümünden gelmiyorlar. Renk katmanında doygunluk 1,25, kontrast 1,06, gama 1,05 ve hafif sıcak bir ton çarpanı; tarama katmanında ışın genişliği 0,55 çevresinde; maske katmanında shadow tipi, 0,25 şiddet ve 3 ölçek. WebGL CRT katmanı yalnızca yerel çekirdekte açılıyor.

Bölge, durum kaydı ve dosyanın yeri

Bölge seçimi bir etiket değil, başka bir makine. PAL’de kare 312 satır ve kare hızı 50,0070 Hz; piksel en boy oranı 1.386:1 iken NTSC’de 1.143:1. Renk artefaktının deseni de değişiyor: PAL satırı 284⅙ chroma çevrimi taşıdığı için desen altı satırda tekrarlıyor, NTSC’de üç.

Arada bir de melez var. Dendy, PAL sinyali kullanan ama CPU’su NTSC kadar hızlı olan bir famiclone; PAL’in kare geometrisiyle NTSC’nin CPU/PPU oranını birleştiriyor. Kare başına CPU çevrimi üç sistemde üç ayrı sayı: NTSC’de 29.780⅔, PAL’de 33.247,5, Dendy’de 35.464. Kaset arayüzünde Otomatik, NTSC ve PAL seçenekleri var; kodda PAL ve Dendy aynı tarafa, diğerleri NTSC’ye eşleniyor.

Durum kaydı tarafında kayıt ROM’a bağlanıyor: saveState çıktısı romHash, slot, dosya adı ve oluşturma zamanıyla birlikte saklanıyor ve işlem beş saniyelik bir zaman aşımı sarmalayıcısının içinde çalışıyor. Kaydı dosya adına değil içeriğin özetine bağlamak bir kimlik meselesi — aynı oyunun farklı kopyaları aynı kayda düşmesin diye.

Sonda kalan şey ilk cümleyle aynı yere çıkıyor. Kaset’e bıraktığınız dosya cihazınızdan çıkmıyor; oyun sizin makinenizde çalışıyor. Bu yazıdaki kararların hepsi aynı soruya farklı biçimlerde bağlanıyor: çalışmanın tam olarak nerede olduğunu bilmek.

Kaynaklar

  • NESdev Wiki — Clock rate, Cycle reference chart (ana saat, CPU ve PPU bölücüleri, kare başına çevrim)
  • NESdev Wiki — PPU frame timing, PPU rendering (tek karelerde atlanan nokta ve atlamanın yeri)
  • NESdev Wiki — NTSC video, PAL video (renk üreteci, renk çevrimi genişliği, satır başına chroma çevrimi, 240p)
  • NESdev Wiki — PPU palettes (altı bitlik palet değeri, ton olarak alt taşıyıcı fazı, paletteki değişkenlik kaynakları)
  • NESdev Wiki — Detect TV system, iNES, NES 2.0 (Dendy, kare başına CPU çevrimi, başlıktaki bölge alanları)
  • MDN Web Docs — SharedArrayBuffer, Window ve WorkerGlobalScope: crossOriginIsolated
  • MDN Web Docs — Cross-Origin-Opener-Policy ve Cross-Origin-Embedder-Policy başlıkları
  • MDN Web Docs — OffscreenCanvas, HTMLCanvasElement: transferControlToOffscreen(), Atomics.wait()
  • MDN Web Docs — Window ve DedicatedWorkerGlobalScope: requestAnimationFrame()
  • MDN Web Docs — AudioWorklet, AudioWorkletProcessor: process(), AudioContext baseLatency ve outputLatency, BaseAudioContext: sampleRate
  • WHATWG HTML Standard — The canvas element (yer tutucu canvas davranışı)
  • W3C Web Audio API — BaseAudioContext.currentTime (ses akışının kendi zamanı)
  • lukexor/tetanes — depo ve README; crates.io — tetanes-core sürüm listesi
  • Nostalgist.js dokümantasyonu — Under the hood ve launch
  • libretro dokümantasyonu — Developing Cores ve Nestopia UE çekirdeği; libretro-common — libretro.h
  • Rust derleyici kitabı — platform desteği: wasm32-unknown-unknown
  • Sony US5382871A ve US6111349A (apertür ızgarası), Zenith EP0239083A2 (gölge maskesi)
  • 47 CFR 73.682 — TV yayın standartları (kanal genişliği ve renk farkı kanallarının bandı)

Diğer yazılar

Bir sisteminiz mi var, kurulacak mı?

Sunum gerekmez. 20 dakika. Sonrası size kalmış.