BNF (Backus-Naur Form) on yleisesti käytetty tapa määritellä ohjelmointikielen syntaksia. Se on yksinkertainen mutta tehokas työkalu, joka kuvastaa kielen rakenteen sääntöjä muodollisesti. BNF:n avulla voimme kuvata, miten lauseet ja ilmaukset muodostuvat ohjelmointikielessä.

Yksi BNF:n perusideoista on jakaa kieli pieniin osiin, joita kutsutaan terminaaleiksi ja ei-terminaaleiksi. Terminaalit ovat yksinkertaisia elementtejä, kuten kirjaimia tai numeroita, jotka eivät tarvitse lisäselityksiä. Ei-terminaalit puolestaan ovat sääntöjä, jotka määrittelevät, miten terminaalit yhdistyvät toisiinsa muodostaen suurempia rakenteita. Esimerkiksi yksinkertainen sääntö voisi olla seuraava:

go
<lause> ::= 'A' | 'B' | '1' | '2'

Tässä <lause> on ei-terminaali, joka voi olla joko 'A', 'B', '1' tai '2'. Tämä sääntö määrittelee kielen osan, joka voi olla yksi neljästä vaihtoehdosta.

Voimme yksinkertaistaa tämän sääntöä vielä enemmän ja poistaa toisen sääntöjen tuotannon kokonaan, jolloin saamme seuraavan:

go
<lause> ::= ('A' | 'B' | '1' | '2')*

Tässä '*' tarkoittaa, että 'A', 'B', '1' tai '2' voivat esiintyä missä tahansa määrässä. Tämä esimerkki havainnollistaa, kuinka voimme yksinkertaistaa syntaksia välttäen tarpeettomia sääntöjä. Liian monen tuotantosäännön käyttäminen tekee kieliopin monimutkaisemmaksi, ja se voi johtaa turhaan vaikeuteen ymmärtää koodia.

Kuitenkin on tapauksia, joissa ei-terminaali voi olla hyödyllinen, vaikka se vaikuttaisi ylimääräiseltä. Jos ei-terminaali edustaa useita mahdollisia terminaaleja ja sitä käytetään myöhemmin muissa säännöissä, on järkevää antaa sille oma tuotantosääntö sen sijaan, että toistaisimme pitkän listan terminaaleja eri kohdissa kielioppia. Tämä on samanlaista kuin ohjelmoinnissa, jossa voi olla parempi kirjoittaa useita pieniä, uudelleenkäytettäviä funktioita kuin kirjoittaa yksi suuri ja monimutkainen funktio.

Jos tarkastellaan toista esimerkkiä, joka liittyy numeroidun luettelon määrittämiseen, voimme kirjoittaa seuraavat tuotantosäännöt:

go
<luettelo> ::= <kohde>*
<kohde> ::= '.' '\n' <numero> ::= '0' | '1' | ... | '8' | '9' <teksti> ::= .*

Tässä olemme lisänneet muutamia erityismuotoja. '...' tarkoittaa, että terminaalien lista jatkuu implisiittisesti (tämä ei ole kovin muodollista, mutta säästää tilaa), ja '.' tarkoittaa mitä tahansa käyttäjän määriteltävissä olevaa terminaalia, kuten tavallisessa säännöllisessä lausekkeessa. Jos tarkastelemme näitä sääntöjä käänteisesti, voimme selittää ne yksinkertaisemmassa muodossa:

  1. Luettelo koostuu nollasta tai useammasta kohteesta.

  2. Kohde on numero, jota seuraa piste, teksti ja rivinvaihto.

  3. Numero on yksi tai useampi numero.

  4. Numero on mikä tahansa numero väliltä 0-9.

  5. Teksti on mikä tahansa merkkijono.

Tässä kuitenkin syntyy ongelma, kun käsitellään numeroita. Esimerkiksi numero, jossa on etunollia, kuten 0020, olisi sallittu, mutta tämä ei ole järkevää numeroidussa luettelossa. Tässä kohtaa on tärkeää pohtia, kuinka tämä virhe voidaan korjata.

Tämä esimerkki tuo esille, kuinka BNF:n avulla voimme määritellä kielen rakenteen mutta samalla se tuo esiin myös kielen mahdolliset rajoitukset. Kielioppi itsessään ei määrittele, mitä kunkin elementin arvo tai merkitys on. Se on työ, joka jää tulkille tai muille osille ohjelman toteutusta.

BNF:n avulla määritetty kieli voi olla niin yksinkertainen kuin muutama sääntö, mutta kuten NanoBASICin tapauksessa, se voi olla myös monimutkainen ja sisältää useita sääntöjä, jotka yhdessä määrittelevät ohjelmointikielen syntaksin. NanoBASIC on esimerkki tästä monimutkaisemmasta rakenteesta, joka perustuu alkuperäiseen Tiny BASIC -kieleen ja sisältää seuraavat tuotantosäännöt:

go
<rivi> ::= '\n' | 'REM' .* '\n' <komento> ::= 'PRINT' | 'IF' 'THEN' | 'GOTO' | 'LET' '=' | 'GOSUB' | 'RETURN' <lauseke> ::= <lauseke> (',' <lauseke>)* <termi> ::= ('+' | '-')* <tekijä> ::= ('*' | '/')* <faktori> ::= ('-' | ε) | <muuttuja> | <numero> | '(' <lauseke> ')' <muuttuja> ::= ('_' | <kirjain>) ('_' | <kirjain>)* <numero> ::= '0' | '1' | ... | '9'
<kirjain> ::= 'a' | 'b' | ... | 'z' | 'A' | 'B' | ... | 'Z'
<relaatio-operaattori> ::=
'<' ('>' | '=' | ε) | '>' ('<' | '=' | ε) | '=' <merkkijono> ::= '"' .* '"'

Tämä kuvaa kielen syntaksia, jossa on mukana kommentteja, peruslausekkeita ja aritmeettisia lausekkeita. Esimerkiksi laskentatehtävät määritellään ensin termeinä ja sitten tekijöinä, mikä liittyy operaattoreiden etusijajärjestykseen. Tärkeää on myös, että vanhat ja uudet muuttujat voivat sisältää useita alaviivoja tai kirjaimia, toisin kuin alkuperäinen Tiny BASIC, jossa oli rajoituksia.

Tämä syntaksi määrittää kielen rakenteen ja säännöt, mutta se ei vielä anna kullekin kielen osalle merkitystä. Se jää tulkille, joka käyttää tätä syntaksia luodakseen koodin ymmärtäviä toimintoja. Tämä on vain yksi osa ohjelmointikielen toteutusta, ja kun koodi ajetaan, syntyy abstrakti syntaksipuu (AST), joka esittää kielen osien suhteet ja merkitykset.

Lopulta BNF ei pelkästään määrittele, miltä kielen tulee näyttää; se myös ohjaa kehittäjää ja tulkin tekijöitä suunnittelemaan ohjelmointikielen rakenteen ja toiminnallisuuden siten, että se on looginen ja tehokas.

Mikä on MacBinary-muoto ja miksi se on tärkeä retro-Mac-tiedostojen käytössä?

MacBinary-tiedostomuoto oli keskeinen osa klassisen Mac OS -järjestelmän tiedostojen hallintaa ja siirtoa. Mac OS:ssa tiedostot olivat usein kahtia jaettuja, sisältäen sekä datavarsan että resurssivarsan, jotka mahdollistivat monimutkaisempien tiedostojen käsittelyn yhdestä tiedostosta. Resurssivarsan avulla sovellukset ja tiedostot pystyivät sisällyttämään metatietoa ja lisäresursseja, kuten grafiikoita ja ääniä. Tämä mahdollisti monien Mac-sovellusten "itsenäisyyden", sillä ohjelmat saattoivat sisältää kaiken tarvittavan yhdessä tiedostossa, joka oli helppo jakaa.

Klassisen Mac OS:n ero muihin käyttöjärjestelmiin oli se, että tiedostot eivät olleet vain yksinkertaisia tietoblokkeja. Niiden sisältö ja käyttö määräytyivät tiedostotunnisteiden ja luojatunnisteiden mukaan, ei tiedostopäätteiden avulla. Tämä mahdollisti sen, että käyttäjät pystyivät nimeämään tiedostonsa haluamallaan tavalla ilman, että heidän tarvitsi huolehtia siitä, että tiedoston sisällön käsittelyyn liittyvät ohjelmat löytäisivät ne automaattisesti.

MacBinary-tiedostomuoto syntyi vastauksena tiedostojen siirtämisen haasteisiin, sillä resurssivarsan puuttuminen muilta käyttöjärjestelmiltä aiheutti usein tiedostojen vaurioitumista tai toimimattomuutta siirron yhteydessä. MacBinary yhdisti datavarsan ja resurssivarsan yhdeksi tiedostoksi, joka oli helpompi siirtää ja käsitellä muilla käyttöjärjestelmillä.

MacBinary-muodon avulla Mac-tiedostojen siirto muiden järjestelmien välillä tuli mahdolliseksi ilman, että tiedoston eheys kärsisi. Tämä oli erityisen tärkeää retro-Mac-tiedostojen käsittelyssä, kuten MacPaint-kuvien, jotka olivat alun perin riippuvaisia Mac OS:n erikoispiirteistä. MacBinary-muoto mahdollisti tiedostojen suojaamisen ja varmistamisen, että ne avautuisivat oikein klassisessa Mac OS:ssa.

MacBinary-tiedostossa on erityinen 128 tavun otsikko, joka edeltää itse tiedoston varsinaista dataa. Otsikko sisältää tärkeää metatietoa, kuten tiedoston nimen pituuden ja itse nimen, tiedostotyypin ja luojatunnisteen. Tiedostotyyppi ja luoja määrittelevät, mikä sovellus avaa tiedoston automaattisesti, mikä oli tärkeää Mac OS:ssa, jossa ei käytetty tiedostopäätteitä.

Vaikka MacBinary-tiedostot olivat yksinkertaisia verrattuna moniin muihin tiedostomuotoihin, niiden oikea rakenne oli tärkeä tiedostojen toimivuuden kannalta. Erityisesti MacPaint-tiedostojen oikea pakkaaminen MacBinary-muotoon oli ratkaisevan tärkeää, jotta ne avautuivat oikein vanhoissa Mac-tietokoneissa ilman lisäohjelmia.

MacBinary-muodon luomiseksi tarvittavat toimenpiteet ovat yksinkertaisia mutta tarkkoja. Ensinnäkin, tiedoston alkuun lisätään 128 tavun otsikko, joka sisältää tiedoston metatiedot. Tämän jälkeen itse datavarsi, joka tässä tapauksessa voi olla esimerkiksi MacPaint-kuvatiedosto ilman erillistä resurssivarsaa, lisätään otsikon jälkeen. Tiedoston loppuun tulee varmistaa, että se päättyy 128 tavun kertalukuun, joten tarvittaessa loppuun lisätään tyhjää tilaa, jotta koko tiedosto on oikean kokoinen.

Tärkeimmät kentät MacBinary-otsikossa sisältävät seuraavat tiedot: tiedoston nimen pituus, tiedoston nimi, tiedostotyyppi (tässä tapauksessa "PNTG" MacPaint-kuville), tiedostojen luojatunniste ("MPNT"), datavarsan pituus sekä tiedoston luonti- ja muokkausajat Mac OS:n aikaleimaformaatissa. Tärkeää on huomioida, että kaikki tiedot tallennetaan Big Endian -järjestyksessä, koska klassinen Mac OS käytti Big Endian -muotoa.

Tämän jälkeen tiedoston koko voidaan määrittää MacBinary-otsikon perusteella ja se voidaan siirtää muiden käyttöjärjestelmien kautta ilman, että tiedoston eheys vaarantuu. Tämä mahdollisti myös sen, että retro-Mac-tiedostot, kuten MacPaint-kuvat, pystyttiin avaamaan oikeassa sovelluksessa ilman, että niiden siirto toiseen järjestelmään olisi johtanut toimimattomuuteen tai tietojen menetykseen.

Tärkeää on myös huomata, että vaikka MacBinary-tiedostot mahdollistavat tiedostojen siirtämisen muiden käyttöjärjestelmien välillä, tämä prosessi ei ole täydellinen ilman huolellista tiedostojen käsittelyä. Esimerkiksi tiedostojen käsittely ja avaaminen voivat vaihdella riippuen siitä, kuinka hyvin eri ohjelmat käsittelevät MacBinary-muotoa. Retro-Mac-sovellusten, kuten MacPaintin, toimivuus on usein sidoksissa siihen, kuinka tarkasti MacBinary-standardeja noudatetaan.

Miten NES-emulaattori rakennetaan ja miksi se on haastavaa

Vanhojen pelien pelaaminen nykytekniikalla on kiehtovaa, ja se vie meidät aikamatkalle pelimaailman alkujuurille. 1990-luvun loppupuolella Dreamcastin julkaisu oli yksi aikakauden merkittävimpiä hetkiä, mutta sen aikaiset pelikehityksen haasteet olivat täysin erilaisia verrattuna nykypäivän peleihin. Tuohon aikaan, kuten NES (Nintendo Entertainment System) -pelikonsolilla, pelit tulivat valmiina, eikä niitä ollut mahdollista päivittää. Jos pelissä oli virhe, se oli vain hyväksyttävä, koska versio 1.0 oli viimeinen.

Tämän aikakauden pelien kehittämisessä oli erityinen tarkkuus ja tekninen osaaminen, joka ei ollut vain haasteena, vaan myös osa sen aikaisen pelin kehittäjien erikoistunutta osaamista. Nykyisin pelit rakennetaan usein valmiiden pelimoottorien, kuten Unreal Engine tai Unity, päälle. Tällöin kehittäjät voivat keskittyä pelimekaniikoiden kehittämiseen, mutta NES-ajan kehittäjien oli kirjoitettava oma pelimoottori suoraan konekielellä, jotta saatiin aikaiseksi pelissä tarvittavat toiminnot.

Pelien kehittäjät kirjoittivat itse koodin, joka hallitsi niin grafiikkaa kuin ääntä. Esimerkiksi PPU (Picture Processing Unit) oli vastuussa grafiikasta ja APU (Audio Processing Unit) äänen käsittelystä. Tällöin oli tärkeää käyttää jokaista suoritinpiirin sykliä maksimaalisesti, sillä prosessointiteho oli rajallinen ja pienimmätkin virheet tai viivästykset saatoivat vaikuttaa pelin kokonaistulokseen.

Tässä ympäristössä syntyi joitakin historian merkittävimpiä pelejä. Vaikka teknologia oli rajallista, pelit olivat uskomattoman monimuotoisia ja niissä oli omat erikoisuutensa. Pelin kehittäjät joutuivat käyttämään luonteenomaista kekseliäisyyttä ja huolellisuutta jokaisessa vaiheessa.

Kun nykyään rakennetaan emulaattoria vanhoille pelikonsolille, kuten NES:lle, tekninen taakka ja monimutkaisuus eivät ole paljoakaan vähenneet. Tavoitteena ei ole vain pelin virtuaalinen jäljittely, vaan myös yksityiskohtien, kuten PPU:n ja CPU:n tarkka simulointi, jotta emulaattori vastaa alkuperäistä laitteistoa mahdollisimman tarkasti.

Rakennettaessa NES-emulaattoria huomioitavaa on, että emulaattori tulee olemaan hyvin yksinkertaistettu versio alkuperäisestä konsolista. Emulaattorin kehittäminen keskittyy vain muutamien komponenttien perustoimintojen jäljittelyyn, kuten CPU:n ja PPU:n peruskäyttäytymiseen. Lisäksi äänet eivät ole mukana, koska APU:ta ei ole vielä toteutettu. Pelit toimivat kuitenkin, ja ne ovat pelattavissa, vaikka täydellistä nopeutta ja virheettömyyttä ei voidakaan saavuttaa.

Emulaattorin pääkomponentit koostuvat kolmesta luokasta: CPU, PPU ja ROM (pelikonsolin "pelikiekko"). Koodi jaetaan useisiin tiedostoihin, kuten main.py (emulaattorin päälooppi), rom.py (ROM-tiedoston käsittely) ja cpu.py (suoritin), jotka kaikki yhdessä rakentavat pelin toimintaa. Suurin osa emulaattorin pääsilmukasta pyörii koodissa, joka lataa ROM-tiedostot, tulkitsee käskyjä ja piirtää grafiikkaa käyttäen Pygamea.

Näiden komponenttien luominen ei ole pelkästään ohjelmointia vaan myös laitteistomallin ymmärtämistä. Esimerkiksi NES:llä oli tarkka rakenne, jossa CPU toimi 1.8 MHz:n taajuudella ja PPU jopa 5.4 MHz:n taajuudella, joka oli kolme kertaa nopeampi. Emulaattorin tulee ottaa tämä huomioon, jotta se pystyy jäljittelemään oikeaa nopeutta ja aikaviiveitä.

Mikä tekee emulaattorin tarkkuuden varmistamisesta erityisen haastavaa, on tarve jäljitellä NES:lle ominaista piirilevyjen ja ohjelmistojen yhteistoimintaa. Esimerkiksi PPU ei pelkästään piirrä ruutua pikseli kerrallaan, vaan se suorittaa monimutkaisempia piirteitä kuten "scanline"-prosesseja ja horisontaalista välivaihetta (hblank) ennen pystysuoraa välivaihetta (vblank), jolloin se sallii CPU:n muuttaa muistia ilman häiriöitä.

Vain tarkasti ohjelmoimalla voidaan simuloida PPU:n ja CPU:n välistä vuorovaikutusta. Esimerkiksi, kun peli on valmis ja uusi ruutu piirtyy, emulaattori voi piirtää koko ruudun kerralla, mutta monimutkaisemmissa peleissä, joissa grafiikkaa muutetaan ruudun piirtämisen aikana, tämä tekniikka ei ole riittävä.

Tässäkin kontekstissa emulaattorin kehittäminen on enemmän kuin pelkkä jäljittelyprosessi; se vaatii ohjelmoijilta syvällistä ymmärrystä alkuperäisen laitteiston toiminnasta, jotta voidaan luoda mahdollisimman tarkka kokemus. Emulaattori on vain ensimmäinen askel kohti täydellistä simulointia, mutta se tarjoaa hyvän perustan lisäominaisuuksien kehittämiselle.

Miksi värit vaihtelevat NES-laitteistojen välillä?

NES-konsolin grafiikka on monivaiheinen prosessi, joka vaatii tarkkaa hallintaa eri muistialueista, kuten sprite-muistista (OAM), nimetaulumuistista ja välimuistista. Erityisesti värit voivat vaihdella eri NES-laitteistojen, kuten NTSC- ja PAL-version, välillä. Tämä johtuu siitä, että samat pelit voivat näyttää hieman eri värisiltä eri laitteistojen tai emulaattoreiden käyttöjärjestelmien mukaan. Tässä luvussa käsitellään, miten PPU (Picture Processing Unit) hallitsee grafiikkaa ja miten sen toiminnot vaikuttavat pelin visuaaliseen ilmeeseen.

PPU-luokka, joka on keskeinen osa emulaattoria, hallitsee grafiikkarenderöintiä ja sisältää tärkeitä muistialueita. Sen alustus määrittelee tärkeimmät parametrit, kuten sprite-muistin, nimetaulun ja välimuistin. Esimerkiksi sprite-muisti (OAM) on käytössä hahmojen piirtämiseen näytölle, kun taas nimetaulut määrittelevät taustan graafisen esityksen. Välimuisti sisältää pikselitiedot ja värit, joita käytetään loppukuvan luomiseen.

PPU-luokan konstruktorissa määritellään myös tärkeät PPU-rekisterit, kuten näyttömuistialueet, taustat ja sprite-kuvat. Näiden rekisterien avulla voidaan hallita, mitkä osat ruudusta näytetään ja milloin, ja luodaan lopullinen visuaalinen kokemus. Vaikka PPU:n rakenteen ymmärtäminen voi tuntua monimutkaiselta, sen perusperiaatteet ovat selkeitä: se piirtää taustat ja hahmot, ja sen tilat, kuten "vblank" (vertikaalinen synkronointi), synkronoivat CPU:n ja PPU:n toimet.

PPU:n "step" -metodi on yksinkertaistettu tapa piirtää kuva yhteen kehykseen. Tämän metodin sisällä, kun näytön skannauslinja on 240 ja sykli 256, tausta ja sprite-kuvat piirretään ruudulle. Tällöin tausta ja sprite-kuvat näytetään kerralla, mikä ei ole tarkka skannauslinjatasolla, mutta on riittävä simulaation kannalta. Tämä mahdollistaa yksinkertaisemman ja tehokkaamman emulaation ilman, että tarvitaan liiallista tarkkuutta jokaiseen pikseliin.

Tärkeä osa taustan piirtämistä on nimetaulujen ja attribuuttitaulujen käyttö. Nimetaulu pitää kirjaa ruudun rakennetta ja määrittelee, mitkä grafiikkatiedot kuuluvat mihinkin kohtaan ruudulla. Jokainen nimetaulun tulo vastaa tiettyä ruudun palaa, joka koostuu 8x8 pikselistä. Nimetaulut puolestaan jakautuvat attribuuttitauluihin, jotka säilyttävät väritiedot ja kertovat, kuinka taustakuvioiden värit jakautuvat.

Attribuuttitaulujen rakenne on tärkeä, koska se määrää, miten värit liittyvät kukin pelialueen tietyihin kohtiin. Attribuuttitaulut eivät ole pelkästään väritietoa, vaan ne myös asettavat, kuinka pikselit näyttäytyvät tietyissä taustan osissa. Tämä yhdistää pelissä käytetyt värit ja grafiikan elementit, joita pelissä käytetään, mutta myös värit voivat vaihdella laitteistosta riippuen.

Sprite-kuvien piirtämisen logiikka on monivaiheinen prosessi, jossa hyödynnetään useita muistialueita ja väriattribuutteja. Kuten taustan piirtämisessä, myös sprite-kuvissa pikselit haetaan muistista ja sijoitetaan ruudulle. Kaikki pikselit, oli kyseessä tausta tai hahmot, saavat lopullisen väriarvon NES:n omasta väri-muistista, joka määrittelee, miten värit näkyvät pelikuvan lopullisessa versiossa.

Tätä järjestelmää tukee muun muassa näytön tilan seuranta ja sen eri osien hallinta. Kun tarkastellaan, miten PPU hoitaa piirron ja värit, huomataan, että värit voivat vaihdella eri laitteistojen välillä, koska tietyn pikselin väri saattaa vaihdella emulaattorin tai alkuperäisen laitteiston mukaan. Erityisesti NTSC- ja PAL-erot voivat aiheuttaa pieniä visuaalisia eroja, vaikka peli olisi sama.

On tärkeää ymmärtää, että NES:n grafiikka ei ole pelkästään staattista kuvanpiirtoa, vaan se on monivaiheinen prosessi, jossa muistissa oleva data muuntuu lopulliseksi visuaaliseksi esitykseksi. Tämä prosessi on alun perin suunniteltu tehokkuuden maksimoimiseksi, mutta se tuo myös esiin sen, miksi värit ja yksityiskohdat voivat poiketa toisistaan laitteistojen välillä. Ymmärtämällä, kuinka PPU ja sen eri muistialueet toimivat, saadaan parempi käsitys siitä, miten emulaattorit voivat vaikuttaa pelin visuaaliseen kokemukseen.