                            
    ۲      ۲    ۱    ۰  ۲     
    ۱      ۱    ۰       ۱   ۰ 
    ۰     ۱   ۰   ۰ ۱   ۱ 
         ۰      ۱    ۱  
    ۰       ۰۱ ۲    ۲ ۱ 
    ۱     ۰۰ ۱ ۲ ۱ ۱ ۱  ۱ ۰ 
    ۲    ۰۱ ۱ ۱ ۰ ۰ ۰  ۰  
    ۱ ۱  ۲     ۰  
    ۰       ۱۰   
            ۲۱ ۰  
    ۰    ۱  ۱ ۲ ۱ ۱ 
               
      Laaman tie DJGPP-peliohjelmointiin versio 2.10. By Jokke / BAD KARMA
	 Copyright (C) Joonas Pihlajamaa 1997. All rights reserved.


Sisllysluettelo:

1. Esittely
        1.1  Disclaimer
        1.2  Mist uusin versio?
        1.3  Huomattavaa lukijalle
        1.4  Kenelle tm on tarkoitettu?
        1.5  Kreditsit
        1.6  Versiohistoria
        1.7  Yhteystiedot
        1.8  Esimerkkien kntminen
2. Alkeet
        2.1  DJGPP - vaikea, suuri, monimutkainen, omituinen, hidas?
        2.2  Grafiikkaa - mit se on?
        2.3  Paletti - hrhelhameita ja tanssia?
3. Peruskikkoja
        3.1  Kaksoispuskuri - luonnonoikku, horoskooppi?
        3.2  PCX-kuvien lataus - vain vhn oikaisemalla
4. Bittikartat ja animaatiot
        4.1  Bitmapit - eikai vain suunnistusta?
        4.2  Animaatiot
        4.3  Pitk spriten trmt? Ent coca-colan?
        4.4  Maskatut spritet
5. Hieman kehittyneemp yleistavaraa
        5.1  Nppimistn ksittely - ja nyt meill on hauskaa
        5.2  Fixed point matematiikka
        5.3  Lookup-tablet ja muita optimointivinkkej
        5.4  Vliaikatulokset ja fontteja
        5.5  Hiirulainen, jokanrtin oma lemmikki
        5.6  Tekstitilan ksittely suoraan
6. Projektinhallinta
        6.1  Projektien hallinta - useat tiedostot
        6.2  Useiden tiedostojen projektit - kntminen ja hallinta
        6.3  Hieman automaatiota - tapaus Rhide
        6.4  Todellista guruutta - salaperinen make
        6.5  Ammattimaista meininki - enginen teko
7. Kehittyneemmt yksityiskohdat
        7.1  Vauhtia peliin - ulkoisen assyn kytt
        7.2  PIT - aikaa ja purkkaa
        7.3  Miten peli toimii yht nopeasti kaikilla koneilla
        7.4  Yleist asiaa pelin levityksest
        7.5  Interpolointi ja viivoja
        7.6  Vapaa skrollaus
        7.7  Sinit ja kosinit sek plasmaa
        7.8  Paletin kvantisointi ja rekursio - Median cut
        7.9  Lis paletin kvantisointia - Local K Mean
        7.10 VESA 2.0, rakenteet
        7.11 VESA 2.0, ensimmiset keskeytykset
        7.12 Miten se todella pitisi tehd
8. Asioiden taustaa
        8.1  Datatiedostot - miten?
        8.2  Lpinkyvyys ja sen vaihtoehto - shadebobit
        8.3  Motion blur - sumeeta menoa
        8.4  Vektorit pelimaailmassa
        8.5  Musiikkijrjestelmist
        8.6  Plasma tekee comebackin - wobblerit
        8.7  Prekalkattuja pintoja - ja tunneli
        8.8  Lis kivaa - zoomaus
        8.9  Polygoneista ja niiden fillauksesta
        8.10 Pari kivaa feikkimoodia
9. Liitteet, jatkeet ja muu roina
        9.1  Saatteeksi
        9.2  Hieman koodereiden jargonia
        9.3  Lhteet
        

1.1 Disclaimer
--------------

Tmn dokumentin ja kaikkien muiden paketin tiedostojen tekijnoikeudet
kuuluvat Joonas Pihlajamaalle, ellei tiedostossa ole toisin ilmoitettu
ja nm ehdot ptevt kaikkiin paketin tiedostoihin jotka eivt
sisll erillisi ehtoja tai joista ei ole niss ehdoissa erikseen
mainittu. Paketin sisltmn materiaalin kytt on sallittu vain
allaolevien ehtojen rajoissa. Jos kyttj ei hyvksy ehtoja tulee
hnen poistaa tm paketti ja sen tiedostot. Paketin sisltmn
materiaalin kytt tarkoittaa kyttjn hyvksyneen levitysehdot.

Dokumentin levitys, monistus ja muu jakelu on sallittu vain
alkuperisess, muuttamattomassa muodossa, lukuunottamatta file_id.diz
-tiedostoa, joka voidaan halutessa uudelleennimet .old- tai
.org-ptteiseksi ja list uusi .diz-tiedosto, jotta kuvaus sopisi
levitettvn BBS-jrjestelmn kyttmn formaattiin.

Minknlaista maksua ei saa peri lukuunottamatta kopiointi- ja
levityskustannuksia, niin kauan kuin niiden yhteenlaskettu summa ei
ylit 20 suomen markkaa. Lhdekoodin kytt on sallittu omissa
ohjelmissa, mutta ohjelman dokumentaatiossa tytyy mainita lhdekoodin
lhde. Tutoriaalin kautta opittu tieto on tysin vapaasti
sovellettavissa.

Tekij ei ota minknlaista vastuuta paketin tiedostojen toiminnasta tai
tietojen oikeellisuudesta. Minknlaista takuuta tutoriaalin sisltmn
informaation kytnnllisyydest ja virheettmyydest ei anneta.

Jos paketti aiotaan sisllytt jonkin suuren tiedostopalvelimen,
CD-ROM levyn tai muun vastaavan massalevitykseen tarkoitetun median
jonka oletetaan levivn suuria mri olisi tekijlle hyv ilmoittaa
shkpostitse tapahtumasta.

Tutoriaaliin liittyy mys rajoitettu tyytyvisyystakuu. Jos et jostain
syyst pid tuotteesta voit poistaa sen mrmttmn ajan jlkeen
ohjelman asennuksesta. Vapautat kiintolevytilaasi ja saat ilman
erillist maksua kokea tutoriaalin poistamisesta aiheutuvan henkisen
tyydytyksen.

Epselvyydet, puutteet ja huomautukset disclaimerista pyydetn
lhettmn tekijlle.


1.2 Mist uusin versio?
-----------------------

Tutoriaalin teko alkoi alunperin MBnetin FAQ-jahkailusta, kun veikkailtiin
tehtisiink PC-Ohjelmointi -alueen kysymyksist FAQ vai eik. Min ptin
sitten tehd ainakin jotain ja niinp uusin versio pitisi olla aina
saatavilla MBnetist PC-Ohjelmointi -alueelta. Alue tullaan jakamaan
jossain vaiheessa, mutta Apajalta se lytyy ainakin.

Lisksi Laamatutin virallinen kotisivu lytyy osoitteen www.mbnet.fi/~jokke
alta. Tst osoitteesta pitisi myskin lyty Laamatutin uusin versio
nopeasti ja helposti (jopa nopeammin kuin mit se tulee MBnettiin).

Tiedostonimi on aina LAAMAxyy.ZIP, jossa x on suurempi (major) versionumero
ja yy pienempi (minor). Pitkhn silm tarkkana!


1.3 Huomattavaa lukijalle
-------------------------

Dokumentin koko on alkuperisest jo viisinkertaistunut ja public
betasta ei voine en puhua. Silti kommentteja tydellisyyksist,
virheist ja puutteista tarvitaan ehk jopa enemmn kuin beta-aikoina,
kun alue on liian laaja yksin tarkistettavaksi. Olen mys kiinnostunut
mahdollisista lisjuttujen tekijist, jolloin luonnollisesti minun ei
tarvitse kirjoittaa kaikkea. Korvauksena pset sitten kreditseihin ja
dokumenttisi julkaistaan tmn mukana.

Teemu Keinonen on jo osallistunut Laamatutin tekoon ja on ninollen
ansainnut erityiskiitokseni samat kiitokset kuuluvat mys 3D-starfield
-selostuksen tehneelle Erik Seesjrvelle. Heidn teoksensa lytyvt
myskin tst phakemistosta nimill LUVUT.TXT ja STARFLD.TXT. Herra
Seesjrvi koodaa nykyn kunniallisena ihmisen 3D-engine ja
pyynnist  huolimatta starfield silyy kunniakkaana osana
tutoriaalia. Lisksi kiitoksen jo tss ansaitsee Pekka Nurminen
lukuista tarkennuksista ja lisehdotuksista joidenkin asioiden
suhteen, sek Tero Kontkanen maanmainion "Laama"-logon teosta.

Eli kun trmt johonkin epselvyyteen, pllekkisyyteen,
eploogisuuteen, toistoon, virheeseen tai puutteeseen niin
ilmoittelehan heti minulle. Osoite tuolta tiedoston lopusta. Vastaan
postiin mahdollisuuksieni mukaan (vastaan siis jokaiseen ellen sitten
huku postiin). Jokainen kommentti tekee minut iloiseksi, sill on aina
mukavaa nhd jos joku on tutoriaalista hytynyt.

Minulle saa lhett viestimuotoisen kannustuksen lisksi mys rahaa
ja 20 markkaa olisi oikein hauska ylltys joskus lyt postiluukusta,
tosin vhemmn ja enemmnkin voi halutessaan lhett. =) Mys pelkk
postikortti tai e-maili on mukavaa. Rahan takia en tt tee, saldo
taitaa thn menness olla yksi lahjoitus Erikilt. :)

Jatkossa tulen julkaisemaan uusia versioita sit mukaa kun asiaa tulee
lis. Eli pid silm tarkkana ja mieli valppaana tutkiessasi
kyttmiesi purkkien tiedostoalueita. Uusimman version lytminen on
selostettu tarkemmin luvussa 1.2. Muista, ett Laamatutin levittminen
on suorastaan toivottua muuttamattomassa muodossa, joten l epri
lhett sit suosikkipurkkeihisi!


1.4 Kenelle tm on tarkoitettu?
--------------------------------

Aloitin dokumentin kirjoittamisen Ilkka Pelkosen mainion suomenkielisen
3d-tutoriaalin innoittamana ja toivon, ett tst on hyty monille
aloitteleville peli/demokoodereille DJGPP:ll. Tutoriaali kattaa
DJGPP:n asennuksen ja monia grafiikkaohjelmoinnin perusniksej, joskus
lhdekoodinkin kanssa. Myhempi osa alkaa menemn pikkuhiljaa yh
teoreettisemmalle tasolle eik tahattomasti, sill kunnon
ohjelmointiin kuuluu paljon muutakin kuin hardware-tuntemus. Pyrin
mys valaisemaan asioita jotka kuuluvat vhemmnkin peliohjelmointiin,
mutta joista ei kunnollista juttua mistn muualta ole saatavilla. 
Ehdotuksia saa aina lhett.

Lhtvaatimuksena tmn lukijalle on siedettv matematiikan taito
(kertolaskut pit olla hallussa, kuten mys jotkin muut
perusksitteet, kuten kokonaisluvut, desimaaliluvut jne.) Sek
C-kielen taitaminen. Assemblerikin voi olla hydyllinen. Tt
kirjoittaessani en viel tied millainen tutoriaalista tulee, joten
katsotaan nyt... Teemu Keinonen on kirjoittanut thn tutoriaaliin
mainion pikku dokumentin lukujrjestelmist ja bittioperaatioista, joten
jos et niit viel hallitse niin lue ensin tiedosto LUVUT.TXT!

Tutoriaalin esimerkit EIVT MISSN NIMESS ole tarkoitettu
kytettviksi peleiss suoraan. Niit kyll saa kytt, mutta ne ovat
hitaita ja ne ovat esimerkkiohjelmia, eivt juuri yhdenlaiseen
pelityyppiin sopivia rtlityj rutiineja. Sitpaitsi mikn ei
voita kokemusta ja kirjoittaessasi omat rutiinisi opit asian paremmin
kuin mitenkn muuten. Jos min olisin kyttnyt muiden rutiineja niin
en olisi nyt tss selittmss ideaa niiden takana, vaan tekisin
alkeellisia pelej, koska en osaisi muunnella muiden koodeja peleihini
sopiviksi. Eli tm dokumentti ei kirjoita sinulle valmiiksi parhaita
ja sopivimpia rutiineja, vaan ainoastaan demonstroi mahdollisia
toteutustapoja, joka tulisi pit mieless dokumenttia lukiessa.

Huomaa mys, ett tmn on kirjoittanut OikeaIhminen(tm), jolla on
mys Shkpostiosoite, jolla voit ottaa hneen yhteytt. Mikn ei ole
minulle mieluisampaa kuin nhd, ett edes joku on tyytyvinen tai
tyytymtn thn tutoriaaliin.

Ja tietenkin koska olen oikea ihminen voit kysy minulta epselvksi
jneit kohtia ja katson voinko selvent tt ja kenties lisn
vastauksen mys seuraavaan versioon tutoriaalista ja autat siten muita
aloittelijoita. Voit jopa saada nimesi jonnekin, ken tiet? Eli kun
tulee jotain mieleen niin mene dokumentin loppuun ja lue
yhteystietoni. Mys kirjoitusvirheist, huonosta / hyvst tekstist
tai selvst tekstist kannattaa ilmoittaa, en nimittin ole ainakaan
viel lukenut tt kokonaan lvitse (lukuunottamatta kun kirjoitin
tmn). Ja kaikki enemmn osaavat voivat ilmoittaa tarkennuksia ja
oikaisuja tutoriaalin tekstiin. =)

Koulutus Kokkolan Yhteislyseon lukiossa (eli lyhyemmin Lnsipuistossa)
on nyt sitten viimein alkanut, jonka jlkeen edess on jokin
teknillinen korkeakoulu ja DI:n arvo, jos luoja suo. :)


1.5 Kreditsit
-------------

Ennenkuin aloitamme, haluaisin tervehti joukkoa tuntemiani
henkilit. Tiedoksi kaikki MBnetin ohjelmointi-alueen lukijoille,
ett ainakin yritin muistaa niin monta kuin vain mahdollista, jos
siis nimesi ei ole listassa ja tunnet sinne kuuluvasi niin ilmoittele!

Teemu Keinonen: Erityiskiitokset lukujrjestelmt -jutustasi!
Erik Seesjrvi: Kiitoksia starfieldist ja onnea 3D-enginelle. =)
Pekka Nurminen: Kiitos mainiosta palautteesta ja avusta monessa asiassa.
Tero Kontkanen: Mahtava logo! Muistinpas vihdoin list senkin.
Sami Kuronen: Alias pysyy, I hope. Jatka vain lukemista! ;)
Jyri Pieniniemi: Tll dokumentilla voi olla laksatiivisia vaikutuksia!
Ilkka Pelkonen: Sinun takiasi jouduin tllaista kirjoittamaan... Tsemppi!
Tommi Kemppainen: Koodaus, skene ja elm. Pitk muuta sanoa?-)
Johan Brandt: Tytyyhn meidn nrttien pit yht!
Asko Soukka: Onnea sen C++:ssan opettelun kanssa, toivottavasti onnistut!
Jari Karppanen: Filekamu, vain 2 vuotta myhss?-) Muistin nyt sinutkin!
Tero Karras: Jos joinaat Doomsdayhin niin katso, ett Bad Karmaa greetataan!
Jere Sanisalo: Terveisi vain sinnekin, toivottavasti Kaboomia on rekattu! ;)
Kaj Bjrklund: Toivon RC:n imevn monta sielua ja seuraavan version! :)
Aleksi Kallio: Npit irti siit Watcomista! DJGPP ja herneet 4ever!
Juhana Venlinen: Hmm, kai tagisaarto ES: vastaan on viel voimassa?-)
Marko kerberg: Menik nimi oikein?-) BLAST 'EM RULEZ, JEE!!!
Jarmo Muukka: Miten ikin JAKSAT kirjoittaa yli sadan rivinohjelmaesimerkkej?
Jukka Vuokko: Huomentapiv. Aiotko tehd Emacsiin sprite-enginen?-)
Petteri Jrvinen: Tsemi autopeliin! Toivottavasti kirje saapui perille. :)
Ilja Brysy: No toivottavasti sait jotain tolkkua jostakin =)
Henri Pyyny: Toivottavasti ette huku lumeen siell Lapissa!
Lasse Laurila: Kyll min viel saan sinut kirjoitetuissa messuissa kiinni!
Santeri Saarimaa: Yh NNY?
iti&Isi: Mit te tt luette?!?
Tomi Jutila: Olet sinkin siis pttnyt alkaa kooderiksi?-)
Timo Jutila: Quakee?!?!
Teemu Kellosalo: l vain vit ett aiot lukea tmn?
Kalle Liukkonen: Muistin sitten sinutkin. =) Shefun oikat hanskassa?-)
Juho stman: No laitoinpas sinutkin tnne. Ylltyitk?-)
The Pihlajamaa: Hemmetti, etunimi psi unohtumaan, tsemi!
Viznut / PwP: Onko sinulla jokin oikea nimikin?-) No mit tuosta...

Erityiskiitoksen ansaitsevat viel koko MBnetin yllpito, sill ilman ko.
purkkia ei minulle olisi koskaan ollut mahdollista oppia niin paljon
ohjelmoinnista, ett voisin kirjoittaa tmn. Nistkin yllpitjist
mainitsen viel erikseen Jere Kpyahon, Tarmo Toikkasen ja Rasmus Wickholmin,
jotka ovat ahkerasti olleet mukana PC-Ohjelmointialueella. Kiitos!


1.6 Versiohistoria
------------------

Kehityst on jlleen tapahtunut ja miks sen mukavampi paikka
nauttia niist etukteen kuin tm luku. Uusi termikin on ilmaantunut,
"uusi tausta" tarkoittaa selostusta toiminnasta Asioiden taustaa
-osaan.

Versio 2.1:
  + Jlleen korjauksia, pitisi alkaa olla jo aika virheetnt
    tavaraa, poistin //-kommentit ja kaikki mainit nyt tyyppi int
  + Uusi luku VESA 2.0-rakenteista
  + Uusi luku VESA 2.0-keskeytyksist
  + Uusi luku grafiikkaenginen teosta
  + Asioiden taustaa -osa, jossa kerron vain mik on homman nimi,
    koodia ei en tipu
  + Uusi tausta datatiedostoista
  + Uusi tausta lpinkyvyydest ja shadebobeista
  + Uusi tausta motion blurrista
  + Uusi tausta vektoreista pelimaailmassa
  + Uusi tausta musiikkijrjestelmist
  + Uusi tausta wobblereista
  + Uusi tausta tunneli-efektist
  + Uusi tausta zoomauksesta
  + Uusi tausta polygoneista ja niiden fillauksest
  + Uusi tausta feikkimoodeista

Versio 2.01:
  + Joukko korjauksia enemmn tai vhemmn kriittisiin asioihin
  + Ei julkisessa levityksess

Versio 2.0: The Joulu Edition Enhanced
  + Ei en READJUST.NOW -tiedostoa
  + Vaikeaselkoisempi disclaimer-teksti
  + Pikku korjauksia materiaaliin ja joitakin tarkennuksia
  + Mahtava, tuore versionumero
  + Uusi, hieno ja selke lukujako ja joitain jrjestelyj
  + Uusi, laaja (?) slangisanasto
  + Lis kiinnostavia ja selkeit ohjelmaesimerkkej
  + Uusi luku interpoloinnista ja viivanpiirrosta
  + Uusi luku skrollauksesta
  + Uusi luku sineist, kosineista ja plasmasta
  + Uusi luku kvantisoinnista median cut -algoritmilla
  + Uusi luku kvantisoinnista local K mean -algoritmilla

Versio 1.3: Assembly-mix, jotain purtavaa mys demokoodereille
  + Tarkennuksia ja parannuksia VGA:n muistista kertovaan osaan
  + Lis koodia pseudona bitmap-osuuteen ja muutenkin enemmn
    selvennyst ko. kohtaan. Kiitoksia selvennyspyynnist.
  + Uusi luku useiden C-tiedostojen kytst
  + Uusi luku objekti- ja archive-tiedostojen teosta
  + Uusi luku Rhiden konffauksesta ja projektinhallinnasta
  + Uusi luku makefileiden kytst
  + Uusi luku enginen teosta
  + Uusi luku ulkoisen assyn kytst
  + Uusi luku timerin koukutuksesta C:ll
  + Uusi luku frameskipist
  + EJS:n starfield-esimerkki ja -selostus.

Versio 1.2: Kes-release, toinen julkisesti levitetty versio
  + Hiiren ksittely
  + Tekstitilan ksittely
  + Lis korjauksia, kiitos ahkeran palautteen

Versio 1.1: Bugikorjaus-release, ei yleisesti levityksess
  + Lukuisia korjauksia enemmn tai vhemmn vialliseen
    tietoon siell sun tll tutoriaalissa

Versio 1.0: Ensimminen julkaistu versio
  + DJGPP:n asenuns
  + Grafiikka
  + Paletti
  + Kaksoispuskuri
  + PCX-kuvat
  + Bittikartat
  + Animaatiot
  + Spritet
  + Nppimist
  + Fixed point
  + Lookup-tablet
  + Fontit
  + Maskatut spritet


1.7 Yhteystiedot
----------------

Hyv, olet siis pttnyt ottaa yhteytt minuun. Yhteyden minuun saat
useallakin tavalla, mutta tss ovat ne joita luultavimmin tarvitset:

www.mbnet.fi/~jokke/ sislt minun, Bad Karman ja sen tuotosten, sek
Laamatutin viralliset kotisivut sek joukon linkkej maailmalle (ainakin
jossain vaiheessa ;).

joonas.pihlajamaa@mbnet.fi on shkpostiosoite, josta minut pitisi saada
kiinni.

Joonas Pihlajamaa on kyttjtunnukseni MBnetiss, jolle voit kirjoittaa
yksityispostiin. Ainakin tll hetkell luen viestini keskimrin 3 kertaa
viikossa, joten vastaus pitisi tulla viikon sisll (ellen ole
lomailemassa tai paastolla koneestani ;).

Joonas Pihlajamaa
Sveltjntie 40
67300 Kokkola

Tm on se osoite, jossa asun. Jos et aivan kymn viitsi tulla niin mikset
lhettisi postikortilla terveisi? Vastauksista kirjeisiin en tied, mutta
katsotaan nyt, ei ole ainakaan viel tullut ainoatakaan kirjett...

Kuulun gruuppiin BAD KARMA, joka tekee tll hetkell peli nimeltn
SLiDER: Roadkill, joka on autopeli ja sen on tarkoitus hakata Slicks 'n'
Slide sek muut vastaavat pelit mennen tullen. Kannattaa tutkia tarkasti
purkkien tiedostoalueita, jos vaikka ilmestyisi. Ilmestymisajankohta
on luultavasti (ensi?-) vuosituhannen loppupuolella.


1.8 Esimerkkien kntminen
---------------------------

Tutoriaalin mukana seuraa sankka joukko esimerkkiohjelmia ja ne
lytyvt hakemistosta EXAMPLE. Jos sinulla on 'make', niin knt
sujuu yksinkertaisesti menemll esimerkkikoodit sisltvn
hakemistoon ja ajamalla komennon 'make' ja sen jlkeen 'make test.exe'
jos sinulla on NASM. 'make clean' / 'make realclean' vastaavasti
tyhjentvt objektitiedostot / objekti- ja exetiedostot.

Kiitoksia Tero Kontkaselle makefile-esimerkist. Tein sen pohjalta nyt
uuden, koska esimerkkiohjelmia oli tullut jonkin verran lis. Jos
sinulla ei ole 'make'-ohjelmaa onnistuu kntminen ksinkin. Lhes
kaikki tiedostot ovat itsenisi eivtk tarvitse muita
objektitiedostoja tai kirjastoja toimiakseen. Poikkeuksina
timertst.exe joka tarvitsee sek timer.c:n ja timertst.c:n knnettyn
ja test.exe, joka tarvitsee test.asm:n ja test.c:n knnettyn.

Hauskaa kokeilua, min menen nukkumaan!


2.1 DJGPP - vaikea, suuri, monimutkainen, omituinen, hidas?
-----------------------------------------------------------

Tutoriaali sivuaa koko ajan DJ Delorien ilmaista Gnu-kntj
DOS:ille, eli DJGPP:t, erityisesti sen kakkosversiota. Itse siirryin
puolessa vliss tt tutoriaalia 2.0 -versiosta versioon 2.01 ja
luulisin, ett esimerkit toimivat molemmilla nist versioista ja
luultavasti uudemmillakin. Vanhemmat versiot eivt luultavastikaan
toimi niden lhdekoodien kanssa. 

Tmn mahtavan ilmaiskntjn lydt esimerkiksi internetist
osoitteesta ftp://x2ftp.oulu.fi jostain
pub/msdos/programming-hakemiston alihakemistosta. Sen saa mys
MBnetist, tarvittavat tiedostot ovat alueella PC-Ohjelmointi (area
8), tiedostoja on useita, ja ne lytyvt ko. alueelta lytyvst
MBNETDJ2.TXT:st. Mys kaikille Mikrobitin tilaajille tullut Huvi &
Hytyromppu sislt tmn kntjn hakemistossa MIKROBIT\DJGPP201\,
tosin sielt puuttuu LGP2721B.ZIP (tarvitaan C++ koodin kntmisess),
jonka Kpyaho unohti laittaa mukaan. Halutessasi voit hakea puuttuvan
tiedoston MBnetist.

DJGPP:n asennukseen purat vain kaikki tarvitsemasi paketit haluamaasi
hakemistoon (esim. D:\OHJELMAT\DJGPP) PKUNZIP:in -d parametrill. Sen
jlkeen list polkuun tuon hakemiston alihakemiston BIN (esim.
D:\OHJELMAT\DJGPP\BIN), ja viel lopuksi teet uuden environment-muuttujan
DJGPP, joka osoittaa DJGPP:n juurihakemistossa olevaan DJGPP.ENV
-tiedostoon. Eli esim.:

SET DJGPP=D:\OHJELMAT\DJGPP\DJGPP.ENV

Nyt voit kokeilla toimivuutta tekemll pienen C-ohjelman (vaikka
koe.c) ja kirjoittamalla:

GCC koe.c -o koe.exe

Lis infoa GCC:n knnsoptioista ja kntjst saat kirjoittamalla:

INFO GCC

Suosittelisin ett lueskelet DJGPP:n dokumentaatiota ja teet tss
vaiheessa paljon testiohjelmia ja opettelet kyttmn
info-lukijaa. Hydyllinen hankinta on mys Rhide, joka on IDE
DJGPP:lle. Ohjelma lytyy MBnetist alueelta 8 (ETSI RHIDE) sek
H&H-Rompulta. Kun tunnet osaavasi kytt vaivattomasti kntj
palaa takaisin dokumentin pariin.

Jos et viel C:t osaa, niin hanki jostain, esimerkiksi kirjastosta hyv
kirja ja opettele sen avulla C-ohjelmointi. En aio alkaa
selittmn kaikkein yksinkertaisimpia asioita esimerkkikoodeissa
taikka kommentoimaan liiemmlti koodia.


2.2 Grafiikkaa - mit se on?
----------------------------

No olet siis pttnyt edet seuraavaan aiheeseen, joka nyttisi
olevan grafiikan ohjelmointi DJGPP:ll. Aloittakaamme siis! Tiedoksi
nyt etukteen, ett muistiosoitteet ovat heksoina, vaikkei sit 
ilmoitetakaan.

Esimerkkin kytn VGA:n perusmoodia, 13h (heksaluku, desimaalina
19), joka on erittin helppokyttinen. Kun tarvitset muita moodeja
sinulla on varmasti jo tarpeeksi taitoa hankkia itse informaatiota,
mutta tmn neuvon ihan alusta alkaen.

Eli olipa kerran PC, jossa oli 16-bittinen muistivyl, joka salli
vain 64 kilon osoittamisen kerralla, sill 16-bittisell osoitteella
voidaan maksimissaan osoittaa 2^16=65536 tavua muistia. PC:n oli
suunnitellut Intel, mutta PC:hen oli luvattu yli 64 kilotavua muistia
ja 32-bittinen muistivyl oli niihin aikoihin kovin kallis. Joten
joku sai suorastaan neronleimauksen: Jaetaan koko muisti 64 kilon
palasiin!

En syvenny tekniikkaan sen kummemmin, vaan totean vain, ett 8088
prosessoriin perustuvassa PC:ss muodostettiin muisti SEGMENTIST ja
OFFSETISTA (SEG:OFF, esim B800:0000). Todellinen osoite muistissa 
saatiin kertomalla SEGMENTTI kuudellatoista ja lismll siihen
OFFSET. (B800:0000 = B800*16+0000 = B8000) Ja kun kummatkin olivat
16-bittisi lukuja saatiin nin 20-bittinen siirrososoite. Ja koska 20
bitill voi ilmoittaa tsmlleen kaksi potensiin 20 eri arvoa oli
maksimimr mit voidaan osoittaa 1 megatavu. Kymmenen ensimmist
segmentti (eli 0000 1000 2000 3000 4000 5000 6000 7000 8000 ja 9000)
omistettiin ohjelmille ja nimettiin perusmuistiksi, jota oli siis
10*64=640 kilotavua. Sitten segmentist A000 alkoi grafiikkamuisti.

No tietokoneet kehittyivt ja esiteltiin suojattu tila, eli PROTECTED
MODE (PM), joka ksitteli koko muistia selektoreilla ja offseteilla,
jotka olivat entisen 16 bitin sijasta 32-bittisi (selektorit ovat
kuitenkin yh 16-bittisi). Vanhat segmenttien varastoimiseen tarkoitetut
SEGMENTTIREKISTERIT varattiin nyt selektoreille, jotka kertoivat
prosessorille, mit LOOGISTA muistialuetta ksiteltiin. DJGPP, joka on
suojatun tilan kntj esim. antaa ohjelmalle alussa 2 selektoria, toinen
osoittaa dataan ja toinen koodiin. Tst pidemmlle en tied tarkasti,
mutta riitt tiet, ett selektorin osoittaessa dataan ei offset 1234
todellakaan ole muistissa kohdassa 1234, vaan se on ohjelman oman 
data-alueen 1234. tavu.

Ja mik meit kiinnostaa, on perusmuistin 11. segmentti, jonka osoite
siis oli A000:0000. Siirrososoite on siis A000*16+0000 = A0000. Mutta,
kuten muistamme, ei onnistu, ett vain tekisimme pointterin, joka osoittaa
tuonne osoitteeseen, sill ohjelman datahan on aivan toisessa
selektorissa kuin perusmuisti. Meidn tytyy ensin lyt oikea
selektori, jonka osoittama looginen muistialue vastaisi PC:n
perusmuistia. Ja tllainen lytyykin nimell _dos_ds. Tmn selektorin
osoittaman muistialueen 0. tavu on perusmuistin 0. tavu, 1. tavu on
perusmuistin 1. tavu ja niin jatkuu edelleen, kunnes tavu numero A0000
on ensimminen VGA:n grafiikkamuistin tavu.

Nyt meill on siis tiedossa segmentin A000, eli VGA-kortin
muistialueen siirrososoite, A0000 ja oikea selektori, _dos_ds. Mutta
miten laitamme tavun tuonne? Hyv kysymys. Se onnistuu vhintn 5:ll
eri tavalla, mutta perehdymme helpompaan. Kirjaston sys/farptr.h
funktioon _farpokeb(selektori, siirrososoite, tavu), jolla psemme
ksiksi tuonne. Normaalin pointterin tekohan ei onnistu, vaan meill
pit olla funktio, joka kykenee osoittamaan toisen selektorin
alueelle.

Ninollen esimerkkiohjelma, joka asettaa VGA-muistin 235. tavun arvoon
100 on tmn nkinen (PIXEL1.C):

#include <go32.h> /* muistathan, _dos_ds on mritelty tll! */
#include <sys/farptr.h> /* tlt lytyy _farpokeb */

int main() {
    int selektori=_dos_ds, 
      siirros=0xA0000 + 235, 
      arvo=100;

    _farpokeb( selektori, siirros, arvo );

    return 0;
}

Arvaan, ett ehk menit ja kokeilit tuota ja petyit, kun mitn ei
tapahtunutkaan. Ei se mitn, niin pitkin tapahtua, sill olimme
tekstitilassa. Jotta jotain tapahtuisi meidn pit olla oikeassa
tilassa, joka oli siis 0x13 (heksanumero 13 C:ss, desimaalimuodossa
19). Tmn tilan rakenne onkin seuraava mihin perehdymme. Ole huoleti,
valitsin tmn tilan, sill se on KAIKKEIN yksinkertaisin tila
PC-yhteensopivalla tietokoneella. Resoluutio on 320 rivi vaakatasossa
ja 200 pystytasossa. Jokaista pikseli merkitn yhdell tavulla, eli
sill voi olla 256 erilaista arvoa. Nytt alkaa aivan ruudun
vasemmasta ylkulmasta (miksi? sit ei kukaan oikein tied, menee
filosofiaksi) ja jatkuu tavu tavulta (pikseli pikselilt) pttyen
lopulta oikeaan alakulmaan. Eli ensimmiset 320 tavua ovat ensimmisen
rivin kaikki vaakatasossa olevat pikselit, sitten seuraavat 320 ovat
toisen rivin pikselit, kunnes lopulta ollaan ruudun alakulmassa.

Ja kun muistamme, ett ensimminen tavu on kohdassa A0000 (heksa siis
tmkin), eli 0 tavua alusta eteenpin, niin me voimmekin tehd hienon
kaavion:

Pikselit:        Sijainti:
..........................
0...319          1. rivi
320...639        2. rivi
...
63680...63999    200. rivi

Nin meill onkin hieno kaava, jolla saamme selville pikselin
sijainnin:

alkio = rivi * 320 + sarake    eli:
offset = y*320+x

Muista, ett C:ss 1. rivi olisi tietenkin rivi numero 0!

Nyt yhdistmme tietomme: VGA:n muisti sijaitsee selektorissa _dos_ds,
alkaen osoitteesta A0000 (heksa, C:ss 0xA0000) ja siit lhtee 64000
tavua, joka on nyttmuisti. Pikselin osoite tss muistissa voidaan
laskea kaavalla y*320+x. Selektorin kanssa voidaan muistia asettaa
komennolla _farpokeb(selektori, siirros, arvo). Tarvittava moodi on
0x13 ja siin on 256 vri ja resoluutio 320 x 200.

Mutta miten psemme sinne? Vastaus on helppo: conio.h:n funktiolla
textmode(moodi)! Ja kun viel yhdistmme thn funktion getch(), joka
odottaa napinpainallusta (lytyy myskin kirjastosta conio.h), sek
palaamme lopuksi tekstitilaan (0x3, eli heksa 3, eli desimaali 3) on
meill jo aika kiva ohjelma kasassa (PIXEL2.C):

#include <go32.h> /* _dos_ds ! */
#include <sys/farptr.h> /* _farpokeb(selektori, siirros, arvo) */
#include <conio.h> /* textmode(moodi), getch() */

int main() {
    int selektori=_dos_ds, siirros=0xA0000, y=100, x=160, 
		  graffa=0x13, texti=0x3, color=100;
    
    textmode(graffa);
    _farpokeb(selektori, siirros+y*320+x, color);
    getch();
    textmode(texti);

    return 0;
}

Tietenkin olisi ollut helpompaa sijoittaa arvo suoraan parametrin
kohdalle:

textmode(0x13);
_farpokeb(_dos_ds, 0xA0000+100*320+160, 100);
getch();
textmode(0x3);

Mutta katsoin aiemman tavan havainnollisemmaksi. Kaiken tekemiseksi
oikein helpoksi teemme tst pikselinsytytyksest makron
#define-komennolla. Tm ei hidasta ohjelmaa yhtn, mutta varmasti
selvent koodia. Se mrittelee makron putpixel(x, y, c), jonka
kntj muuttaa knnsvaiheeksa _farpokeb-funktioksi. x tarkoittaa
saraketta vlilt 0-319 ja y rivi vlilt 0-199, sek c vri vlilt
0-255. Muista, ett vaikka teetkin makron sinun pit silti
sisllytt mukaan kirjastot sys/farptr.h ja go32.h! Sulut makron
farpokeb-funktion muuttujien x ja y ymprill selittyvt sill, ett
koska makro puretaan suoraan kutsukohtaan niin esim. komento:
putpixel(50, 40+a, 100) purkautuisi muotoon: _farpokeb( _dos_ds,
0xA0000+40+a*320+50, 100), joka ei tietenkn ole haluttu tulos, sill
40+a pit ksitell ennen sijoitusta, eli sulut vain ymprille! Tss
se siis on:

#define putpixel(x, y, c) _farpokeb( _dos_ds, 0xA0000+(y)*320+(x), c)

Kun haluat kytt sit, niin teet vaikka seuraavanlaisen
koodinptkn (PIXEL3.C):

#include <sys/farptr.h>
#include <go32.h>
#include <conio.h> /* textmode(moodi) ja getch() lytyvt tlt! */

#define putpixel(x, y, c) _farpokeb( _dos_ds, 0xA0000+y*320+x, c)

int main() {
    textmode(0x13);
    putpixel(319, 199, 150);
    getch();
    textmode(0x3);
    return 0;
}

Ohjelma sytytt pikselin aivan ruudun alareunaan. Jos et en muista,
miten ohjelma knnettiin DJGPP:ll, on tmn kokeilemiseksi
tarvittava komento: "GCC PIXEL3.C -o PIXEL3.EXE" ja sitten kokeilu
komennolla "PIXEL3".

Painu nyt kokeilemaan ohjelmaa ja muuntelemaan sit! Laita se tekemn
ruksi, pystyviiva, vaakaviiva, tai vaikka ympyr jos osaat, tai
yhdist se randomin kanssa ja tee nytnsstj! Kokeilemalla tulet
parhaiten sinuiksi uuden asian kanssa. Ja kun olet valmis, siirrymme 
seuraavaan aiheeseen, palettiin.


2.3 Paletti - hrhelhameita ja tanssia?
----------------------------------------

Kuten edellisess luvussa opimme, voi tilassa 13h olla 256 erilaista
vri. Teit ehk jo ohjelman, joka piirt pikselin jokaisella vrill
viivaa ja huomasit, ett kytss olevat vrit ovat huonoja,
puuttelisia, kirkkaita, tummia tai muuten vain inhottavia. Mutta ei
ht - niit voi muuttaa! Ja vaikka paletissa ei mielestsi olisikaan
mitn vikaa haluat ehk tehd sellaisia efektej kuten hivytys,
plasma, "crossfade" (toinen kuva ilmestyy toisen alta pikkuhiljaa)...
Niss kaikissa tarvitaan enemmn tai vhemmn itse tehty palettia ja
siksi meidn pitkin opetella nm asiat ennenkuin menemme pidemmlle.
Kaiken ytimen on VGA ja sen paletti, etenkin sen asettaminen, mutta
ehk mys sen lukeminen. Tss luvussa teemme funktiot, yhden tai
useamman vrin, asettamiseen ja lukemiseen, sek tutustumme
paletinpyritykseen (palette rotation).

Ensin taas vhn teoriaa efektien ja paletin takana. Kuten ehk
tiedtkin, valo voidaan koostaa komponenteista. Tietokoneella
jokaisella vrill on yleens kolme komponenttia: punainen, vihre ja
sininen (red, green, blue). Tt kutsutaan nimell RGB. Itseasiassa
jokainen moodin 13h vri on vain osoite taulukkoon, jonka jokainen
alkio sislt vrin punaisen, virhen ja sinisen komponentin mrn,
eli vahvuuden.

Jos meill olisi puhtaan punainen vri, sen arvot olisivat seuraavat:
r=63, g=0 ja b=0. Sininen taas olisi 0,0 ja 63. Violetti, joka on
sinisen ja punaisen yhdistelm, voisi olla vaikkapa 63,0 ja 63 (eli
tysi mr punaista ja sinist). Jos taas haluaisimme tumman punaisen
vrin, olisivat sen vriarvot vaikka 30, 0, 0. Koska 30 on vhemmn
kuin puolet kirkkaan punaisen puna-arvosta, on tm vri siis yli
puolet tummempi! Helppoa! Ja miksi maksimimr on vain 63? Siksi,
koska VGA:n rekistereiss vrille on varattuna vain 6 bitti, jolla
voidaan esitt numerot vlill 0...63. Tm joudutaan huomioimaan
esim. PCX:n paletin latauksessa, sill siin vrit ovat vlill
0...255. Tss joudutaan jakamaan vriarvot neljll, jotta saadaan
toimiva luku.

Eli ymmrrmme nyt, ett jokaisella vrill on itseasiassa punainen,
vihre ja sininen komponentti, mutta mit siit? Vastaus on helppo,
jos haluamme, voimme muuttaa mit tahansa tilan 0x13 (tai miksei
muunkin tilan) vri helpolla joukolla komentoja. Meidn tarvitsee
vain kirjoittaa asetettavan vrin numero porttiin 3C8h (h lopussa
siis tarkoittaa heksalukua, C:ss 0x3C8) ja sitten porttiin 3C9 ensin
punainen komponentti, sitten vihre komponentti ja lopuksi sininen
komponentti. Tmn jlkeen VGA korottaa vri-indeksi automaattisesti
yhdell, eli jos ensin sytmme porttiin 3C8h vrinumeron 5 ja sitten
punaisen, virhen ja sinisen porttiin 3C9h korottuu VGA:n sisinen
laskuri yhdell, ja voimme halutessamme tunkea heti seuraavan vrin
RGB arvot porttiin 3C9.

Nyt olemme jauhaneet teoriaa tarpeeksi. Menkmme pikkuiseen
esimerkkiin. Esittelemme tietorakenteen RGB, joka sislt vrin
RGB-arvot ja sitten funktion, jolle annetaan parametrin osoitin
tllaiseen rakenteeseen ja vrin numero jolle nm vriarvot
asetetaan. Myhemmin yhdistmme tmn pieneen esimerkkiohjelmaamme,
mutta (PALETTE.H):

typedef struct {
    char red;
    char green;
    char blue;
} RGB;

void setcolor(int index, RGB *newdata) {
    outportb(0x3C8, index);    
    outportb(0x3C9, newdata->red);    
    outportb(0x3C9, newdata->green);
    outportb(0x3C9, newdata->blue);
}

Huomiosi ehk kiinnittyy viel outoon funktioon outportb, jolle
annetaan ensimmisen portin numero ja sitten sinne sytettv
tavu. Funktion kyttmiseksi sisllytt mukaan kirjaston dos.h. 
Ehk sinua kiinnostaisi mys tmn kytt? No olkoon, tehkmme
esimerkkiohjelma kokonaisuudessaan. Kun edellinen pikku koodinptk on
nimell PALETTE.H, voimme helposti sisllytt sen seuraavaan
esimerkkiohjelmaamme kuten ihan tavallisen kirjaston. Muista vain,
ett kirjaston tytyy olla samassa hakemistossa ohjelman kanssa,
muuten ei esimerkki knny. Eli tss sitten itse koodiosa, joka
tuikkaa keskelle ruutua vrin 50. Sitten se odottaa napinpainallusta
ja muuttaa funktiollamme vrin punaiseksi. Huomaa, ett vain alussa
kajotaan nyttmuistiin. Toinen kohta hoidetaan vrinvaihdolla!
Eli (PAL1.C):

#include <conio.h>
#include <sys/farptr.h>
#include <go32.h>
#include <dos.h>

#include "palette.h"

#define putpixel(x, y, c) _farpokeb(_dos_ds, 0xA0000+y*320+x, c)

int main() {
    RGB newcolor;
 
    textmode(0x13);
    putpixel(160, 100, 50);
    getch();
    newcolor.red=63;
    newcolor.green=0;
    newcolor.blue=0;
    setcolor(50, &newcolor);
    getch();
    textmode(0x3);
    return 0;
}

Seuraavana huomionkohteenamme onkin sitten vriarvojen luku, joka on
yht suoraviivaista kuin edellinenkin (tosin tarpeellisuus on 
kyseenalaista, tt ei tarvitse jos on itse asettanut paletin).
Erotuksena on, ett vriarvo kirjoitetaankin porttiin 3C7h ja portista
3C9h _luetaan_ vrin arvo. Jlleen tripletin (kolme alkiota, RGB) luvun
jlkeen indeksi kohoaa, joten voisimme lukea seuraavat vrit. Luku
portista tapahtuu funktiolla inportb(portti). Muuta tietoa emme
tarvitsekaan.

Listkmme nyt kirjastoomme (PALETTE.H) kolme uutta funktiota.
getcolor(int index, RGB *color) lukee vrin <index> vriarvot ja
asettaa ne RGB-rakenteeseen <color>. setpal(char *palette) asettaa
koko paletin kerralla hyvksikytten automaattista indeksin korotusta
(indeksi nollataan aluksi ja sytetn koko data pern, indeksi
korottuu jokaisen rgb-arvon jlkeen). getpal(char *palette) taas lukee
vastaavasti koko paletin. Niiden kytst sitten
esimerkkiohjelmassamme, joka seuraa ajallaan. Eli uutuudet kirjastoon
PALETTE.H:

void getcolor(int index, RGB *color) {
    outportb(0x3C7, index);
    color->red=inportb(0x3C9);
    color->green=inportb(0x3C9);
    color->blue=inportb(0x3C9);
}

void setpal(char *palette) {
    int c;
    
    outportb(0x3C8, 0);
    for(c=0; c<256*3; c++)
	outportb(0x3C9, palette[c]);
}

void getpal(char *palette) {
    int c;
    
    outportb(0x3C7, 0);
    for(c=0; c<256*3; c++)
	palette[c]=inportb(0x3C9);
}

Kuten huomasit, ei viimeisiss funktiossa ole lainkaan en
RGB-rakennetta. Tm siksi, ett koko paletti on huomattavasti
helpompi ksitell nin. Jos olet sit mielt, ett RGB oli parempi
tai haluat muuttaa loputkin pointtereiksi, en sit
est. Char-pointteriversiossa on aina kolme tavua perkkin
ilmoittamassa RGB-tripletti. Toisen vrin r alkaa siis 4. tavusta,
eli indeksist 3. Jos haluat jonkin vrin r-arvon, niin lasket:
"palette[number*3+0]". Vihrell korotat tuota yhdell (number*3+1) ja
sinisen kanssa kahdella. Helppoa tmkin.

Nyt on kaikki trkein katettu VGA:n paletista, joten kysytkin ehk
(aina sin sitten olet kysymss ;) mihin nit nyt sitten voi
kytt. Itseasiassa paletilla on loputtomasti
kyttmahdollisuuksia. Ensimminen on 256-vristen kuvien paletin
asettaminen, sill vrll paletilla kuvat yleens nyttvt enemmn
tai vhemmn sotkulta. Toisena on hivytysefekti, sek feidaus
valkoiseen. Palettiliutuksesta kytetn usein termi feidaus, joka
tarkoittaa, ett palettia liutetaan svy svylt toiseen vriin,
jolloin saadaan vaikka hieno ruudun tummeneminen. Kokeilemmekin sit
ihan kohta, kunhan selitn viel yhden efektin, palettirotaation.

Palettirotaatiossa on paletti, jonka vriarvoja pyritetn
ympri. Eli kytnnss vri, joka ennen oli numerolla 5 onkin
rotaation jlkeen vrinumerossa 6. Tt jatketaan koko ajan, ja vri
matkaa koko paletin lvitse, ja kun se on lopussa niin se siirretn
paletin alkuun. Yleens vri 0 ei kuitenkaan siirret, sill se on
taustavri ja yleens musta. Usein kytetn mys palettia, jossa on
useampia vrej kuin 256, jolloin erona on vain se, ett ainoastaan
osa vreist nkyy ruudulla.

"JA MIHIN TT", kuulen sinun kysyvn. Olet kenties nhnyt plasman,
jonka vrit vaihtuvat koko ajan (kunnon plasmassa on kyll lisksi
mukana muutakin kuin pyriv paletti, mutta pyrityksell saadaan
kummasti liseloa muuten liikkuvaan plasmaan). Tai tunnelin, jossa
vrit siirtyvt kauemmaksi tai lhemmksi. Tllaisia efektej voidaan
helposti toteuttaa palettirotaatiolla. Ennenkuin ymmrrt voit ehk
tarvita pienen demonstraation. Kohta teemmekin esimerkin, joka piirt
vaakatasossa viivoja, jokainen eri vrill alkaen yhdest pttyen
255:teen. Sitten teemme hienon liukupaletin ja alamme pyrittmn
sit. Eli tehkmme viel funktio (listn kirjastoon PALETTE.H):

void rotatepal(int startcolor, int endcolor, char *pal) {
   char r, g, b;
   int c;

   r=pal[startcolor*3+0]; /* tallennamme ensimmiset vrit ja siirrmme */
   g=pal[startcolor*3+1]; /* ne lopuksi loppuun. Tm paletti pyrii siten, */
   b=pal[startcolor*3+2]; /* ett viimeinen vri kulkeutuu kohti alkua */

   for(c=startcolor*3; c<endcolor*3; c++)
       pal[c]=pal[c+3]; /* muista, ett uusi vri on kolmen vlein,
			   sill vlisshn on aina kolme tavua, r,
			   g ja b, joita ei saa sekoittaa, muuten
			   saisimme aikaan vaikkapa sinisen paloauton!
			   (kiinnostava tavoite sinns) */

   pal[endcolor*3+0]=r;
   pal[endcolor*3+1]=g;
   pal[endcolor*3+2]=b;
}

Viel ennen esimerkki tarvitsemme yhden rutiinin, joka tekee
efektistmme edes jotenkin siedettvn. Palettia pit nimittin
vaihtaa ennen kuin ruudulle aletaan piirt, tai muuten voi edess
olla aika huonolaatuinen efekti (normaalipaletissa ei ole mitn
vriliukuja). Varsinkin nin yksinkertaisessa ohjelmassa voi nopealla
nytnohjaimella/koneella nopeus olla liiankin suuri, joten hidastamme
vhn rutiinia odottamalla signaalia, jonka VGA antaa pstessn
ruudun loppuun ja lhtiessn palaamaan ylkulmaan aloittaakseen taas
piirron. Thn teemme funktion, joka odottaa kunnes piirto on valmis
ja kuvaruudulle voi kopioida pelkmtt kesken piirron muutoksia
tehdess aiheutuvia ongelmia. Listkmme seuraava funktio kirjastoon
PALETTE.H:

void waitsync() {
    while( (inportb(0x3DA)&8) != 0);
    while( (inportb(0x3DA)&8) == 0);
}

Nyt sitten hienoon esimerkkiohjelmaamme, joka piirsi niit viivoja ja
pyritti palettia. Huomaa funktio genpal(char *palette), joka asettaa
paletin liukuvreill tehdyksi, sek waitsync()-funktion kytt
(kokeile vaikka ilman waitsync():i, niin net eron)! Eli tss se
olisi (PAL2.C):

#include <conio.h>
#include <sys/farptr.h>
#include <go32.h>
#include <dos.h>

#include "palette.h"

#define putpixel(x, y, c) _farpokeb(_dos_ds, 0xA0000+y*320+x, c)

void genpal(char *palette) {
    char r=0, g=0, b=0;
    int c, color=0;
    
    for(c=0; c<64; c++) { /* MUSTA (0,0,0) - PUNAINEN (63,0,0) */
	palette[color++]=r;
	palette[color++]=g;
	palette[color++]=b;
	if(r<63) r++;
    }
    for(c=0; c<64; c++) { /* PUNAINEN (63,0,0) - VIOLETTI (63,0,63) */
	palette[color++]=r;
	palette[color++]=g;
	palette[color++]=b;
	if(b<63) b++;
    }
    for(c=0; c<64; c++) { /* LILA (63,0,63) - VALKOINEN (63,63,63) */
	palette[color++]=r;
	palette[color++]=g;
	palette[color++]=b;
	if(g<63) g++;
    }
    for(c=0; c<64; c++) { /* VALKOINEN (63, 63, 63) - MUSTA (0,0,0) */
	palette[color++]=r;
	palette[color++]=g;
	palette[color++]=b;
	if(r) r--;
	if(g) g--;
	if(b) b--;
    }
}

int main() {
    int x, y;
    char palette[256*3];

    textmode(0x13);
    genpal(palette);
    setpal(palette);
    for(y=0; y<200; y++) for(x=0; x<320; x++)
	putpixel(x, y, y);
    while(!kbhit()) {
	rotatepal(1, 255, palette);
	waitsync(); /* odotetaan ett piirto on valmis ennen uuden
		       paletin asettamista! */
	setpal(palette);
    }
    getch();
    textmode(0x3);

    return 0;
}

Huomasit varmaan, ett ruudun onnettoman geometrian takia kaikki vrit
EIVT mahtuneet ruudulle. No niin. Ja mits kivaa seuraavaksi?
Seuraavaksi tutustumme viimeiseen palettikikkaan, jonka periaatteen
olet jo voinut keksikin, eli feidauksen.

Genpal-funktio olisi voinut kytt mys erillist rutiinia jolle 
annetaan parametreina monenko vrin matkalla liu'utaan vrist toiseen.
Kuitenkin koska tuo oli yksinkertaisemman nkinen tein sen tuolla
tapaa.

Teemme minimaalisia lisyksi PALETTE.H:hon, sek pikkuisen
esimerkkiohjelman, joka demonstroi efekti kytnnss. Ideahan on
erittin yksinkertainen. Meill on paletti, jossa on sekailaisia
vrej ja haluamme hivytt sen. Miten? No tietenkin muuttamalla
ruudun mustaksi. Miten se tapahtuu? Nollaamme jokaisen vrin, mutta
emme kerralla, vaan vhennmme joka kierroksella ja asetamme uuden
paletin. Tst funktiosta voit tehd helposti muitakin efektej,
kuten feidauksen valkoiseen (korotetaan jokaista vri joka
kierroksella kunnes ollaan vriss 63) tai vaikka paletista toiseen
(jos kohdepaletin vastaava komponentti on suurempi niin korotetaan
arvoa, jos pienempi niin vhennetn). Esittelen tss vain
hivytyksen, mutta lydt kirjastosta PALETTE.H toteutettuna mys
valkoiseen ja toiseen palettiin feidauksen. Voit mys itse tehd
hauskoja efektej, kuten feidata valkoiseen, tehd valkoisen paletin
ja feidata sen mustaan. Kokeile! Mutta, tss rutiinimme:

void fadetoblack(char *palette) {
    char temppal[256*3];
    int c, color;

    memcpy(temppal, palette, 256*3);
    for(c=0; c<63; c++) { /* tarvitsemme maksimissaan 63 muutosta */
	for(color=0; color<256*3; color++)
	    if(temppal[color]) temppal[color]--;
	waitsync();
	setpal(temppal);
    }
}

Sitten yhdistmme efektin lopuksi edelliseen esimerkkiohjelmaamme
lismll sen juuri ennen tekstitilaan vaihtoa:

fadetoblack(palette);

Kokonaisuudessaan ja toimivana, vanhat osat mukana on esimerkkimme
tiedostossa PAL3.C. Siihen on tehty mys pari muuta muutosta, kuten
se, ett aluksi paletti feidataan valkoiseen, asetetaan oikeasti val-
koiseksi (muuten feidatessa mustaan paletti vlht hetken normaalivri-
sen, ttkin SAA kokeilla).

No niin. Pahin tiedonnlksi lienee tlt erlt tyydytetty! Viihdy
esimerkkien parissa ja tee mit vain mieleen juolahtaa niill. Muista,
ett palettifunktiot toimivat mys tekstitilassa. Tmn voit kokeilla
vaikka kyttmll fadetoblack-funktiota. Muista kuitenkin laittaa
loppuun textmode(0x3), vaikket moodia olisi vaihtanutkaan, sill et
vlttmtt pid DOS-kehotteestasi jokainen vri mustana...


3.1 Kaksoispuskuri - luonnonoikku, horoskooppi?
-----------------------------------------------

No niin, olet nemm sulattanut jo kaiken edellisen tiedon. Mainiota!
Tnn psemme (tai miten nyt haluamme asian ilmaista) yhteen
peliohjelmoinnin perustempuista, kaksoispuskuriin. Periaate tmn
takana on aivan naurettavan yksinkertainen, ja itseasiassa min opin
tmn ern lehden lhdekoodia vilkaisemalla (Mikrobitin
grafiikkaohjelmointikurssi, numero 11/95). Eli thn asti olemme
tunkeneet grafiikkaamme suoraan nyttpuskuriin tavu
kerrallaan. Valitettavasti tss on haittoja. Ensimmisen on se, ett
meill on kiire. Nimittin kytss on vain lyhyt aika kun nytt ei
piirret monitorille ja jos siin ajassa ei ehd piirt nytt niin
nytt alkaa vlkkymn, ilmestyy lumisadetta (varsinkin paletinvaihdon
kanssa!) ja muitakin ei-toivottavia ilmiit esiintyy.

Lisksi on todettava valitettava tosiasia: Nyttmuisti on
HIDASTA. Jos haluamme tehd sen kaikkein tehokkaimmin niin kopioimme
kaiken tavaran kerralla nytlle. Eli sen sijaan, ett liskisimme
pikseleit sinne, toisia tnne kopioimme tavaran nytlle nytn
alusta loppuun neljn tavun (kaksoissana) kokoisina palasina. Mutta
miten saamme ruudulle pikseleit sinne tnne, kun kaikki pitisi
kopioida kerralla? Vastaus on, ett kytmme kaksoispuskuria!
Kaksoispuskuri, englanniksi doublebuffer on saman kokoinen kuin
nyttmuisti, mutta sille on varattu tilaa keskusmuistista, joten se
on nopeampaa kuin hidas, kortilla sijaitseva nyttmuisti (nin vain
on, uskokaa pois). Sinne pikselinpiirto tapahtuu huomattavasti
sutjakammin, ja kaiken lisksi meill ei ole mitn kiirett. Vaikka
piirrmme uuden pikselin, ei se ny nytll ennenkuin kaksoispuskuri
on kopioitu, eli flipattu nyttmuistiin.

DJGPP:ll nyttmuisti varataan vaikka malloc-kskyll ja vapautetaan
suorituksen loppuessa free-kskyll. Kokoa pit puskurilla olla
tilassa 13h 64000 tavua. Eroja oikeaan nyttmuistiin
kaksoispuskurissa on DJGPP:ll:

  - Se on nopeampaa.
  - Se sijaitsee omassa muistissa, joten se voidaan taulukoida. Ei
    en putpixel-makroja, vaan dblbuf[y*320+x]=color.
  - Se voidaan kopioida nopealla _dosmemputl-rutiinilla, joka on
    viimeiseen saakka optimoitu (hidas se on siltikin, mutta se on
    nyttkortin ja VGA:n rakenteen vika.)
  - Se ei ny ruudulla ennenkuin ksketn.
  - Se ei vilku.
  - Se silyy muistissa vaikka kytisiin tekstitilassa.
  - Paljon muuta kivaa.

Voit kytt mys dynaamisen muistinvarauksen (malloc tai C++:ssalla
new-operaattori) tilalla taulukkoa, kuten joissakin esimerkeiss on
tehty, tllin kytt muotoa char dblbuf[64000] (tai unsigned
char...). Mallocin kytt on kuitenkin suositeltavampaa kuin tllainen
valtavien taulukoiden ottaminen pinosta.

Muttamutta, tarvitsisimme esimerkin. Mist saamme sellaisen? No tss
pieni esimerkki. Mukana on makro flip(char *buffer), joka kopioi 64000
tavua puskuria nyttmuistiin DJGPP:n _dosmemputl-komennolla, joka
lytyy kirjastosta sys/movedata.h ja tarvitsee mys _dos_ds: ja
siten kirjastoa go32.h. Eli tss tllaista (DOUBLE1.C):

#include <go32.h>
#include <sys/movedata.h>
#include <time.h>
#include <stdlib.h>
#include <conio.h>
#include <dos.h>
#include <stdio.h>

#define flip(c) _dosmemputl(c, 64000/4, 0xA0000)

char *dblbuf;

void varaamuisti() {
    dblbuf=(char *)malloc(64000);
    if(dblbuf==NULL) {
	printf("Ei tarpeeksi muistia kaksoispuskurille!\n");
	exit(1);
    }
}

int main() {
    int x, y;
    
    varaamuisti();
    srand(time(NULL)); /* alustetaan satunnaislukugeneraattori */
    textmode(0x13);
    while(!kbhit()) {
	for(y=0; y<200; y++)
	for(x=0; x<320; x++)
	    dblbuf[y*320+x]=rand()%256;
        flip(dblbuf);
    }
    getch();
    textmode(0x3);
    return 0;
}

Kokeile mys ohjelmaa DOUBLE2.C, joka on toteutettu ilman
kaksoispuskurointia, jos eroa ei viel huomaa, tulee se
joka tapauksessa viel esiin, ja on muitakin hydyllisi asioita miss
kaksoispuskuri, tai kolmoispuskurikin on tarpeen. Mutta, kokeile tmn
kytt ja palaa tmn dokumentin pariin VASTA kun osaat tydellisesti
kaksoispuskurin kytn (oikeammin ymmrrt miten se toimii, miten sit
kytetn, mihin se perustuu ja miten siihen piirretn
pisteit). Sitten syksymmekin uuteen tuntemattomaan. Katsotaan nyt
mihin...


3.2 PCX-kuvien lataus - vain vhn oikaisemalla
-----------------------------------------------

Noniin, kaikki wannabe gamekooderit. Nyt on aika menn vaikeimpaan
aiheeseemme, johon monen kooderin taidot ovat viimein tyssnneet ja
jota minkn en viel tysin ymmrr, enk tied osaanko sit
selitt. 

Se on hyvuskoisuus, sill PCX:n sislt lytyy looginen ja helposti
ymmrrettv rakenne. Ja vaikkei sitkn tysin ymmrr, voi
aina vain kytt samaa rutiinia (kuten min) PCX:n lataamiseen.
Esittelenkin tss kappaleessa lyhyesti tmn yhden yleisimmist
kuvaformaateista olevan tiedostotyypin saloja. 256-vrisen tyypillisen
PCX:n rakenne voidaan jakaa karkeasti neljn (4) osaan:

 - 128 ensimmist tavua headeria sislten info kuvasta
 - kuvadata RLE-pakattuna
 - salaperinen tavu 12
 - palettidata, viimeiset 768 tavua

Ensimmisen ja kaikkein vaikeimpana on headeri, jonka loikimme lhes
kokonaan yli, sill tosipelikooderi tiet lataavansa oikeaa
PCX-kuvaa, joka on oikeaa formaattia oikeankokoiseen puskuriin ja
jtt selittmttmt kaatumiset muiden harteille! Tai itseasiassa en
sit selit kun en siihen ole perehtynyt syvemmin. Kiinnostuneille
PCGPE:ss on tmkin formaatti selitettyn lahjakkaan kryptisesti
englannin kamalalla mongerruksella. Kaikki sit haluavat hankkivat sitten
tiedoston PCGPE10.ZIP, joka sislt kaikkea hydyllist
peliohjelmointiasiaa, englanniksi siis.

Headerista tahdomme tiet vain sen, ett PCX-kuvan koko lasketaan
seuraavasti:

 - Mennn offsettiin 8 (fseek(handle, 8, SEEK_SET)).
 - Luetaan kaksi tavua ja tehdn niist sana (unsigned short int,
   katsomme latauskoodia kohta) ja meill on koko vaakatasossa.
 - Luetaan toiset kaksi tavua ja tehdn niille samoin kuin
   edellisille, nyt meill on y-koko.

Sitten onkin vaikein pala PCX:n rakenteessa. Sit kutsutaan nimell
RLE-koodaus (run length encoding) ja se tarkoittaa sit, ett jos
meill on perkkin 10 pikseli vri 15 emme kirjoitakaan PCX:n
kymment kertaa numeroa 15, vaan kirjoitamme sinne tavun 192+10=202 ja
sen pern tavun 15. Nyt kun PCX-lukija lukee ensimmisen tavun se
katsoo, ett ahaa, nyt tulee toistoa ja toistaa seuraavaa tavua
puskuriin tavu-192 kertaa (202-192=10). Nin me teemmekin
yksinkertaisen pseudorungon:

  - Lue tavu1
  - Jos tavu1 on suurempi kuin 192 niin lue tavu2 ja toista tavua 2
    tavu1-192 kertaa.
  - Jos tavu1 ei ollut suurempi laita puskuriin tavu1.

Nin helppoa, nyt viel paletti. Sekin on helppoa, kunhan muistamme
kaksi seikkaa:

 1) Etsimme paletin tiedoston LOPUSTA pin (fseek(handle, -768, SEEK_END))
 2) Jaamme vrikomponentit neljll, sill PCX:ss vriarvot ovat
    vlilt 0-255, VGA:ssa 0-63 (255/4=63).

Nyt yhdistmme taas kaiken tietomme, ja teemme funktion, joka ottaa
argumenttinaan PCX:n nimen ja puskurin jonne se ladataan. Ohjelma EI
VARAA MUISTIA puskurille, vaan se pit varata etukteen. Voit itse
tehd muutokset ohjelmaan jos haluat. Yleens kuitenkin etukteen on
tiedossa kuvan koko, kun PCX:i kytetn
peleiss. Kuvankatseluohjelmaa tehdess pit kuitenkin koko ottaa
selville jo viimeistn sen vuoksi, ett kuva nytetn oikein, vaikka
puskurissa olisikin tilaa.

Eli tss meill on valmiiksi pureskeltu PCX-lataajan runko, teemme
sille oikein oman kirjaston PCX.H. Kirjasto tarvitsee stdio.h:n
tiedostonksittelyfunktioita ja niiden tietorakenteita:

void loadpcx(char *filename, char *buffer) {
    int xsize, ysize, tavu1, tavu2, position=0;
    FILE *handle=fopen(filename, "rb");

    if(handle==NULL) {
	printf("Virhe PCX-tiedoston avauksessa: Tiedostoa ei lydy!\n");
	exit(1);
    }
    fseek(handle, 8, SEEK_SET);
    xsize=fgetc(handle)+(fgetc(handle)<<8)+1;
    ysize=fgetc(handle)+(fgetc(handle)<<8)+1;
    fseek(handle, 128, SEEK_SET);
    while(position<xsize*ysize) {
	tavu1=fgetc(handle);
	if(tavu1>192) {
	    tavu2=fgetc(handle);
	    for(; tavu1>192; tavu1--)
		buffer[position++]=tavu2;
	} else buffer[position++]=tavu1;
    }
    fclose(handle);
}

void loadpal(char *filename, char *palette) {
    FILE *handle=fopen(filename, "rb");
    int c;

    if(handle==NULL) {
	printf("Virhe PCX-tiedoston palettia luettaessa:"
	       " Tiedostoa ei lydy!\n");
	exit(1);
    }

    fseek(handle,-768,SEEK_END);
    for(c=0; c<256*3; c++)
	paletti[c] =fgetc(handle)/4;
    fclose(handle);
}

Kuten jo varmasti huomasit ovat paletin ja PCX:n latausrutiinit
erillisin. Tm siksi, ett joskus on huomattavasti ktevmp ladata
vain kuva, jos palettia ei mihinkn tarvita. Seuraavaksi seuraa
kappaleen esimerkkiohjelma, joka kytt hyvkseen tutoriaalin
varrella esiteltyj rutiineja ja muodostaa pienen esityksen. Ohjelma
lataa PCX-kuvan PICTURE.PCX ja paletin siit. Sitten se liskisee sen
ruudulle. Lopuksi kuva himmenee tyhjyyteen ja palataan
tekstitilaan. Esimerkki olettaa kuvan olevan kokoa 320x200,
256-vrinen ja paletin sisltv PCX-kuva RLE-pakattuna. Voit korvata
kuvan mill haluat joko muuttamalla lhdekoodia tai kopioimalla oman
kuvasi PICTURE.PCX:n plle.

Huomaa, ett ohjelmassa luodaan kaksoispuskuri, johon kuva
ladataan. Nyttmuistin vnkminen parametriksi aiheuttaa 100%
varmasti kaatumisen, tai jos jotenkin sstyt silt niin ainakaan
mitn ei ilmesty nytlle. Mutta asiaan (PCX1.C):

#include <go32.h>
#include <conio.h>
#include <stdio.h>
#include <sys/movedata.h>
#include <dos.h>

#include "palette.h"
#include "pcx.h"

#define flip(c) _dosmemputl(c, 64000/4, 0xA0000)

int main() {
    char palette[256*3];
    char dblbuf[64000];

    textmode(0x13);
    loadpcx("PICTURE.PCX", dblbuf);
    loadpal("PICTURE.PCX", palette);
    setpal(palette);
    flip(dblbuf);
    getch();
    fadetoblack(palette);
    textmode(0x3);

    return 0;
}

Toivottavasti ymmrsit tst luvusta ainakin kyttperiaatteen. Eli
loadpcx(nimi, puskuri) lataa kuvan puskuriin ja flip(puskuri) laittaa
sen nytlle (jos kuva on kokoa 320x200). Paletti ladataan tarvittaessa
funktiolla loadpal(nimi, palettipuskuri) ja asetetaan aktiiviseksi
komennolla setpal(palettipuskuri). Huomaa, ett esimerkiss asetetaan oikea
paletti ENNEN kuvan laittamista ruudulle. Huomataksesi miksi vaihda
setpal- ja flip-funktioiden paikkaa ja lis vliin getch(), jotta ehdit kat-
sella rauhassa muutosta. Tllaista tss kappaleessa. Mene nyt kokeilemaan
PCX-kuvien latausta. Seuraavassa kappaleessa tutustummekin sitten johonkin
peliohjelmoijaa lhell olevaan asiaan...


4.1 Bitmapit - eikai vain suunnistusta?
---------------------------------------

Tnn siis teemme pienen bitmap-enginen C:ll. Itse olen aiemmin tehnyt
kaikki sprite- ja bitmap -rutiinini C++:ssalla, mutta tll kertaa
kytmme C:t, sill haluan niden esimerkkien toimivan ilman plussiakin.
Eli mit on bitmap?

Bitmap, eli bittikartta on mrtyn kokoinen suorakulmion muotoinen esine,
jolla on puskuri muistissa sislten sen vrit, kuten nyttpuskurinkin
kanssa on. Hydylliseksi bitmapin tekee se, ett laitamme siihen pyyhkimis-
ja piirtotoiminnot, sek liikutustoiminnot, joilla voimme siirrell bitmap-
piamme ympri ruutua. Lisksi teemme siihen vrin, joka tarkoittaa ettei
sit kohtaa bitmapista tarvitse kopioida ruudulle. Nin saamme tehty bit-
mappiimme reiki, eli teemme sen osittain lpinkyvksi. Mutta miten tm
kaikki sitten tehdn? Koko asia on, kuten kaikki asiat ohjelmoinnissa lo-
pulta ovat - naurettavan helppo.

Eli, menkmme takaisin kaksoispuskurin aikoihin. Siin meill on
puskuri, jonka koko on 320x200 pikseli ja se kopioidaan kokonaan nytn
plle. Bittikartassa on muutama selke ero:

 - Se voi alkaa mist tahansa kohdasta ruutua, vaikka koordinaateista
   15, 123.
 - Se voi olla mink kokoinen tahansa (yleens kuitenkin ruutua pienempi).
 - Sen peittm tausta tallennetaan ja palautetaan kun bittikartta
   pyyhitn pois, mik mahdollistaa liikuttelemisen.
 - Siin on lpinkyv vri, meill 0, jota ei piirret ruudulle. Jos siis
   koko bittikartta olisi vri 0, emme nkisi ruudulla mitn!

Eli itseasiassa bittikartta on pari puskuria, joille on varattu tilaa
siten, ett jokainen bittikartan vri voidaan sil
puskuriin. Puskureita on perusbittikartassa kaksi, eli itse kuvan
sisltv kartta, joka on jrjestelty aivan samoin kuin
esim. kaksoispuskuri, mutta koko on bittikartan mukainen. Toinen on
taustapuskuri, joka on muuten sama, mutta sinne vain siltn
piirrettess alle jneet pikselit, jotta ne voidaan bittikarttaa
ruudulta pyyhkiess palauttaa sielt.

Eli tllainen voisi olla 3x3 kokoinen bittikartta:

Bittikartta:      Taustapuskuri (mit bittikartan alle on
                  piirrettess jnyt):
30 20 19          0  0  0
19 23 42          0  0  0
12 32 43          0  0  0

Kuten huomaatte bittikartta on piirretty mustalle pohjalle, sill
taustapuskuri eli se mit bittikartan alle ji on tynn mustaa, eli
vri 0. Bittikartta on kaikkein helpointa mritell omaan
datarakenteeseensa, joka sislt tarvittavat tiedot kartan piirtelyyn
ja pyyhkimiseeen, nimetn se vaikka structiksi BITMAP.

Koordinaattien mrittely saavutetaan siten, ett meill on rakenteessamme
X-ja Y-koordinaatit, joista piirto kaksoispuskuriin aloitetaan. Koko
taas on helpompi. Jos kaksoispuskurin koko oli 320x200, niin kaava
oikean pikselin hakemiseksi oli y*320+x. Jos meill on bitmap kokoa
ysize * xsize, niin oikea koordinaatti on y*xsize+x. Piirrettess
loopataan X: ja Y:t siten, ett luemme yksi kerrallaan pikselin
bittikartasta, ja jos se on jokin muu kuin vri 0 (yleens musta, tm
oli siis lpinkyvksi sovittu vri), otamme ensin sen alle jvn
pikselin talteen taustapuskuriin ja laitamme sitten vasta bittikartan
vrin ruudulle oikeaan kohtaan (bittikartan vrit sisltvst
puskurista).

Eli tarvittavat tiedot bittikarttarakenteeseen ovat:

 - bittikartan vrit (char * -pointteri)
 - taustan vrit (char * -pointteri)
 - x-sijainti ruudulla (int)
 - y-sijainti ruudulla (int)
 - koko x-suunnassa (int)
 - koko y-suunnassa (int)

Lisksi meill on xspeed ja yspeed, joita kytetn esimerkeiss
silmn bittikartan liikenopeutta x- ja y-suunnassa. Nill
tempuilla meill on nyt teoria liikuteltavan bitmapin tekemiseksi.
Ensin mrittelemme rakenteen, joka sislt kaiken tarvittavan tiedon
bittikartastamme (BITMAP.H):

typedef struct {
    char *bitmap;
    char *background;
    int x;
    int y;
    int xsize;
    int ysize;
    int xspeed;
    int yspeed;
} BITMAP;

Sitten tehtvnmme on tehd "interface", eli kyttliittym
bitmap-engineemme. Siihen sisllytmme seuraavat funktiot:

  - bdraw(BITMAP *b) piirt bittikartan kohtaan BITMAP.x, BITMAP.y

  - bhide(BITMAP *b) tyhjent edellisell piirtokerralla piirretyn bitti-
    kartan. Huomaa, ett JOKAISEN PIIRRON JLKEEN ON TULTAVA TYHJENNYS
    ja ett BITTIKARTTAA EI LIIKUTETA SEN OLLESSA RUUDULLA (todellisuudessa
    tietenkin kaksoispuskurissa, joka kopioidaan ruudulle kun kaikki bitti-
    kartat ovat nkyviss, sanoinhan, ett hydymme viel siit!)

  - bmove(BITMAP *b) lis X-koordinaattiin muuttujan BITMAP.xspeed ja
    Y-koordinaattiin vastaavasti muuttujan BITMAP.yspeed.

  - bsetlocation(BITMAP *b, int x, int y) asettaa uudet X- ja
    Y-koordinaatit.

  - bsetspeed(BITMAP *b, int xspeed, int yspeed) asettaa uudet X- ja
    Y-nopeudet. Huomaa, ett liike yls saavutetaan negatiivisella
    Y-nopeudella ja vastaavasti liike vasemmalle negatiivisell
    X-nopeudella.

  - bload(BITMAP *b, int x, int y, int xspeed, int yspeed, int xsize,
    int ysize, char *bitmapbuffer, int bufferx, int buffery, 
    int bufferxs), jossa 8. parametrist lhtien kertoo
    latauspuskurista, jona tulemme kyttmn 320x200 kokoista PCX, kuvaa,
    sislten kaikki bitmapit mit pit ladata. Jos kuvan x-koko ja y-koko,
    sek aloituskoordinaatit kuvassa on ilmoitettu oikein, onnistuu lataus
    suorakulmion muotoiselta alueelta tysin onnistuneesti, eik lataus-
    rutiinin kytt vaadi kovin paljoa miettimist. Lis kytst ajal-
    laan tulevassa esimerkiss.

No niin. Lhtekmme tekemn kirjastoamme BITMAP.H yksi funktio kerrallaan.
Rakenne BITMAP on jo esitelty, joten alkakaamme kermn sen pern
ksittelyfunktioita. Ensimmisenhn oli vuorossa bdraw(), joka onkin
helpoimpia ja trkeimpi funktioita. Katsellaanpas esimerkkikoodia:

void bdraw(BITMAP *b) {
    int y=b->y,
	x=b->x,
	yy, xx;

    /* Eli loopataan koko suorakulman kokoinen alue. bitmap- ja
       ja background -puskureissahan lasketaan sijainti seuraavasti:
       y * b->xsize + x. */

    for(yy=0; yy<b->ysize; yy++) { 
    for(xx=0; xx<b->xsize; xx++) {

	/* eli vrill 0 tm vertailu alla ei ole tosi, joten vrill
	   0 merkittyj kohtia EI piirret! */

	if(b->bitmap[yy*b->xsize+xx]) {

	    /* doublebuffer muuttuja osoittaa kaksoispuskuriin. Huomaa, ett
	       ylkulma on y*320+x, mutta koska haluamme viel piirt useita
	       rivej, lismme yy-looppimme y-arvoon, kutenn mys xx-looppi
	       x-arvoon. Jos et ymmrtnyt niin poista vliaikaisesti kohdat
	       ja net mit tapahtuu */

	    b->background[yy*b->xsize+xx]=
		doublebuffer[ (y+yy) * 320 + (x+xx) ];


	    /* sitten vain asetetaan bittikartasta oikea kohta ruudulle,
	       alle peittyv osa on jo tallessa puskurin background vastaa-
	       valla kohdalla. */

	    doublebuffer[ (y+yy) * 320 + (x+xx) ]=
		b->bitmap[yy*b->xsize+xx];
	}
    }
    }
}

Koska joiltakin on esiintynyt valituksia siit, ett koodi j hmrn
peittoon, niin esittelen tss saman pseudona, jos se olisi hieman
selvemp:

funktio bdraw
    kokonaisluvun kokoiset kierroslaskurit a ja b

    looppaa a vlill 0 - <y-koko>
        looppaa b vlill 0 - <x-koko>

            bittikarttasijainti = a * <x-koko> + b
            ruutusijainti = ( <y-sijainti> + a ) * 320 + b + <x-sijainti>

            jos bittikartta(bittikarttasijainti) ei ole 0 niin

                tausta(bittikarttasijainti) = kaksois(ruutusijainti)
                kaksois(ruutusijainti) = bittikartta(bittikarttasijainti)

            end jos

        end looppi b
    end looppi a

end funktio

Kun lhdet korvaamaan a:n muuttujalla yy ja b:n muuttujalla xx ja
korvaat bittikartan sisiset muuttujat <y-koko>, <x-koko>,
<y-sijainti> ja <x-sijainti> BITMAP-rakenteen muuttujilla b->ysize,
b->xsize, b->y ja b->x sek tausta:n ja bittikartan:n
b->background:illa ja b->bitmap:illa, kaksois-muuttujan
kaksoispuskurisi nimell niin olet aikalailla ensimmisess,
alkuperisess sorsassa. Jos yhtn selvent niin voit poistaa
kommentit alkuperisest sorsasta kokonaan ja siirt sijainnin laskut
sielt []-sulkeiden sisst juuri tuollaisiin
bittikarttasijainti-tyylisiin apumuuttujiin, jolloin koodi selvenee
hieman. Olkoot, tss se on:

void bdraw(BITMAP *b) {
    int a, b, bitmapsijainti, ruutusijainti;

    for(a=0; a < b->ysize; a++) {
        for(b=0; b < b->xsize; b++) {
            bitmapsijainti=a * b->xsize + b;
            ruutusijainti = ( b->y + a ) * 320 + b + b->x;

            if(b->bitmap[bitmapsijainti] != 0) {

                b->background[bitmapsijainti] = doublebuffer[ruutusijainti];
                doublebuffer[ruutusijainti] = b->bitmap[bitmapsijainti];

            }
        }
    }
}

Varaa aikaa edellisten tutkimiseen, sill on trke, ett ymmrrt periaat-
teen. Tietenkin saat lisselvyytt kokeilemalla muuttaa noita kohtia, jol-
loin net muutoksen kntmll uudelleen esimerkkiohjelman, jonka
myhemmin esittelemme ja ajamalla muunnellun version. Seuraavana onkin
huomattavasti nopeammin tehty pyyhintfunktio, joka eroaa vain siten, ett
sen sijaan, ett silisimme taustan ja korvaisimme ruudun pikselin
bitmap-puskurin arvolla laitammekin background-puskuriin tallennetun pikse-
lin takaisin kaksoispuskuriin, joka on piilotusfunktion jlkeen samassa
kunnossa kuin ennen piirtoakin!

void bhide(BITMAP *b) {
    int y=b->y,
	x=b->x,
	yy, xx;

    /* Eli loopataan koko suorakulman kokoinen alue. bitmap- ja
       ja background -puskureissahan lasketaan sijainti seuraavasti:
       y * b->xsize + x. */

    for(yy=0; yy<b->ysize; yy++) { 
    for(xx=0; xx<b->xsize; xx++) {

	/* eli vrill 0 tm vertailu alla ei ole tosi, joten vrill
	   0 merkittyj kohtia EI piirret! */

	if(b->bitmap[yy*b->xsize+xx]) {
	    doublebuffer[ (y+yy) * 320 + (x+xx) ]=
		b->background[yy*b->xsize+xx];
	}
    }
    }
}

Tuohon ette varmaan en pseudoja tarvitse, koska sehn eroaa
edellisest vain tuon sijoituksen osalta, eli ensimminen sijoitus
draw-funktiosta knnetn vain toisinpin, niin alkup. tausta
palautuu.

Seuraavaksi kolme helponta funktiota heti riviss, sill niiden toteuttami-
nen on helppoa ja ymmrtminen viel helpompaa, muista, ett X-ja Y-koor-
dinaatteja vhennetn negatiivisill nopeuksilla, sill X+(-1)=X-1:

void bmove(BITMAP *b) {
    b->x+=b->xspeed;
    b->y+=b->yspeed;
}

void bsetlocation(BITMAP *b, int x, int y) {
    b->x=x;
    b->y=y;
}

void bsetspeed(BITMAP *b, int xspeed, int yspeed) {
    b->xspeed=xspeed;
    b->yspeed=yspeed;
}

Seuraava onkin vaikea pala, joten lisn koodia saadakseni siit vhn
selvemmksi. Idea siis on, ett otamme pikselin tuplapuskuriin ladatus-
ta ja laitamme sen bitmap-puskuriin. Eli oikeastaan knteisesti nyt-
tfunktioon nhden. Eli katsotaanpas:

void bload(BITMAP *b, int x, int y, int xspeed, int yspeed, int xsize,
    int ysize, char *bitmapbuffer, int bufferx, int buffery, 
    int bufferxs) {
    int yy, xx;

    bsetlocation(b, x, y);
    bsetspeed(b, xspeed, yspeed);
    b->xsize=xsize;
    b->ysize=ysize;

    b->bitmap=(char *)malloc(xsize*ysize);
    b->background=(char *)malloc(xsize*ysize);
    if(b->background==NULL || b->background==NULL) {
	printf("Ei tarpeeksi muistia bitmap-puskureille!\n");
	exit(1);
    }
    
    /* Eli loopataan koko suorakulman kokoinen alue. bitmap-
       puskurissahan lasketaan sijainti seuraavasti:
       y * b->xsize + x. */

    for(yy=0; yy<ysize; yy++) { 
    for(xx=0; xx<xsize; xx++) {

	/* doublebuffer muuttuja osoittaa kaksoispuskuriin. Huomaa, ett
	   ylkulma on y*320+x, mutta koska haluamme viel piirt useita
	   rivej, lismme yy-looppimme y-arvoon, kutenn mys xx-looppi
	   x-arvoon. Jos et ymmrtnyt niin poista vliaikaisesti kohdat
	   ja net mit tapahtuu */

	b->bitmap[yy*xsize+xx]=
	    bitmapbuffer[ (buffery+yy) * bufferxs + (bufferx+xx) ];
    }
    }
}

bload on itseasassa tysin sama kuin ensimminenkin funktio, mutta
alussa meill on pari alustusta jotta BITMAP-rakenne saadaan halutuksi
(muistinvarausta, sijainnin nollausta, koon alustus...). Vain
piirtofunktio on korvattu versiolla, joka ei piirr ruudulle, vaan
lataa ruudulta (bitmapbuffer tss tapauksessa, jottei tarvi oikeaa
kaksoispuskuria vlttmtt kytt) pikselit. Ei se loppujenlopuksi
ole sen vaikeampi.

Nyt kun lismme kaikki yhteen kirjastoomme BITMAP.H ja teemme lopuksi
viel pienen esimerkkiohjelman, joka liikuttelee palloa
ruudulla. Koska kirjastomme ei kykene estmn ruudun yli menemisi,
niin meidn pit knt liikkuvan pallon suuntaa ennenkuin alareuna
osuu ruudun alareunaan ja menee sitten siit yli (eli jos bittikartan
koko, sijainti ja nopeus yhteenlaskettuna on yli ruudun koon, tai
bittikartan sijainti ja nopeus yhteenlaskettuna on pienempi kuin
0). Eli kun jompikumpi edellisist ehdoista tyttyy niin knnetn
pallon suuntaa ja saadaan pallo "pomppimaan" reunoista.

Mutta, olemme taas puhuneet ihan tarpeeksi. Menkmme nyt esimerkkiohjel-
mamme pariin (BITMAP1.C). Siin lataamme bittikartan tiedostosta BITMAP.PCX
ja tausta tiedostosta BITBACK.PCX. Nin nemme lpinkyvyyden toiminnassa
(muutenhan pallo olisi nelinmuotoinen). Lisksi tietenkin kytmme jo va-
kioiksi muuttuneita palettifunktiota ohjelmamme koristukseksi:

#include <go32.h>
#include <sys/movedata.h>
#include <conio.h>
#include <stdio.h>
#include <dos.h>
#include <stdlib.h>

char *doublebuffer;

#include "palette.h"
#include "pcx.h"
#include "bitmap.h"

#define flip(c) _dosmemputl(c, 64000/4, 0xA0000)

int main() {
    char palette[768];
    BITMAP bitmap;

    doublebuffer=(char *)malloc(64000);
    if(doublebuffer==NULL) {
	printf("Ei tarpeeksi muistia kaksoipuskurin varaukseen!\n");
	return 1;
    }
    textmode(0x13);
    loadpcx("BITMAP.PCX", doublebuffer);
    loadpal("BITMAP.PCX", palette);
    setpal(palette);
    bload(&bitmap, 160, 100, 1, 1, 16, 16, doublebuffer, 1, 1, 320);
    loadpcx("BITBACK.PCX", doublebuffer);
    /* Lataus vasta kun bittikartta on otettu edellisest tiedostosta.
       Ei ladata palettia koska se on sama kuin edellisess PCX:ss. */

    while(!kbhit()) {
	bdraw(&bitmap);
	waitsync();
	flip(doublebuffer);
	bhide(&bitmap);
	bmove(&bitmap);
	if((bitmap.x+bitmap.xsize+bitmap.xspeed)>320 ||
	   bitmap.x+bitmap.xspeed<0)
	    bitmap.xspeed= -bitmap.xspeed;
	if((bitmap.y+bitmap.ysize+bitmap.yspeed)>200 ||
	   bitmap.y+bitmap.yspeed<0)
	    bitmap.yspeed= -bitmap.yspeed;
    }
    getch();
    fadetoblack(palette);
    textmode(0x3);

    return 0;
}

Varaa kunnolla aikaa ja tutki lhdekoodeja, mieti teoriaa ja kokeile kaikkea
kytnnss mit mieleen tulee. Kun luulet keksineesi idean niin palaa
takaisin dokumentin reen, ja siirrymme seuraavaan aiheesemme. Menehn
siit! Jos vielkin tuntui silt ettet tajunnut niin ota yhteytt ja
kysy mik ji mietityttmn, niin tarkennan sitten viel tt.


4.2 Animaatiot
--------------

Tmnkertainen aiheemme on pieni parannus koodiin, joka on paljon ny-
tll ja jonka jlkeen on tmn tutoriaalin bittikarttarutiinit lhes k-
sitelty. Tulemme kyll hyvksikyttmn edellisen kappaleen koodia
tehdessmme fonttiengine, sek parantelemme koodia tehdessmme trmys-
tarkistuksen, mutta itse animointi- ja bittikarttateoria ksitelln
kokonaan tss ja edellisess kappaleessa.

Eli tnn tutustumme ensimmisen animaatiohin. Mit animaatiot sitten
ovat? No itseasiasas animaatio on vain sarja kuvia, joita vaihdellaan
ja saadaan kuva liikkeest. Animaatiota voidaan kytt lhes kaikkeen
peliss. Sill voidaan tehd pyriv alusanimaatio, jonka jokainen
kuva on yksi aluksen suunta. Jokaisella suunnalla voisi olla viel oma
animaationsa, joka saa vaikka rakettimoottorit hehkumaan ja laserit
aiheuttamaan vlhdyksi aluksen pinnassa. Pienell mielikuvituksella
ja taitavalla graafikolla pstn ihmeisiin. Tss kappaleessa esi-
telty kirjasto ei varmaankaan ky suoraan moneen tarkoitukseen tai ole
tarpeeksi nopea peliin, mutta enginen onkin vain tarkoitus nytt
pperiaatteita animoinnin ja muiden olennaisien asioiden takana.

Eli animaatio on kuvasarja, jotka nytetn tietyss jrjestyksess. Miten
sitten toteutamme tmn. Tss on tapa jolla min olen sen tehnyt. Meillhn
on tysin toimivat rutiinit yhden kuvan nyttmiseen. Tehkmme vain
animointikoodi, joka vaihtaa pointterin bitmap osoittamaan seuraavaan
kuvaa, eli frameen. Tt tytyy kutsua silloin kun sprite, joksi kutsumme
animoivaa bittikarttaamme tstlhin ei ole piirretty puskuriin. Jlleen
voit kokeilla siirt animointikoodin kutsun kohtaan jossa esine on piir-
rettyn, mutta se ei tule nyttmn hyvlt (jos objektin peittmn alueen
muoto muuttuu). Eli siis tarvitsemme uuden rakenteen, joka voi sil
useita kuvia, koodin joka vaihtaa bitmap-pointterin osoittamaan seuraavaan
kuvaan, laskurin joka kertoo monennessako kuvassa mennn ja toisen muuttu-
jan joka kertoo montako kuvaa meill on animaatiossa, sek lopulta uuden
latausfunktion, joka osaa ladata useita kuvia ksittvn animaation.

Thn kaikkeen voimme kopioida vanhaa koodiamme ja lisill sinne tar-
peellisia osia. Eli teemme nyt uuden rakenteen, jossa voi olla maksimis-
saan MAXFRAME mr frameja, eli kuvia (tm toteutuksen helpottamiseksi):

#define MAXFRAME 64

typedef struct {
    char *frame[MAXFRAME];
    int curfrm;
    int frames;

    char *bitmap;
    char *background;
    int x;
    int y;
    int xsize;
    int ysize;
    int xspeed;
    int yspeed;
} SPRITE;

Se olikin helppoa. Nm rutiinit tulevat kirjastoon SPRITE.H, josta lydt
mys joukon vanhoja tuttujamme uudelleennimettyn ja vhn
muunneltuina (sdraw, shide...). Seuraavaksi sitten animointirutiini:

void sanimate(SPRITE *s) {
    s->curfrm++;
    if(s->curfrm >= s->frames)
	s->curfrm=0;
    s->bitmap=s->frame[s->curfrm];
}

Radikaaleja muutoksia tarvinnee mys latausrutiinimme. Trkeimmt muutok-
set siin on, ett se lukee framet rivist. Katso SPRITE.PCX esimerkkin
tllaisesta animaatiosta. Jos ihmettelet outoja kertolaskuja joissain
kohdin se johtuu siit, ett jokaisen framen jlkeen hyptn 1 pikseli
yli, sill teemme rajat animaatioiden vliin selvennykseksi. Eli tss
olisi latauskoodimme, uusi parametri on animaatioiden mr:

void sload(SPRITE *s, int x, int y, int xspeed, int yspeed, int xsize,
    int ysize, char *bitmapbuffer, int bufferx, int buffery, 
    int bufferxs, int frames) {
    int yy, xx, current;

    ssetlocation(s, x, y);
    ssetspeed(s, xspeed, yspeed);
    s->xsize=xsize;
    s->ysize=ysize;
    s->curfrm=0;
    s->frames=frames;

    for(current=0; current<frames; current++) {
	s->frame[current]=(char *)malloc(xsize*ysize);
        if(s->frame[current]==NULL) {
            printf("Ei tarpeeksi muistia sprite-puskureille!\n");
            exit(1);
        }
    }
    s->background=(char *)malloc(xsize*ysize);
    s->bitmap=s->frame[s->curfrm];

    if(s->background==NULL) {
	printf("Ei tarpeeksi muistia sprite-puskureille!\n");
	exit(1);
    }
    
    /* Eli loopataan koko suorakulman kokoinen alue. bitmap-
       puskurissahan lasketaan sijainti seuraavasti:
       y * s->xsize + x. Uloimpana looppina on uutena framelooppi,
       joka on listty koska meidn pit ladata usea kuva. */
    for(current=0; current<frames; current++)
    for(yy=0; yy<ysize; yy++) { 
    for(xx=0; xx<xsize; xx++) {
	/* doublebuffer muuttuja osoittaa kaksoispuskuriin. Huomaa, ett
	   ylkulma on y*320+x, mutta koska haluamme viel piirt useita
	   rivej, lismme yy-looppimme y-arvoon, kutenn mys xx-looppi
	   x-arvoon. Jos et ymmrtnyt niin poista vliaikaisesti kohdat
	   ja net mit tapahtuu */
	s->frame[current][yy*xsize+xx]=
	    bitmapbuffer[ (buffery+yy) * bufferxs + (bufferx+xx) +
			  (xsize+1)*current ];
    }
    }
}

Kirjastoon SPRITE.H listn viel bdraw, bhide, bmove, bsetlocation ja
bsetspeed nimettyn nimill sdraw, shide, smove, ssetlocation ja ssetspeed
funktioiden erottamiseksi bitmap-rutiineista (jos vaikka halutaan kytt
molempia). Muitakin pikkumuutoksia on tehty. Huomaat ne helposti
kurkkaamalla kirjaston sisn. Nyt meill onkin animaatiot taitava engine,
jota meidn tytyy tietenkin heti kokeilla. Tss on esimerkkiohjelmamme
SPRITE1.C, joka havainnoi funktioiden kytt:

#include <go32.h>
#include <sys/movedata.h>
#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <dos.h>

char *doublebuffer;

#include "palette.h"
#include "pcx.h"
#include "sprite.h"

#define flip(c) _dosmemputl(c, 64000/4, 0xA0000)

int main() {
    char palette[768];
    SPRITE sprite;

    doublebuffer=(char *)malloc(64000);
    if(doublebuffer==NULL) {
	printf("Ei tarpeeksi muistia kaksoipuskurin varaukseen!\n");
	return 1;
    }
    textmode(0x13);
    loadpcx("SPRITE.PCX", doublebuffer);
    loadpal("SPRITE.PCX", palette);
    setpal(palette);
    sload(&sprite, 160, 100, 1, 1, 16, 16, doublebuffer, 1, 1, 320, 8);
    loadpcx("BITBACK.PCX", doublebuffer);
    /* Lataus vasta kun bittikartta on otettu edellisest tiedostosta.
       Ei ladata palettia koska se on sama kuin edellisess PCX:ss. */

    while(!kbhit()) {
	sdraw(&sprite);
	waitsync();
	waitsync();
	flip(doublebuffer);
	shide(&sprite);
	smove(&sprite);
	sanimate(&sprite);
	if((sprite.x+sprite.xsize+sprite.xspeed)>320 ||
	   sprite.x+sprite.xspeed<0)
	    sprite.xspeed= -sprite.xspeed;
	if((sprite.y+sprite.ysize+sprite.yspeed)>200 ||
	   sprite.y+sprite.yspeed<0)
	    sprite.yspeed= -sprite.yspeed;
    }
    getch();
    fadetoblack(palette);
    textmode(0x3);

    return 0;
}

Luultavasti huomaat nykimist, sill tysin optimoimaton sprite-enginemme
ei aivan pysty 70 frameen sekunnissa. Siksi laitoin ohjelmamme odottamaan
kahta vertical retracea, jotta nykiminen ei olisi niin hiritsev
(P75:llni kahdella waitilla meno nytt paljon tasaisemmalta, eik yhden
framen hyppy ny lheskn niin selvsti). Jos kuitenkin sinulla on hidas
kone niin poista toinen tai kummatkin odotuksista, se nopeuttaa koodia
paljon, mutta voit joutua laittamaan delay-komennolla viivett stksesi
pyrimist tasaisemmaksi. Pienell optimoinnilla olisimme toki saaneet
moninkertaisesti lis nopeutta, mutta koodi olisi menettnyt luettavuut-
taan, joka esimerkkiohjelmien tarkoitus on. Tietenkin kun alat tekemn
omaa pelisi teet uudet ja paremmin tarkoitukseesi sopivat rutiinit ke-
rmiesi tietojen pohjalta.

Nyt onkin tmn kappaleen aika loppua ja sinun on aika paneutua uuden
asian pariin. Seuraavassa luvussamme ksitellnkin sitten viimeist
kysymyst spritejen parissa, monen spriten kytt, niiden trmyksi
ja ylitseliukumisia. Mutta nyt jtn sinut rauhaan. Nemme seuraavassa
luvussa!


4.3 Pitk spriten trmt? Ent coca-colan?
---------------------------------------------

Nyt psemmekin vihoviimeiseen vaiheeseen teoriassamme ja ryyditmme sit
pienin, tai ehk niinkn pienin muutoksin SPRITE.H-kirjastoomme. Nimit-
tin jokainen vhnkn vakavasti pelintekoa harkinnut tarvitsee useampia
kuin yhden spriten. Mutta mit tapahtuu kun ne ovat menossa pllekin?
Jos teet vain loopin, joka piirt spriten ja toisen, joka pyyhkii ne
samassa jrjestyksess olet varmaan huomannut, ett se ei aiheuta toivot-
tuja tuloksia. Muutos mit tarvitaan on pieni ja yksinkertainen, mutta
ajatellaanpas esimerkkimme.

Ajatellaan, ett sinulla on kolme pikseli. Punainen, sininen ja keltainen.
Haluat laittaa ne samaan kohtaan ruudulle. Laitat ne edell olevassa
jrjestyksess mustalle ruudulle ja laitat lapulle muistiin punaisen koh-
dalle, ett sen alla oli musta, sinisen kohdalle, ett sen alla oli
punainen ja keltaisen kohdalle, ett sen alla oli sininen.

Nyt haluat poistaa ne. Ottaisitko ne nyt samassa jrjestyksess, eli ensin
punainen, sitten sininen ja lopuksi keltainen? Et, sill jos ottaisit lopuksi
keltaisen, katsoisit lapustasi sen alla olleen sinisen vrin ja ruutu
muuttuisikin siniseksi. Tss meidn tytyykin menn knteisesti, eli
keltainen, sininen ja sitten vasta punainen, jonka tilalle laitat lopulta
mustan ja kaikki on hyvin.

Eli jos sinulla olisi 10 bittikarttaa taulukossa SPRITE s[10], niin niiden
piirto ja pyyhkiminen tapahtuisi seuraavasti:

for(c=0; c<10; c++) sdraw(s[c]);
flip(doublebuffer);
for(c=10; c>=0; c--) shide(s[c]);

Ja ei en toimimattomia koodinptki, vaan hienosti toistensa ylitse
liukuvat spritet.

Mutta aina ei haluta kaikkien vain liukuvan toistensa ylitse. Milt
nyttisi matopeli, jossa madot kiltisti liukuvat toistensa ylitse?
Ei kovin oikealta, sanoisin. Meidn tytyy siis tehd rutiini, joka
tarkistaa trmyksen kahden spriten vlill. Olkoon sen kutsutapa
seuraava: scollision(SPRITE *a, SPRITE *b) ja se palauttaa arvon
1 jos trmys on tapahtunut, muuten se palauttaa nollan. Jos siis
haluat tehd trmyksen tultua jotakin, niin koodi menisi suurinpiirtein
nin:

if(scollision(sprite[0], sprite[1]))
    tee_jotain_kun_tulee_pamahdus();

Mutta, miten toimii tm salaperinen funktiomme? Itseasiassa min en
saanut siit mitn selv luettuani sen aikoinani Mikrobitin grafiikka-
ohjelmointikurssin toisesta osasta, mutta luulisin nyt pystyvni teke-
mn samanlaisen, ja jos onnistumme pystynen selittmnkin toimintaperi-
aatteen.

int scollision(SPRITE *a, SPRITE *b) {
    /* Lasketaan spritejen ylkulmien vliset etisyydet. Huomaa, ett tss
       lasketaan mukaan nopeudet, eli palautusarvo 1 kertoo spritejen
       trmvn ENSI vuorolla. Nin ehditn pllekkin meneminen est
       ajoissa. */
    int xdistance= (a->x+a->xspeed) - (b->x+b->xspeed);
    int ydistance= (a->y+a->yspeed) - (b->y+b->yspeed);
    int xx, yy;
    /* Jos x- tai y-etisyys on suurempi kuin suuremman leveys eivt
       spritet voi mitenkn olla toistensa pll. */
    if(xdistance>a->xsize && xdistance>b->xsize) return 0;
    if(ydistance>a->ysize && ydistance>b->ysize) return 0;

    for(xx=0; xx< a->xsize; xx++)
    for(yy=0; yy< a->ysize; yy++)
	if(xx+xdistance < b->xsize && xx+xdistance>=0 &&
	   yy+ydistance < b->ysize && yy+ydistance>=0)
	if(a->bitmap[ yy * a->xsize + xx ] &&
	   b->bitmap[ (yy+ydistance) * b->xsize + (xx+xdistance) ])
	    return 1;
    return 0;
}

Loopissa ideana on se, ett laskuilla saadaan b-spriten vastaava koordinaatti
selville ja jos se on siis positiivinen ja spriten b rajoissa (pienempi
kuin leveys tai y-koordinaatin ollessa kyseess korkeus). Tarkemmin en
ala selittmn. Jos vlttmtt haluat saada selville miten ptk toimii
niin piirr pari tilannetta paperilla ja katso miten niiden kanssa tapah-
tuu. Nyt meill onkin ksiteltyn kaikki trkein spriteist ja voimme
menn viimeiseen pelkstn spritej kyttvn ohjelmaamme. Tm ohjelma
on pienimuotoinen peli, jossa liikutaan edellisen esimerkin palikoilla. Pe-
laajia on 2 ja tarkoitus on leikki hippaa. Eli toinen yritt pakoon ja
toinen yritt ottaa kiinni. Peli loppuu kun pelaajat trmvt. Kontrol-
lit ovat pelaajalla 1 wsad ja pelaajalla 2 ujhk. Tm on vain pieni esi-
merkki siit mit nill taidoilla voisi tehd. Lisksi nappeina on
+ ja - nopeuden stn (nyt ei odoteta waitsyncill) sek ESC lopetuk-
seen kesken. Eli SPRITE2.C:

#include <go32.h>
#include <sys/movedata.h>
#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <dos.h>

char *doublebuffer;

#include "palette.h"
#include "pcx.h"
#include "sprite.h"

#define flip(c) _dosmemputl(c, 64000/4, 0xA0000)

int main() {
    char palette[768];
    SPRITE pl1, pl2;
    int quit=0, waittime=0;

    doublebuffer=(char *)malloc(64000);
    if(doublebuffer==NULL) {
	printf("Ei tarpeeksi muistia kaksoipuskurin varaukseen!\n");
	return 1;
    }
    textmode(0x13);
    loadpcx("SPRITE.PCX", doublebuffer);
    loadpal("SPRITE.PCX", palette);
    setpal(palette);
    sload(&pl1, 100, 100, 0, 0, 16, 16, doublebuffer, 1, 1, 320, 8);
    sload(&pl2, 220, 100, 0, 0, 16, 16, doublebuffer, 1, 1, 320, 8);
    loadpcx("BITBACK.PCX", doublebuffer);

    while(!quit) {
	sdraw(&pl1);
	sdraw(&pl2);
	flip(doublebuffer);
	shide(&pl1);
	shide(&pl2);
	smove(&pl2);
	smove(&pl1);
	sanimate(&pl1);
	sanimate(&pl2);
	if((pl1.x+pl1.xsize+pl1.xspeed)>320 ||
	   pl1.x+pl1.xspeed<0)
	    pl1.xspeed= -pl1.xspeed;
	if((pl1.y+pl1.ysize+pl1.yspeed)>200 ||
	   pl1.y+pl1.yspeed<0)
	    pl1.yspeed= -pl1.yspeed;

	if((pl2.x+pl2.xsize+pl2.xspeed)>320 ||
	   pl2.x+pl2.xspeed<0)
	    pl2.xspeed= -pl2.xspeed;
	if((pl2.y+pl2.ysize+pl2.yspeed)>200 ||
	   pl2.y+pl2.yspeed<0)
	    pl2.yspeed= -pl2.yspeed;

	if(scollision(&pl1, &pl2))
	    quit=2; /* 2 tarkoittaa, ett toinen saatiin kiinni */

	while(kbhit()) { /* tyhjennetn nppispuskuri */
	    switch(getch()) {
		case 'w': pl1.yspeed=-1; pl1.xspeed=0; break;
		case 's': pl1.yspeed=1; pl1.xspeed=0; break;
		case 'a': pl1.xspeed=-1; pl1.yspeed=0; break;
		case 'd': pl1.xspeed=1; pl1.yspeed=0; break;

		case 'u': pl2.yspeed=-1; pl2.xspeed=0; break;
		case 'j': pl2.yspeed=1; pl2.xspeed=0; break;
		case 'h': pl2.xspeed=-1; pl2.yspeed=0; break;
		case 'k': pl2.xspeed=1; pl2.yspeed=0; break;

		case '+': if(waittime) waittime--; break;
		case '-': waittime++; break;

		case  27: quit=1; break;
	    }
	}
	delay(waittime);
    }
    if(quit==2) { /* jos kiinni, niin feidataan ensin valkoiseen (rjhdys) */
	fadetowhite(palette);
	for(waittime=0; waittime<256*3; waittime++)
	    palette[waittime]=63;
    }
    fadetoblack(palette);
    textmode(0x3);

    return 0;
}

Tss oli sitten sellainen lhdekoodi, jota kukaan vhnkn omanarvontuntoa
omaava peliohjelmoija, taikka muukaan ohjelmoija EI TEE. Jos pelist to-
della halutaan selv ja helposti laajennettava ei tehd jokaiselle pelaa-
jalle eri sprite eri nimell, vaan kaikki pelaajaspritet ovat
taulukossa. Ja muutenkin esimerkkikoodi ainoastaan demonstroi mahdolli-
suuksia oppimiemme asioiden kyttmiseen, ei suinkaan minklainen pelin
runko pitisi olla. Siihen me palaamme myhemmin. Mutta menepps pelaamaan
ja nyt kavereillesi minklaisia pelej osaisit jo tehd. =) lk
palaa takaisin ennenkuin tmn kappaleen asiat ovat hallussa. Sill niiden
osaamista luultavasti tullaan vaatimaan seuraavissakin luvuissa. Mutta jos
olet malttamaton, niin on tietenkin mahdollista palata takaisin opettelemaan,
mutta turhauttavaa se on.

Jlkikteen kaiken sprite, animaatio ja bittikarttanprilyn jlkeen totean,
ett kaikissa kohdissahan ei kytetty tsmlleen oikeita termej. Bittikart-
tahan on kytnnss vain kuvadata ja mahdollisesti hieman listietoa, ani-
maatio on yleens perkkisi bittikarttoja osaksi yhteisell datalla, 
olio on yleens sitten se mik osaa pyyhki itsens ja joka tiet mitk
bittikartat ja muut vastaavat sille kuuluvat, joka voi pyyhki itsens ja
tehd monia muitakin kivoja asioita. Sprite on sitten jotain siell jossain
vlill tai pss, en tied kovin tarkasti mutta kytin nyt tt nimityst
tysin toimivasta oliosta joka kykenee itsens ksittelyyn.

4.4 Maskatut spritet
--------------------

Vhn aikaa sitten kerroin PC-Ohjelmointi -alueella tmn kurssin sisllst
ja eiks vain joku mennyt kysymn minulta selittik tutoriaali maskatut
vai maskaamattomat spritet. Minhn en ollut edes kuullut moisesta asiasta
ja utelin ideaa sen takana. Sainkin kuulla se ja tein sen pohjalta assemb-
lerilla nopean rutiinin. Pienell nopeuskokeella se osoittautui 11 kertaa
nopeammaksi kuin muutama luku sitten tekemmme rutiini. Aion nyt selitt
idean tmn tekniikan takana, joten kiinnittk turvavynne ja valmistau-
tukaa!

Maskatuiden spritejen ideana on se, ett niiden piirrossa ei tarvita pikse-
likohtaisia vertailulauseita lainkaan, jolloin voidaan kytt assembleril-
la neljn tavun kanssa operoivia funktioita. Mutta miten sitten kierrmme
vertailulausekkeet silytten silti lpinkyvyyden nollavrin kanssa?
Idea perustuu bittioperaattoreihin.

Jokaiselle spriten framelle tehdn etukteen maski, joka on nolla kohdissa
joissa on pikseli ja 255 lpinkyviss kohdissa. Nyt sitten vain suoritamme
kaksoispuskurin pikselille loogisen AND-operaation:

Maski spritelle  FF 00 FF FF
Nytt           4F 3C 93 5A
----------------------------
Tulos            4F 00 93 5A

Kuten huomaatte, jvt lpinkyvt kohdat (FF) jljelle. Sitten vain
kytmme OR-operaattoria sytyttmn spriten pikselit, sill ne kohdat
ovat juuri sken nollautuneet, joten looginen OR asettaa juuri oikeat
bitit:

Sprite           00 46 00 00
Maskattu nytt  4F 00 93 5A
----------------------------
Tulos            4F 46 93 5A

Lopun saat toteuttaa aivan itse. Huomattavaa tss on se, ett jos haluat
kytt tehokkaita 4 tavun (dword) operaatioita on bittikartan leveyden
oltava jaollinen neljll. Huipputehoon tarvitset assembleria, sill C:ll
on vaikea kontrolloida edell mainittuja asioita. Jos et viel osaa assemb-
leria, varsinkaan DJGPP:n AT&T syntaksia, suosittelen seuraavia tiedostoja:

ASSYT.ZIP     Assemblerin alkeet suomeksi.
PCGPE10.ZIP   PCGPE sislt kaiken muun lisksi assemblytutoriaalin.
DJTUT2_4.ZIP  Jos osaat Intel-syntaksin, muttet AT&T-syntaksia
              (movd %eax, %ebx). Sislt mys muuta kiinnostavaa
              materiaalia, jota tsskin tutoriaalissa on sivuttu.
NASM095B.ZIP  Tll voit tehd Intel-syntaksin assemblerilla DJGPP:n
              COFF-muotoisia objektitiedostoja. Tiivistettyn TASM joka
              osaa myskin DJGPP:n objektiformaatin. Huomaa, ett uusin
              versio voi olla muutakin kuin 0.95 (NASM095B.ZIP).

Lisksi voisi olla hyv idea lainata kirjastosta kirja 486-ohjelmointi,
joka on suomenkielinen assembler-ohjelmointia ksittelev kirja ja kaiken
lisksi hyv sellainen!

Loppulisyksen jlleen kiva vinkki Pekka Nurmiselta. Kaksoispuskuri 
kannattaa tarvittaessa tehd sen verran levemmksi, ett jos spitea
ei saada katki juuri neljn tavun kohdalta ei tuo tule toisesta reunasta
vastaan. Eli jtt sinne nelj tavua ruudun reunoihin, jota ei vain 
sitten kopioida nytlle. Nin kaksoispuskurin kooksi tulisi 328x200.


5.1 Nppimistn ksittely - ja nyt meill on hauskaa
-----------------------------------------------------

Jos pelasit ahkerasti esimerkkipelimme, niin ehk huomasit, ett painaessasi
useita nappia ilmenee mys useita ongelmia. Nihin voivat kuulua nppimis-
tn jumiutuminen, nappien huomiotta jttminen jne. Tarvitsemme siis ru-
tiinin joka pstisi meidt plkhst. Tarvitsemme nppishandlerin!
Tm perustuu siihen, ett joka kerta kun nappia painetaan kutsutaan
keskeytyst 9, joka lukee merkin nppimistlt portista 60h (0x60) ja
muuntaa sen ASCII:ksi ja laittaa nppimistpuskuriin. Mutta meps ohi-
tammekin tmn ja teemme oman handlerin, joka ei muutakaan mitn miksi-
kn ASCII:ksi, vaan laittaa nppimisttaulukon vastaavan kohdan arvoon
1, josta peli voi sitten sen tarkistaa. Ja kun nappi pstetn tulee
mys keskeytys, tll kertaa tulee napin arvo + 128, joten vhennmme
luetusta arvosta 128 ja nollaamme vastaavan kohdan taulukosta. Ja millainen
on tm taulukko?

Taulukossa on 128 alkiota, yksi jokaiselle SCAN KOODILLE, jollaisia nppi-
mist syyt. Olen tehnyt nist numeroista kirjaston, jossa esimerkiksi
ESC-nppimen scan koodi on nimell SxESC ja sen arvo on 1. Jos siis haluat
pelisssi tiet onko ESC painettuna, osoitat nppimistpuskuriin:

if(keybuffer[SxESC]==1) printf("ESC painettu!\n");

Kirjasto on nimell D_SCAN.H. Ja sitten tarvitsemme siis koodia, joka lukee
tavun portista 60h ja jos se on alle 128 se laittaa vastaavan kohdan
taulukosta ykkseksi ja jos se on yli tai yhtsuuri kuin 128, niin laitamme
alkion tavu-128 nollaksi. Lopuksi lhetmme signaalin PIC:ille, ett kes-
keytyksemme on valmis, eli outtaamme tavun 20h porttiin 20h. Tllainen on
siis handlerimme (KEYBOARD.H):

void keyhandler() {
    register unsigned char tavu=inportb(0x60);

    if(tavu<128) keybuffer[tavu]=1;
    else keybuffer[tavu-128]=0;

    outportb(0x20, 0x20);
}

Tm onkin oikeastaan helpoin osa tehtvmme. Vaikeampi (joskin esimerkki-
koodin takia helppo) on koukuttaa tarvitsemamme nppimistkeskeytys ja
palauttaa se kun tarvitaan nppimistrutiineja (gets, getch...) tai pois-
tutaan ohjelmasta. Lisksi tarvitsemme joukon apumuuttujia, jotka ovat
tss:

volatile unsigned char keybuffer[128], installed;
_go32_dpmi_seginfo info, original;

Keybuffer sil nppinten tilat, installed kertoo onko tm handleri a-
sennettuna ja est samalla uudelleenasentamisen. Kaksi viimeist muuttujaa
info ja original ovat koukuttamiseen ja koukutuksen (hooking) poistamiseen
tarvittavia rakenteita, joista infoa kytetn oman asentamiseen ja origi-
naliin siltn alkup. handlerin osoite ja muut tarpeelliset tiedot.

Tss on koukutukseen ja palautukseen tarvittava koodi, johon emme perehdy
kovinkaan tarkasti, lisinfoa asiasta saat vaikka DJGPP:n FAQ:sta hakusanalla
handler:

int setkeyhandler() {
    int c;
    
    for(c=0; c<0x80; c++)
	keybuffer[c]=0; /* nollataan napit */
    if(!installed) {
	_go32_dpmi_get_protected_mode_interrupt_vector(0x0009, &original);
	info.pm_offset=(unsigned long int)keyhandler;
	info.pm_selector=_my_cs();
	_go32_dpmi_allocate_iret_wrapper(&info);
	_go32_dpmi_set_protected_mode_interrupt_vector(0x0009, &info);
	installed=1;
	return 1;
    } else return 0;
}

int resetkeyhandler() {
    if(installed) {
	_go32_dpmi_set_protected_mode_interrupt_vector(0x0009, &original);
	installed=0;
	return 1;
    } else return 0;
}

Lismme kaikki kolme funktiota ja globaalit muuttujamme tiedostoon
KEYBOARD.H. Nyt meill on tarpeen vaatiessa tydellisen toimiva nppimis-
thandleri (jota ehk myhemmin tulemme kyttmn).


5.2 Fixed point matematiikka
----------------------------

Alamme pikkuhiljaa lhesty kurssimme loppua (tai ken tiet, todellista
alkua?), joten ksittelen tss hieman pelin optimointiin vaikuttavia
tekijit ja parannuksia aiemmin esittelemiimme kirjastoihin (omaan peliin
kun kannattaa kuitenkin tehd osa kirjastoista uusiksi). Selitn fixed-
pointin, lookupin idean ja pari muuta nopeuttavaa temppua sek mainitsen
pullonkauloja joita nopeuttamalla saadaan aikaan dramaattisia muutoksia.

Siis fixed point, mit se on? Kuten tiedt, C:n int-tyyppi on kokonaisluku,
eli sill ei voi ilmoittaa desimaalilukuja. Monesti desimaaliluvu olisivat
tarpeellisia, esimerkiksi sprite-enginess, jos halutaan ett eri spritet
liikkuvat eri nopeuksilla. Nytt nimittin todella typerlt jos ohjus 
pomppii kymmenen pikseli eteenpin, koska se on 10 kertaa nopeampi kuin 
pelin hitain sprite. Tarvitsemme siis nopeudeksi desimaaliluvun, jolloin 
ohjuksen nopeus voisi olla 1 ja kilpikonnan 0.1 (jolloin se liikkuisi yhden
pikselin joka 10. frame). Valitettavasti float-tyyppisten muuttujien k-
sittely on moninkertaisesti hitaampaa (tosin pentium-optimoitu peli voi 
niit kytt, ainakin assemblerilla voidaan pentiumin matematiikkapro-
sessoria kytt tysipainoisesti ja peli nopeuttaa). Niinp meidn ty-
tyisi pysty esittmn kokonaisluvuilla desimaalilukuja. Onko tm mahdol-
listakaan? 

Kyll se on, katsokaamme hieman toisella tavalla normaaleja lukujamme.

Meidn luvuissamme on kokonaislukuosa ja desimaaliosa sek vliss piste. 
Kokonaislukuosalla voidaan ilmaista 10^<numeroja> lukua, eli jos 
kokonaislukuosassa on 3 numeroa niin voimme ilmaista sill 10^3=1000
erilaista lukua, vlill 0-999. Pisteen toisella puolella on kaikki muuten
samalla tavalla, mutta meidn tytyy ajatella knteisesti. Voimme ilmaista
desimaaliosalla desimaalin, joka on yksi 10^<numeroja>:sosa. Tm nytt
sekavalta, mutta oletetaan ett meill on 2-numeroinen desimaaliosa, niin
pienin desimaali on 1/10^2, eli yksi SADASOSA. Seuraava kaavio varmaan sel-
vent asiaa:

1234.123  =  1234 + 123/10^3  =  1234 + 123/1000  =  1234.123

Nyt menemme vhn pidemmlle. Oletetaan, ett meill olisi luvussa pilkku
AINA samalla kohdalla ja desimaalia esittvi lukuja 3. Takaisin voisimme
sen palauttaa vain jakamalla kokonaisluku tuhannella (kolme desimaalinumeroa,
eli siis 10^3=1000):

1234123 = 1234123/1000  =  1234.123

Kuten huomaat pilkku voidaan ajatella sinne nelosen ja ykksen vliin.
Nyt kysyt ehk ett mit hyty tst on. Siit on seuraava hyty: Meill
on kaksi lukua, 0.1 ja 5.4, jotka haluamme laskea yhteen. Muunnetaanpa ne
oikeaan muotoon: 0.1*1000=100 ja 5.4*1000=5400. Haluamme laskea ne yhteen:
100+5400 = 5500. Nyt muuntakaamme takaisin:

5500/1000 = 5.5 = 5.5 (5.4 + 0.1 = 5.5).

Eli meill on sama tulos! Vhennyslasku toimii ihan yht hyvin. Voimme las-
kea desimaalilukuja kokonaisluvuilla. Mutta tarvitsemme viel kaksi laskua,
kerto- ja jakolaskun. Koska lukumme ovat kummatkin 1000-kertaisia todelli-
suuteen nhden niin ne kertomalla saamme 1000000-kertaisen tuloksen, joten
lopuksi meidn tytyy jakaa tulos tuhannella. Eli:

5400*100 = 540000  =>  540000/1000 = 540  =>  540/1000 = 0.54
(5.4 * 0.1 = 0.54)

Ja tadaa! Meill onkin oikea tulos. Viel jakolasku, siinhn jaamme vain
numerot toisillamme, mutta tss hvi meilt desimaaliosa, eli meidn pi-
tisi kertoa tulos lopuksi tuhannella. Tarkemman tuloksen saamme kun 
kerromme ensin jaettavan tuhannella ja sitten vasta jaamme:

(5400*1000) / 100 = 54000  =>  54000/1000 = 54  (5.4 / 0.1 = 54).

Nyt meidn tytyy sitten syventy siihen miten toteutamme nopeasti edelliset 
asiat tietokoneen binrijrjestelmll. Se on erittin helppoa. Teemme 
vaikka 32-bittisen luonnollisen (unsigned int), josta 16 alinta bitti on 
varattu desimaaliosalle. Koska binrijrjestelm on 2-kantainen, niin 
meidn tytyy vain muuttaa pikku laskumme kahden potensseilla leikkimisiksi. 
Tllaisella luvulla voimme siis esitt 16-bittisen kokonaislukuosan, 
maksimissaan 2^16=65536 ja 16-bittisen desimaaliosan, joten pienin desimaali 
n 1/2^16 = 1/65536 = n. 0.000015228.

Entiset laskumme toimivat ihan hyvin, muunnamme vain luvut kertomalla ne
65536:ll ja palautamme jakamalla 65536:ll. Nopeuttamisessa apuna ovat 
viel bittisiirrot, joiden avulla voimme kertoa nopeasti 65536:lla 
siirtmll bittej 16 vasemmalle ja jakaa siirtmll niit oikealle. 
Tss on pieni esimerkkiohjelma, joka demonstroi fixedin kytt:

#include <stdio.h>

int main() {
    unsigned int a, b, tulos;

    a=(unsigned int)(5.4 * 65536.0);
    b=(unsigned int)(0.1 * 65536.0);

    tulos=a+b;
    printf("A+B=%f\n", tulos/65536.0);
    
    tulos=a-b;
    printf("A-B=%f\n", tulos/65536.0);
    
    tulos=(a*b)/65536;
    printf("A*B=%f\n", tulos/65536.0);
    
    tulos=(a/b)*65536;
    printf("A/B=%f\n", tulos/65536.0);

    return 0;
}                                  

Mieti nyt kaikkea ihan rauhassa. Jos luulet ymmrtneesi edes jotain niin
hyv, jos et ymmrtnyt mitn niin lue uudelleen ja uudelleen ja kokeile
paperilla. Jos et siltikn ymmrtnyt niin lue jostain toisesta dokumentis-
ta! Fixed-pointissa on huomattava pari asiaa:

1) Luvut voivat menn yli ja tulee ihmeellisi tuloksia. Jakolaskuesimerkis-
   sni en voinut kertoa a:ta ensin 65536:lla, sill muuten olisi luku men-
   nyt ympri. Kannattaa aina varmistaa ettei luku voi menn ympri.

2) Kyt bittioperaatioita aina kuin mahdollista. 32-bittisest
   16.16-fixedist (tarkoittaa, 16 bitti kokonais- ja 16 bitti desimaali-
   osalle) saat desimaaliosan halutessasi AND-funktiolla maskin 0xFFFF 
   kanssa. Voit kytt kaikkia nerokkaita optimointikikkoja jos vain kek-
   sit niit. Mys pyrhdyst voi kytt hyvksi (jotenkin).

3) Signed luvut toimivat samoin, mutta ylin bitti merkkaakin etumerkki,
   eli 16.16-luku int-tyyppin onkin oikeasti 15.16.

4) Valitse itse pilkun paikka. Mit enemmn bittej desimaaleille sit tar-
   kempia lukuja. Mit enemmn bittej kokonaisluvuille sit suurempia ja
   eptarkempia lukuja.


5.3 Lookup-tablet ja muita optimointivinkkej
---------------------------------------------

Lookup-tableissa, eli lookupeissa ei ole oikeastaan muuta selittmist, kuin
ett niiss toistuvia, vain yht (tai joskus kahtakin) muuttujaa kyttvis-
s monimutkaisissa laskutoimituksissa (tai muuten vain hidastavissa) 
lasketaan tulokset etukteen taulukkoon kytten indeksin sit lukua joka
oli muuttuvana laskutoimituksessa. Thn ky esimerkkin sinin laskeminen
taulukkoon. Sin-funktio on hidas laskea ja siin pit aina suorittaa pitk
konversio asteista radiaaneiksi (3.14*2*aste/256, 256:n ollessa suurin 
kulma + 1, 360-asteisella ympyrll luku olisi 360 ja suurin kulma 359) ja 
lopuksi viel ottaa siit sini. Nyt laskemmekin kaikki 256 arvoa taulukkoon 
(fixed-point-sellaiseen, muoto 1.14, 16-bittinen signed, muuntoluku 16384):

for(c=0; c<256; c++) 
    sin_table[c] = (short)(sin(3.141592654*2*c/256.0)*16384);

Nyt jos haluamme kulman 15 sinin, niin osoitamme vain sin_table[15], emmek
(short)(sin(3.141592654*2* 15 /256.0)*16384).

Sitten sekalaisia optimointivinkkej:

1) Suuria mri dataa ksittelevt loopit assemblerilla. Lis tietoa
   inline-assemblerin kytst DJGPP:ll tiedostosta DJTUT*.ZIP,
   vaikka MBnetist, tai tmn tutoriaalin Nasmia ksittelevst
   luvusta.

2) Kaikki muuttumattomat vertailulausekkeet loopin ulkopuolelle:
   for(c=0; c<1000000; c++) if(a==b) puskuri[c]=0;     onkin:
   if(a==b) for(c=0; c<1000000; c++) puskuri[c]=0;
   Vhennmme nin 1000000 vertailua.

3) l tuhlaa aikaasi optimoimalla suuria mri logiikkaa, ellei siit
   todella ole hyty. Esimerkkin vaikka kaksoispuskurin tyhjennyksen
   tekeminen inlinen memsetin sijaan sst kyll aikaa, mutta kun
   ajansst funktiokutsun jmisess pois on jotain 1/10000 siit
   mit aikaa memsetiss menee joka tapauksessa, on hydyttmyys
   varsin ilmeist.

4) Kyt fixedi floatin tilalla aina kuin mahdollista.

5) Laske kaikki toistuva konemainen laskenta taulukkoihin.

6) Kyt DJGPP:n knnsvalitsinta -O2, tai jopa -O3 (joka kyll suurentaa
   ohjelmaasi reilusti).

Yleenskin kannattaa uhrata paljon aikaa grafiikkakirjastojen ja nikirjas-
tojen optimointiin ja pit itse runko selken C-kielisen kutsujen joukko-
na. Tm ei paljoa hidasta ja selvent uskomattomasti koodia ja nopeuttaa
kehityst.


5.4 Vliaikatulokset ja fontteja
--------------------------------

Tss vaiheessa osaat nyt kaikki trkeimmt niksit mit peliohjelmointiin
tarvitaan. Tst luvusta lhtien alan tietoisesti vhentmn, ellen
jopa joissain kohdissa poistamaan esimerkkiohjelmia. Mit tst lhtien
tarvitset on maalaisjrke ja kyky osata soveltaa oppimiasi asioita.

Eli tnn meill on siis jotain, mit kutsutaan nimell fontit? Idea fon-
tienginen teossa on tehd tavallaan karsittu bittikarttaengine. Fontti-
enginen voit tehd esimerkiksi poistamalla sprite-koodistamme pyyhkimisen
(halutessasi voit mys poistaa lpinkyvyyden tai jtt pyyhkimisen jos
tarvitset sit, sinun pit siin tapauksessa vain tehd erikoisjrjeste-
lyj) ja kytt animaationa kuvasarjaa jossa on piirrettyn merkit a-z,
A-Z, 0-9 ja sitten joitakin mahdollisesti tarvittavia vlimerkkej, kuten
.!?,;:'" ja muut vastaavat. Sitten vain teet funktion, joka vaihtaa framek-
si oikean kuvan ja piirt sen, jonka jlkeen se korottaa x-arvoa merkin
leveydell (plus jonkin verran vli seuraavan merkin ja viimeisen vlille)
ja ottaa ksittelyyn seuraavan merkkijonon merkin.

Koodi voisi nytt vaikka tlt:

void printString(char *string, int x, int y) {
    int c;

    for(c=0; c<strlen(string); c++ {
        if(string[c]>'a' && string[c]<'z') {
            setframe(string[c]-'a'); /* a olisi frame 0 */
            drawchar(x+c*9, y); /* merkin leveys 8 + 1 pikseli erottamaan */
        } else if(string[c]>'A' && string[c]<'Z') {
            setframe(string[c]-'A' + 'z'-'a' + 1);
            /* eli suomeksi A-kirjaimella olisi paikka heti viimeisen pienen
               kirjaimen jlkeen, joka on 'z'-'a' */
            drawchar(x+c*9, y);
        } else if(string[c]>'0' && string[c]<'9') {
            setframe(string[c]-'0' + 'z'-'a' + 1 + 'Z'-'A' + 1);
            /* tm taas tulee pienien JA isojen kirjaimien jlkeen */
            drawchar(x+c*9, y);
        } else if(c == '.') { /* jos c on erikoismerkki */
            setframe('9'-'0' + 1 + 'z'-'a' + 1 + 'Z'-'A' + 1);
            drawchar(x+c*9, y);
            /* ideana siis, ett piste tulee kaikkien kirjainten ja
               numeroiden jlkeen */
        }
        ...
            
    }
}

Kuten ehk huomasit tuli koodista aivan kammottavaa sekasotkua ja on ihme
jos sait siit jotain selv. Lisksi koodi ei ole erityisen nopeaakaan,
saati sitten ett se edes vlttmtt toimii. Mutta miten voisimme nopeuttaa
tt? Vastaus on lookup-tablet. Sill mehn tiedmme, ett C:ll kirjain on
vain numero vlill 0-255. Niinp teemme taulukon jonka jokainen alkio
osoittaa indeksin mukaisen ASCII-kirjaimen framenumeroon. Jos et ymmrtnyt
niin tss on esimerkki taulukon kytst:

frame = asciitaulukko['a'];

Asciitaulukon alkio 'a' (numerona 97) olisi 0, joten framenumeroksi tulisi
ninollen tm luku. Sitten vain framenvaihto: "setframe(frame)".
Tietenkin tuo kannattaisi kytt nin: "setframe(asciitaulukko['a'])"...

Mutta miten sitten taulukko alustetaan? Tapoja on monia, jotkin ovat seka-
vampia ja jotkin vhn selvempi, mutta annan sinun itsesi ptt mik on
paras. Mahdollisuutena olisi ensin tytt taulukko nollalla (joka olisi
tyhj frame) ja sitten loopata aakkoset a-z tytten taulukon kohdat 'a'-'z'
oikeilla framearvoilla (1...26), sitten loopataan 'A'-'Z' tytten ne alkiol-
la 27...52 jne. Mys lataaminen kannattaa automatisoida.

Muista lisksi huomioonottaa erikoismerkit enginesssi. Tarpeellisia voivat
olla vlilynti (32), rivinvaihto (\n), tabulaattori (\t) jne. Ja lisksi
saat aivan vapaasti ptt onko fontin vri mahdollista vaihtaa vai kytt-
k aina samanlaisia fontteja, joka mahdollistaa vhn hienommat, vaikka moni-
vriset fontit.


5.5 Hiirulainen, jokanrtin oma lemmikki
----------------------------------------

Tnn, tytt ja pojat, set puhuu hieman kotielimist. Ne ovat sellaisia
pieni valkoisia tkit, joilla on hnt ja jotka viipottavat matolla.
Sen lisksi niit voi mys painella. Ei, nyt ei ole kyse mistn karvaisesta,
vaan ihan aidosta tietokoneen lislaitteesta, jota hiireksikin kutsutaan.

Tll karvattomalla ystvllmme on sdyttmn monia haaroja sukupuussaan.
Lytyy Logitechia, Microsoftia, Targaa ja ties mit vimputinta ja kaiken
kukkuraksi rautatasolla kskyttminenkin on suorastaan sdyttmn
epstandardia. Onneksi htiin rient kymmenisen vuotta vanha apu nimel-
tn _hiirikeskeytys_, kiinnostavemmin ilmaistuna keskeytys 33h. Tt
keskeytyst kytten saadaan kaikkien hiireen tungettujen vimpainten, kuten
nappien ja pohjassa (yleens) pyrivn pallukan tila. Nm tiedot ovat helpon
saatavuuden lisksi mys naurettavan helppokyttisi, kunhan vain tiet
miten niit kytt.

Jos et viel tied miten keskeytyksi kytetn tulee tss tiivistettyn
niiden kytt DJGPP:ll. Keskeytykselle annetaan parametrit rekistereiss
ja ne saadaan rekistereiss. Jos DJGPP oli yht huoleton kuin Borland
Turbo-kntjineen olisi meillkin rekisteri ax nimell _AX jne. Mutta koska
kaikki on tehty rakkaalla kntjllmme hipun vaikeammaksi teemme sen
standardilla tavalla. Alhaalla net tarvittavat askeleen keskeytyksen kut-
sumiseksi ja rekisterien nplykseksi. Esimerkki kytt yht kymmenist kes-
keytyksen aiheuttavista funktiosta int86(...) kirjastosta dos.h:

1) Tarvitset rekisterit muuttujinaan sisltvn unionin, int86:n tapauksessa
   unioni on nimeltn REGS ja sen sisll on pari structia joihin
   tutustut vaikka selaamalla ko. kirjastoa. En ala perehtymn syvemmin
   nihin x, d ja w-rakenteisiin. Tss kuitenkin kytmme viimeist, joka
   on 16-bittiset rekisterit.

   union REGS rekisterit;

2) Tunge kaikki parametrit uuteen muuttujaasi.

   rekisterit.w.ax=jotain;
   rekisterit.w.di=muuta;
   rekisterit.w.cs=kivaa;

3) Kutsu funktiota int86(vektori, inputti rekisterit, outputti rekisterit)

   int86( keskeytys, &rekisterit, &rekisterit );

4) Kaivele esiin muuttuneet rekisterisi ja tallenna ne muuttujiin.

   ihan=rekisterit.w.bx;
   helppo=rekisterit.w.ds;
   homma=rekisterit.w.cx;

Tehdesssi hiiriohjattua ohjelmaa sinun pit tietysti hiiren koordinaattien
ja nappien ksittelyn lisksi piirt kursori ruudulle, ellet sitten halua
kytt (amatrimisen nkist) kursoria, jonka ajuri piirtelee ruudullesi.
Grafiikkatilassa tm onnistuu vaikka tekemll hiirest yksi spriteist ja
liikuttelemalla sit. Antaa paljon paremman kuvan ohjelman tekijstkin!
Tekstitilassa vaihdat vaikka ko. kohdan vri. Thn ihmeelliseen tilaan
tutustumme kohtapuolin, eli jatka lukemistasi jos haluat tehd tekstitila-
ohjelman, joka kytt kursoria...

Tss nyt olisivat nm kaikkein kytnnllisimmt ja alkuun auttavat funk-
tiot. Lis lydt vaikkapas Ralph Brownin interruptilistasta tai kenties
jopa HelpPC:st. RB:n lista on MBnetiss nimell INTERxxy.ZIP, jossa xx on
versionumero (kai 48 tarkoittaen 4.8:aa) ja y paketin numero, itse listassa
A-E tjsp. ja muitakin kirjaimia on sislten muunmuassa selailuohjelman,
konvertoinnin Windowsin help-muotoon jne.. Mutta, kuten lupasin:

Funktio 0 - Hiiren alustus
  Parametrit: AX=0
  Palauttaa:  AX=0 jos ajuria ei ole installoitu, FFFFh jos on installoitu.

Funktio 1 - Nyt kursori (se kauhea siis)
  Parametrit: AX=1
  Palauttaa:  -

Funktio 2 - Piilota kursori (se kauhea siis)
  Parametrit: AX=2
  Palauttaa:  -

Funktio 3 - Anna koordinaatit ja nappien tila
  Parametrit: AX=3
  Palauttaa:  CX=x-koordinaatti (0...639)
              DX=y-koordinaatti (0...199)
              BX=nappien tila (bitti 0 vasen nappi, bitti 1 oikea ja
                               bitti 2 keskimminen nappi)

Funktio 4 - Aseta kursorin koordinaatit
  Parametrit: AX=4, CX=x-koordinaatti, DX=y-koordinaatti
  Palauttaa:  -

Funktio 5 - Nappien painallukset
  Parametrit: AX=5,
              BX=mik nappi (0 vasen, 1 oikea ja 2 keskimminen)
  Palauttaa:  Muuten kuten funktio 3, mutta koordinaatit kertovat kursorin
              sijainnin viime painalluksella ja BX kertoo ko. napin painal-
              luksien mrn sitten viime kutsun.

Funktio 6 - Nappien vapautukset
  Parametrit: AX=6,
              BX=mik nappi (0 vasen, 1 oikea ja 2 keskimminen)
  Palauttaa:  Muuten kuten funktio 5, mutta vapautuksen tiedot.

Funktio 7 - Vaakarajoitukset
  Parametrit: AX=7,
              CX=pienin sallittu X-sijainti,
              DX=suurin sallittu X-sijainti
  Palauttaa:  -

Funktio 8 - Pystyrajoitukset
  Parametrit: AX=8,
              CX=pienin sallittu Y-sijainti,
              DX=suurin sallittu Y-sijainti
  Palauttaa:  -

Funktio B - Liikemr
  Parametrit: AX=B
  Palauttaa:  CX=vaakamikkien mr
              DX=pystymikkien mr

Funktio F - Mikkej pikseli kohden
  Parametrit: AX=F
              CX=vaakamikkien mr
              DX=pystymikkien mr
  Palauttaa:  -

Lisksi on viel ainakin funktio C, joka asettaa oman ksittelijn, mutta
koska se ei luultavasti kiinnosta kovin monta (rm-osoitetta odottava ksit-
telij ei ehk oikein toimi PM:ss kunnolla jne...) jtn sen tss vliin.
Sitten vain tekemn kaiken maailman testiohjelmia. Esimerkkej ei tule
tss lainkaan, sill oletan jokaisen pystyvn edellisten ohjeiden perusteel-
la kyhmn itsen tyydyttvn ohjelman.

Jos homma ei kuitenkaan ota luonnistuakseen tai tss kappaleessa oli muita
epselvyyksi niin otahan yhteytt niin kaivelen lis tietoa aiheesta.
Erityiskiitos tmn kappaleen teon auttamisesta kuuluu nyt kyll MB:n numerol-
le 4/96 josta katsoin nopeasti tiivistelmn hiirifunktioista.

Ja ensi kappaleessa onkin uudet kujeet, nyttisi olevan tekstitilan hallinta
seuraavana edess...


5.6 Tekstitilan ksittely suoraan
---------------------------------

Tst kappaleesta tulee tulemaan rimmisen lyhyt. Ainoa meit kiinnostava
seikkahan on tekstimuistin osoite (tila 3, 80x25, mys muut voivat toimia)
ja rakenne. Osoite on perusmuistin segmentti B800h, eli lineearinen osoite
selektorin _dos_ds osoittamassa muistissa olisi C:ll 0xB8000. Rakenne
on mys naurettavan yksinkertainen. Erona VGA:han (ks. kappale
"Grafiikkaa - mit se on?" jos et muista) on vain se, ett yksi alkio
koostuu kahdesta tavusta (joista ensimminen on merkin ASCII ja toinen
merkin vri) ja ruudun leveys on 80 merkki. Jos ei mennyt phn niin
tutustu viel kerran VGA:ta ksittelevn kappaleeseen ja tutkaile seuraavia
makroja:

#define putchar(x, y, c) _farpokeb(_dos_ds, 0xB8000+(y*80+x)*2, c);
#define putcolor(x, y, c) _farpokeb(_dos_ds, 0xB8000+(y*80+x)*2+1, c);

Viel jos olit kiinnostunut hiiren kursorin tekemisest tekstitilaan voisi
seuraava funktio olla sinulle omiaan:

void inline addcolor(int x, int y, char c) {
    int originalc=_farpeekb(_dos_ds, 0xB8000+(y*80+x)*2+1);
    putcolor(x, y, originalc+c);
}

Sitten vain "piirrt" kursorin lismll vriarvoon - sanotaan vaikka 17
ja pyyhit kursorin lismll siihen saman arvon vastaluvun (-17), eli
toisinsanoen vhennt siit 17:

#define CShow(x, y, c) addcolor(x, y, c)
#define CHide(x, y, c) addcolor(x, y, -c)

Makrojen kytt sitten komennoilla "CShow(17)" ja "CHide(17)"...

Lopuksi viel sananen merkin vrin muodosta. Se on XYYYZZZZ, jossa jokainen
kirjain edustaa yht bitti vritavussa. X ilmaisee vilkkuuko merkki (1).
YYY ilmaisee taustan vrin (0-7) ja ZZZZ ilmaisee tekstin vrin (0-15).
Tss viel pikkuruinen makro, joka voi osoittautua hydylliseksi:

#define BuildC(blink, fore, back) ( (blink<<7) + (back<<4) + (fore) )

Sitten vain vaikka komento "putcolor(x, y, BuildC(0,15,1))", joka aiheuttaisi
vlkkymttmn valkoisen tekstin sinisell pohjalla (31).

Sellaista tll kertaa. Nyt painun suihkuun ja katsomaan X-Filesia. Jatketaan
taas vaikka huomenna!


6.1 Projektien hallinta - useat tiedostot
-----------------------------------------

Nyt seuraakin sitten jakso lukuja (tai yksi luku, katsotaan nyt),
joissa ksitelln kaikkea trke mit pelej ohjelmoidessa pit
osata sen hardwaren tuntemuksen lisksi. Tarkoituksena on kyd lpi
useiden c-tiedostojen kytt, headerien teko, Rhiden projektit,
makefileet, ulkoisen assyn ja assyn yleenskin kytt, engineiden
teko, kirjastojen luonti. Kaikki suhteellisen kevytt kamaa kun ne
vain kerran opettelee, joten aloitamme.

Thn asti olen opettanut teille huonoja tapoja joita itsellni oli
tapana kytt viel puolitoista vuotta sitten (ja vasta viime aikoina
olen pssyt lopullesesti niist eroon). Olen nimittin laittanut
koodia noihin .h-tiedoistoihin ja tehnyt niist kirjastoja, joiden
rutiineja on sitten helppo kytt. Laajempien projektien ja miksei
hieman suppeampienkin kanssa alkaa kuitenkin ennenpitk esiinty
suorastaan rsyttvn hidasta kntmist. Ajattele seuraavaa
tapausta:

Peliprojektissa on niengine sound.h (yksinkertainen, vain vhn alle
3000 rivi), sprite-engine sprite.hh (minimaalinen toiminta, hieman
inline-assy, 800 rivi), sekalaisia hardware-rutiineja
(kellokeskeytys, nppishandleri jne. 1000 rivi) sek itse pelin
koodia 2000 rivi. Nin joka kerta knnmme vhn alle 7000 rivi
C-koodia. Mutta miksi knt kaikki joka kerta kun vain yksi muuttuu
yleens kerrallaan? Muuttakaamme hieman lhestymistapaa lytksemme
parempi keino.

Keinoa kutsutaan projekteiksi, usean C-tiedoston kytksi ja ties
miksi. Ideana on, ett jokainen looginen kokonaisuus on jaettu omaan
.c-tiedostoonsa ja .h-tiedostoonsa. Tllaisia voisivat olla
nppishandleri, timerhandleri, sprite-rutiinit, modien lataus,
nienginen ohjelmointirajapinta, sb-osa koodista, gus-osa koodista
jne.. Jokaiselle tiedostolle olisi sitten oma .h-tiedostonsa, jossa
mritelln kaikki c-tiedoston funktiot ja globaalit muuttujat (jos
niit tarvitaan). Sitten toiset c-tiedostot jotka tarvitsevat tuon
tiedoston funktiota tai muuttujia ottaisivat vain includella
h-tiedoston mukaan ja kntjn linkkeri huolehtisi siit, ett
ohjelmakutsut menevt oikeisiin osoitteisiinsa.

Katsotaanpas pient esimerkki h-tiedostoa ja c-tiedostoa. En vit
tmn olevan ainoa oikea tapa, tm on vain yksi tapa hoitaa homma:

ESIM.H:

#ifndef __ESIM_H
#define __ESIM_H

#include <stdio.h>

#define ESIMTEKSTI "Moikka, olen esimerkki!"

void Esimteksti();

extern int Kutsukertoja;

#endif


ESIM.C:

#include "esim.h"

int Kutsukertoja=0;

int Oma=666;

void Esimteksti() {
    puts(ESIMTEKSTI);
    Kutsukertoja++;
}


Lhdetnps askeltamaan ESIM.H-tiedostoamme lvitse. Ensimmisen
rivi #ifndef __ESIM_H, joka ilmoittaa C-koodin esiksittelijlle, ett
jos __ESIM_H ei ole mritelty (IF Not DEFined, IFNDEF) niin osio
#ifndef:in ja #endif:in vliss tulee ottaa mukaan. Sen jlkeen
mritelln tuo kyseinen muuttuja, jotta H-tiedostoa ei pureta
kahteen kertaan (voi sattua kaikkea hassua jos vaikka h-tiedostot
kutsuvat toisiaan). Sitten tulee tmn C-tiedoston tarvitsemien
funktioiden kirjastot ja #definet (kirjastot voitaisiin sijoittaa mys
C-tiedostoon, mutta joskus tst tulee ongelmia, jos kytetn makroja
tai muuta vastaavaa). 

Sitten tulevat muuttujat ja funktiot. Muuttujien eteen TULEE laittaa
extern-mre, joka kertoo ett ne on oikeasti mritelty jossain
muualla, jottei kntj varaa muistia nille joka H-tiedoston
includettamisen kohdalla, jolloin linkatessa useissa C-tiedostoissa on
varattu muistia samannimiselle globaalille muuttujalle -> ongelmia.
Funktioiden edess extern ei ole pakollinen ja sen voikin jtt pois
ja list extern-mreen jos ko. funktio on ulkoisessa
assembler-tiedostossa.

Funktion parametrien nimet voi halutessa jtt mrittelyist pois,
mutta se ei ole suositeltavaa. Muista mys, ett globaalit muuttujat
esitelln ja alustetaan VAIN ja AINOASTAAN C-tiedostossa, ei
H-tiedossa!

C-tiedosto sislt vastaavat H-tiedostossa "luvatut" funktiot ja
muuttujat. Jos haluat tehd globaaleja muuttujia jotka eivt ny
muihin C-tiedostoihin, niin jtt sen esittelyn H-tiedostosta pois,
jolloin headerin sisllyttvt muut C-tiedostot eivt tied mitn
ko. muuttujan olemassaolosta eik vahingossa tule virheit. Tllainen
on esimerkki C-tiedoston muuttuja Oma.

Useita C-tiedostoja kyttesssi teet siis jokaisesta loogisesta
kokonaisuudesta oman "paketin", joka sislt C-tiedoston, joka on
toimiva kokonaisuutensa ja H-tiedoston, joka tarjoaa muille
C-tiedostoille mahdollisuuden kytt tmn paketin rutiineja.
Muista, ett kyttesssi includea tuollaisen tiedoston kohdalla
kytetn heittomerkkej normaalin <>-parin sijasta, jottei kntj
lhde hakemaan ESIM.H:ta omasta include-hakemistostaan, vaan jotta se
hakisi tiedoston senhetkisest tyskentelyhakemistosta.

Mieti nyt nm asiat selviksi, jotta ymmrrt miten tehdn useita
tiedostoja ja kytetn ilman ongelmia, niin voit sen jlkeen jatkaa
seuraavaan lukuun, jossa kerrotaan miten niist muodostetaan ajettavia
ohjelmia, kirjastoja ja objektitiedostoja.


6.2 Useiden tiedostojen projektit - kntminen ja hallinta
-----------------------------------------------------------

No niin, osaat nyt tehd C-tiedostoja ja H-tiedostoja, mutta sill ei
varmaankaan pitklle ptkit. Lhdemme nyt tutkimaan hieman
kntjmme, GCC:n sielunelm ja tutustumme muutamaan elintrken
tietoon joita ilman ei voi edes el. Nimittin janoamme tietoa
formaateista.

Tiedostot joiden kanssa pyrimme DJGPP:n kanssa voidaan jakaa helposti
pelkisten neljn (4) kategoriaan. Tss ne ovat:

1. Lhdekooditiedostot (c, cc, s, asm). Kntj muuttaa koodin
   konekieleksi ja tekee muut tarvittavat tehtvt tuottaen
   objektitiedoston.

2. Objektitiedosto (O). Sislt koodin ja symboleja (eli funktioiden
   ja muuttujien nimi) ja kaikkea muuta kivaa infoa jotka liittyvt
   olennaisesti rutiinien kskyihin ja dataan. Linkkeri linkkaa kaikki
   objektitiedostot yhteen ja lis tarvittavaa kynnistyskoodia sun
   muuta luodakseen ajettavan tiedoston. Nm ovat ernlaisia
   rakennuspalikoita, joissa kaikki on jo binrimuodossa.

3. Archive (A). Tt voidaan halutessa kytt useiden objektien
   silmiseen, eli paketoidaan monta objektitiedostoa yhteen kasaan
   jotka voidaan liitt sitten yhten pakettina
   kntjlle. Objekteista siis kootaan nippu jota voidaan ksitell
   yhten kokonaisuutena.

4. Ajettava tiedosto. Sislt objektitiedostoista tehdyn EXE:n, jossa
   on lisksi tarvittava koodi ohjelman kynnistmiseen.

GCC:n toimintaperiaate EXE:n knnss on seuraava: Lhdetn
kntmll lhdekooditiedostot objektitiedostoiksi. Tss vaiheessa
siis laajennetaan makrot, includet ja esiksittelijn komennot (kaikki
#ifndef-rakenteet sun muut). Sitten knnetn koodi konekielelle ja
tehdn objektitiedostot. 

Seuraavaksi kutsutaan linkkeri joka liitt objektitiedostot yhteen ja
lis tarvittavat kirjastot (LIBC.A tulee EXE:en aina mukaan ja
lisksi muut -l<nimi> parametreill annetut kirjastot) sek
aloituskoodin, joka kutsuu main-funktiota, jonka oletetaan lytyvn
jostain O-tiedostosta.

Itseasiassa tuo ei mene aivan noin yksinkertaisesti, mutta trkeint
on ymmrt, ett lhdekoodista tehdn rakennuspalikoita,
objektitiedostoja joista voidaan myhemmin koota ajettavia tiedostoja.

Jos meill siis olisi C-tiedostot main.c ja apu.c (mahdollisesti
vastaavine H-tiedostoineen), joista main.c sisltisi main-funktion ja
pkoodin ja apu.c kaikkia tarpeellisia rutiineja, niin voisimme
knt ne objektitiedostoiksi ja aina kun jompaakumpaa muunnetaan,
niin kntisimme tmn lhdekooditiedoston uudelleen. EXE
muodostettaisiin erikseen toisella komennolla jolloin muutos toisessa
tiedostossa vhentisi knnettvn koodin mr (tosin linkkausty
pysyisi ennallaan).

Miten sitten nit erilaisia tiedostoja tehdn? Hyv kysymys. Alla
nette kaikkein komentoja objektitiedostojen, EXE:jen ja archivejen
luontiin, lhdekoodit osaatte varmaan jo. =)

Objektitiedosto GCC:ll:
  gcc -c koodi.c -o objekti.o (halutessa lhdetiedostoja voi olla useampia)

Archive-tiedosto objektitiedostoista:
  ar rs archive.a objekti1.o ... (kaikki halutut objektit vain pern)

Ajettava tiedosto archive-, objekti- ja lhdekooditiedostoista (GCC
osaa ksitell ne ptteiden mukaan):
  gcc <tiedostot> -o tulos.exe <parametrit>

Lis infoa sit haluaville lytyy englanninkielisen komennolla
INFO. Sit lytyy aika paljon enk todellakaan halua tst
tutoriaalista mitn DJGPP:n komentoriviparametrien selityst. =)

Eli kerrataan viel vaiheet joita kyttte "oikeaoppisen" projektin
tekoon:

1. Luo C- ja H-tiedostot ja muu tarvittava lhdekoodi
2. Knn ne O-tiedostoiksi (tyyliin gcc -c koodi.c -o objekti.o)
3. Jos haluat tehd kirjastoja, niin tee objektitiedostoista ar:ll
   niit. Esimerkiksi grafiikkaenginen objektitiedostot voisi liitt
   yhteen ja nimet libgraf.a:ksi ja siirt DJGPP:n LIB-hakemistoon.
   Myhemmin nuo enginen objektit olisi helppo list EXE:een pelkll
   -lgraf -parametrilla.
4. Knn ajettava ohjelma objektitiedostoista ja archive-tiedostoista
   (gcc <tiedostot> -o tulos.exe <parametrit>). Archive-tiedoston
   nimen voi antaa joko tiedostojen mukana tai parametrin -l<nimi>
   JOS archive on DJGPP:n LIB-hakemmistossa nimell lib<nimi>.a.

Grafiikkaenginekin voi olla projekti, jolloin jttte EXE:ksi
kntmisen kokonaan pois, ja teette vain archive-tiedoston. Tai jos
tarvit vain yhden .o -tiedoston, niin miks siin, valinta on vapaa.

Nyt sinun pitisi osata tehd objektitiedostoja lhdekoodista,
kirjastotiedostoja objekteista ja ajettava ohjelma objekteista (ja
mahdollisesti mys kirjastoista). Kun hallitset nm asiat jatkamme
jlleen taivaltamme.


6.3 Hieman automaatiota - tapaus Rhide
--------------------------------------

No tll hetkell me osaamme kaikki tarvittavat taidot komentorivilt,
mutta uusien tiedostojen nimien muistaminen ei aina ole kivaa ja
komentorivill vntminen sopii vain perusteiden harjoitteluun. Rhide
on tapa pst koko roskasta helpolla ilman perusteita edes
objektitiedostoista, mutta koska teill tulee olemaan niin paljon
helpompaa kun ne osaatte niin olen katsonut tarpeelliseksi ne mys
neuvoa. (sill Rhidenkin kanssa kunnon projekteilla tarvitaan tuota
osaamista).

Ainahan psee helpolla, mutta valitettava tosiasia on, ett se joka
hyppsi edelliset kappaleet ylitse onkin sormi suussa kun tulee
ongelma eteen. Mikn ei korvaa tietoa ja kokemusta, ei edes hyv
ohjelmointivline.

Eli tmn kappaleen tarjoama informaatio ksittelee Rhide ja sen
projekteja projektien hallinnassa. Jos teit ei Rhide kiinnosta niin
voitte hypt yli, lupaan ett seuraava kappale kiinnostaa teit,
sill makefilejen kytt on vaihtoehtoinen (ja gurumpi, elegantimpi ja
yleisempikin) tapa automatisoida projektien kntminen. Mutta te
joita kiinnostaa yksi tmn hetken parhaimmista DOS-ympristn
IDE-ohjelmista pysyk kappaleessa, tosin asia voi olla joillekin jo
vanhaa leip.

Eli Rhiden sislt makefileiden kaltaisen jrjestelmn projektien
hallintaan, mutta toisin kuin make se sislt tekoly, joka osaa
projektille valitusta kohteesta ptell millainen tulos halutaan ja
projektin tiedostojen ptteist minktyyppinen tiedosto on kyseess
ja miten se pit knt. Koska Rhide on aika yksinkertainen
jrjestelm ksittelen vain lyhyesti sen perusasiat, eli projektien
teon, availun, ksittelyn, Rhiden kustomoinnin ja kohteiden
mrmisen.

Eli aloittakaamme tekemll oletusprojekti Rhidelle. Ensimminen
tehtvsi lienee installoida Rhide, joka yleens koostuu purkamisesta
DJGPP-hakemistoon ja ohjelman kynnistmisest kokeeksi. Dokumenttien
lukeminenkaan ei ole pahasta, mutta kyll ilmankin voi prjt, tosin
vaikeuksien sattuessa ne ovat usein korvaamattomia. Rhiden jotkin
versiot ovat olleet enemmn tai vhemmn bugisia, mutta ainakin
versiot 1.1 (bugikorjattuna!), 1.2 ja 1.3 ovat toimineet minulla hyvin,
joten joko Altavistaan hakusanalla Rhide, MBnettiin tai MB:n
H&H-rompulle.

Sitten kun Rhide toimii niin menette DJGPP:n BIN-hakemistoon ja
kirjoitatte "rhide rhide". Tm tarkoitus on luoda/muuttaa
BIN-hakemistossa olevaa rhide-nimist projektia, jonka asetukset
ladataan AINA kun rhide kynnistetn ilman projektia ja jotka
toimivat uusien projektien oletusasetuksina. Muuttele rhide-projektia
niin paljon kuin haluat/uskallat/viitsit ja lopeta sen jlkeen
rhide. Voit kokeilla viel asetusten toimivuutta menemll jonnekin
hakemistoon miss on jokin muu mr kuin yksi projekteja (jos niit
on vain yksi niin se ladataan automaattisesti) ja kynnistmll
Rhiden.

Nyt pitisi kaiken olla valmista uuden projektin teolle. Ota
Project-valikosta Open project ja kirjoita avautuvan ikkunan
Name-sarakkeeseen haluamasi projektin nimi. Ruudun alalaitaan avautuu
ikkuna joka kertoo projektin tiedostot. Aktivoimalla tmn ikkunan ja
painamalla insert-nappia (tai Project-valikosta Add item) saat
listty uusia tiedostoja. Kun olet valmis paina Cancel-nappia.
Tll tavalla list haluamasi tiedostot (lhdekooditiedostot, tosin
jos ehdottomasti haluat voit laittaa jonkin valmiiksi knnetynkin O-
tai A-tiedoston mukaan) projektiin.

Mukaan listtvi kirjastoja voit mritt Options-valikon
Libraries-kohdasta. Muista, ett tm hakee kirjastoja VAIN DJGPP:n
LIB-hakemistosta, ja ett kirjaston nimeen listn aina kntjn
toimesta eteen LIB ja loppuun .A, eli l kirjoita koko kirjaston
nime tyyliin LIBJOKIN.A, vaan JOKIN. Sellainen erikoisuus kyll
kntjst lytyy, ett ylipitkt (yli 5 merkki) kirjaston nimet
katkaistaan, joten IOSTREAM antaa tiedoston LIBIOSTR.A, eik
virheellist LIBIOSTREAM.A:ta (joka olisi siis liian pitk).

Kun olet tyytyvinen kaikkeen muuhun niin ota viel Project-valikosta
main targetname ja mrit kohteen nimi. Jos olet tekemss
niengine, niin sinulla on nienginen C-tiedostot projektissasi ja
kohteena (esim.) LIBSND.A. Jos taas teet C++ EXE:, niin sinulla on
C-tiedostot joita kytetn, kohteena (esim.) PLUSPLUS.EXE ja
mahdollisesti kirjastossa IOSTR ja jotain muuta. .A-ptteest Rhide
osaa automaattisesti knt archive-muotoisen tiedoston ja
.EXE-ptteest ajettavan. Muutkin voivat toimia (O ainakin), mutten
ole kokeillut koskaan, sill siihen ei yleens ole tarvetta.

Projektin kntminen onnistuu napilla F9, jolloin Rhide osaa
automaattisesti katsoa tiedoston pivyksist mitk tiedostot ovat
muuttuneita (lhteen pivmr uudempi kuin kohteen) ja knt nin
vain tarpeellisen. Aikaa sstyy ja hermoja samoin. Kntmisen
jlkeen hakemistostasi lytyy luultavasti kasa objektitiedostoja,
joita voidaan kytt myhemmin linkkauksessa (jos vastaava
lhdekooditiedosto ei ole muuttunut).

Sellaista tll kertaa. Aika perusasiaa ja itsekin pteltviss,
mutta joskus vain ky siten ettei jotain perusasiaa itse hoksaa, tai
ainakin sst aikaa kun ei tarvitse kaikkea kokeilla. Nyt hallussa
pitisi olla projektien teko Rhidell ja niiden toimimaan saaminen, ei
sen kummempaa tll kertaa. Voit jatkaa halutessasi seuraavaan jos
tuntuu ett osaat tmnkin kappaleen materiaalin.


6.4 Todellista guruutta - salaperinen make
-------------------------------------------

Make on kuin suoraan Unix-maailmasta tullut. Jos pelkk vilkaisu sen
info-sivuille (INFO MAKE) saa aloittelijan vapisemaan horkassa. Mutta
ei ht, min kvin siell ja selvisin elossa - tosin en ole en
ollut sama itseni sen jlkeen. Olen nimittin huomattavasti gurumpi
jlleen sill voin knnell projektini halutessani hienosti
komentorivilt automatisoituna. Ja se onnistuu maken
makefileill. Tss luvussa kerron miten niit tehdn, tosin en
mitn monimutkaisempaa valota kun mitn ihmekonsteja harvemmin
normaalissa perustyskentelyss tarvitsee.

Eli ensimmisen tehtvn on jlleen kaivaa make jostain, paikat ja
keinot ovat samat kuin Rhiden kohdalla, mutta toisin kuin Rhide maken
pitisi toimia ilman manuaaliin vilkaisua (koska se on huomattavasti
yksinkertaisempi systeemi). Ideana on tehd projektille ns. makefile,
jonka make osaa tulkita ja tehd sen mukaan tiedostossa ksketyt
asiat.

Mutta tehdksemme oikeanlaisia makefilej meidn tytyy ensin hieman
ymmrt filosofiaa maken takana.

Normaali makefile koostuu yleens alussa olevasta kasasta
muuttujamrittelyj, joita myhemmin kytetn kntmisess. Sen
jlkeen on kasa ohjeita, jotka koostuvat muutamasta
komponentista. Tss on ohjeen muoto ja esimerkki yhdest:

kohde: riippuvuudet
	komento kohteen tekoon

esim.

ohjelma.exe: ohjelma.o
	gcc ohjelma.o -o ohjelma.exe -s -Wall -v -O2

Eli ensimmisen on kohde joka kertoo makelle, ett tss on ohje
miten teet tmn. Sitten on riippuvuudet, joka kertoo, ett niden
pit olla kunnossa ennenkuin tt ohjetta aletaan
toteuttamaan. Seuraavalla rivill on yksi TAB:in painallus ja komento
jolla kohde tehdn (komentoja voi olla useampiakin, jokainen omalla
rivilln alkaen TAB:illa). Huomaa, ett tarvitsemme EHDOTTOMASTI
oikean TAB:in, emme mits MSDOS EDIT:in lelutabbeja, jotka eivt
itseasiassa ole kuin mrtty mr vlilyntej. Eli pit olla
jonkinlainen editori, joka osaa kytt aitoja TAB-merkkej.

En taida alkaa miettimn syvllisemmin maken toimintaa, mutta ideana
on, ett esittelet ensin pkohteen ja sen riippuvuudet ja sen jlkeen
esittelet nm uudet riippuvuudet ja niiden riippuvuudet jatkaen
pohjalle asti kunnes lopulta sinulla on kohteena objektitiedosto ja
lhteen lhdekooditiedosto ja alla komento tmn kntmiseksi,
jolloin make katsoo pivmrn mukaan tarvitseeko tm kohde
pivittmist. Jos lhde on uudempi kuin kohde niin ksky suoritetaan
mutta jos kohde on uudempi niin se on tydytty knt lhteen
muuttamisen jlkeen eik knt tarvita. Tll tavalla vain
muuttuneiden tiedostojen aiheuttamat knnstarpeet hoidetaan eik
ylimrist tyt tehd.

Yleens makefiless on ensin kohde all, jossa riippuvuuksina on kaikki
mit makefilen tulee saada tuloksena valmiiksi (EXE:t, kirjastot),
sitten on niden tuloksien ohjeet riippuvuuksina objekti- ja
archive-tiedostot, sitten archive-tiedostot riippuvuuksina
objektitiedostot ja lopuksi objektitiedostot riippuvuuksina
lhdekooditiedostot. Tss on esimerkki joka varmaan valaisee aika
sekavaa selitystni. =) Huomaa mys makrot, jotka mritelln alussa
ja joita muuttamalla on helppo vaihtaa knnksess tarvittavia
parametrej ja kntjien nimi:

CC=gcc
CFLAGS=-s -Wall

AR=ar
ARFLAGS=rs

all: esim.exe libx.a

esim.exe: esim.o libx.a
	$(CC) $(CFLAGS) esim.o libx.a -o esim.exe

libx.a: x1.o x2.o
	$(AR) $(ARFLAGS) libx.a x1.o x2.o

esim.o: esim.c
	$(CC) $(CFLAGS) -c esim.c -o esim.o

x1.o: x1.c
	$(CC) $(CFLAGS) -c x1.c -o x1.o

x2.o: x2.c
	$(CC) $(CFLAGS) -c x2.c -o x2.o

Kun tmn tiedoston tallentaa nimelle makefile tarvitsee sinun vain
antaa komento make niin ohjelma osaa automaattisesti knt kaikki
makefiless mritellyt tiedostot. Kyttksesi muita makefilen nimi
pit maken komentoriville antaa parametri -f<makefile>.

Esimerkki oli hyvin yksinkertaistettu ja vltin kyttmst paria
hauskaa kikkaa jotka tekevt makefilest paljon lyhyemmn (ja
sotkuisemman nkisen). Jos kuitenkin toiminta on epvarmaa, niin
selostetaan se tss viel kertaalleen:

1. Make aloittaa lausekkeesta all (komentorivill voit halutessasi
   mrt mik ohje tulee tehd, esim make libx.a ei koskisi esim.*
   -tiedostoihin) ja etenee tekemn esim.exe:.

2. Esim.exe:n teko tarvitsee ensin esim.o:n, siirrytn siihen.

3. Esim.o tarvitsee esim.c:n, mutta sille ei lydy ohjetta, joten
   suoritetaan ensimminen knns. Makrot CC ja CFLAGS puretaan
   komentoriville ja se suoritetaan ja kaiutetaan nytlle. Jatketaan
   esim.exe:n riippuvuuksien tutkimista.

4. Esim.exe:n teko tyss kun siihenkin pit tehd libx.a, joten
   siirrytn tekemn sit.

3. Libx.a:han pit olla x1.o ja x2.o, joten siirrytn niihin.

4. Riippuvuudelle x1.c ei ole ohjetta, joten suoritetaan x1.o:n
   komento (niss kohtaa olisi pivmrtarkistus, mutta koska
   noita objektitiedostoja ei viel ole olemassa niin...) ja palataan
   takaisin.

5. x2.o tehdn samaan tapaan kuin edellinen ja palataan libx.a:n
   pariin

6. Riippuvuudet kunnossa, tehdn kirjasto libx.a, palataan esim.exe:n
   kimppuun.

7. Esim.exe:n riippuvuudetkin ovat hanskassa, joten tehdn se ja
   palataan kohtaan all.

8. Libx:kin on tehty juuri, joten kaikki on valmista, poistutaan.

No niin, kyll toiminta varmaankin selvisi, ja jos ei niin paljon
pidemmt ja selvemmt tekstit lyt englanniksi komennolla info make
(no selvemmist en itseasiassa tied :).

Mutta make ei viel ole ohitse, en uskalla pst teit kappaleesta
ennenkuin osaatte tehd ohjeita jotka tekevt vaikka 30
objektitiedostoa kerralla, ne kun ovat kovin mukavia systeemej
verrattuna siihen ett joutuisit kirjoittamaan jokaista varten oman
ohjeen.

Ideana tss on ernlainen nimentydennys. Make osaa poistaa ptteen
nimest ja korvata sen toisella, jota ominaisuutta kytetn juuri
thn useiden samankaltaisten tiedostojen tekoon kerralla. Jos siis
sinulla on 10 objektitiedostoa ja jokainen knnetn
vastaavannimisest lhdekooditiedostosta (o1.o ja o1.c, o2.o ja o2.c
jne.), niin niiden knt onnistuu seuraavalla tyylill (aika maken
infoista pllitty ja suoraan knnetty tavaraa mutta who cares?-):

KOHTEET: KOHDE-PATTERN: RIIPPUVUUS-PATTERN ...

OBJECTS=object0.o object1.o object2.o object3.o object4.o object5.o 
	object6.o object7.o object8.o object9.o

$(OBJECTS): %.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

Eli ensimmisen tulee lista (OBJECTS) tehtvist kohteista, sitten
tulee %-merkki, joka esiintyy kohde-patternissa vain kerran, ja
maken infosivut kyttvt siit nime "stem". Tm vastaa mit tahansa
kohtaa yhden kohteen nimest, kaikki muut kohteen nimess (.o tss
tapauksessa) tytyy vastata tysin.

Jos siis kohteena olisi foo.o ja kohde-pattern olisi %.o, niin "stem"
(anteeksi minulla ei ole sanakirjaa ksill ;) saisi arvon foo. Jos
riippuvuus-pattern olisi %.c niin riippuvuus tlle tiedostolle olisi
foo.c. Ei mitn sen vaikeampaa, % on kuin DOS-maailman * ja
ensimmisen tulee lista tiedostoista (kuten hakemistolistaus), sitten
stemill varustettu patterni ja lopuksi riippuvuudet jotka
tydennetn sill mit stem vastaa.

Lisksi tytyy kiinnitt huomio merkkisarjoihin $< ja $@, joista
ensimminen korvataan riippuvuudella (tai riippuviiksilla jos niit on
useampia) ja toinen kohteen nimell. Mys muita vastaanvankaltaisia
lytyy, mutta ne eivt ole lheskn niin hydyllisi kuin nm kaksi.

Nill evill ainakin pitisi onnistua makefileiden teko aika
pitklle. Hyvi esimerkkej lytyy lukemattomista DJGPP-paketeista,
joissa kntminen hoidetaan makefileill. Makefilet ovat muutenkin
yleisin tapa levitt lhdekoodin kanssa softaa, harvemmin olen nhnyt
kirjaston knnst automatisoitavan Rhiden projekteilla. :)


6.5 Ammattimaista meininki - enginen teko
------------------------------------------

Tm luku kertoo hieman niist vhisist kokemuksista mit minulla on
ollut projektien kanssa, tai oikeammin kertoo mit kannattaisi ottaa
huomioon enginen teossa, jotta se toimisi mys huomenna ja jotta siit
jlkeenpin saisi jotain selvkin.

Nppr tapa pohjelman yksinkertaistamiseksi on tehd tietyn
tehtvn suorittavista tiedostoista yksi paketti, kirjasto jonka
headerin koodiin sisllyttmll voi kyseisen tehtvn hoitaa
kirjaston tarjoamilla rutiineilla. 

Sen lisksi ett tapa yksinkertaistaa koodia se mys parantaa sen
yllpidettvyytt huomattavasti ja myskin muunneltavuus on aivan eri
luokkaa kuin "kaikki-yhdess-kasassa" -ohjelmilla. Lisksi kun engine
on kerran valmis voi sit kytt uudelleen ja uudelleen - yleens
pienill muutoksilla tai parhaimmillaan muuttamattomanakin.

Mutta tllaisenkin teossa kannattaa huomioida joitakin asioita, jottei
jlkeenpin paljastuisi ett olet tehnyt turhaa tyt koko
ajan. Nimittin ensin on tarkoin otettava selv mit enginelt
vaaditaan ennenkuin sellaista alkaa tekemn. Hyv tapa on mietti
millaista peli on tekemss ja millaisia ominaisuuksia enginelt
vaaditaan. Matopelin teossa ei vlttmtt tarvita kovin kummoisia
jrjestelmi, sill ne eivt useastikaan vaadi kovinkaan monimutkaista
toimintaa hyvn jljen aikaansaamiseksi. Toisin on vaikka sivultapin
kuvatussa ammuskelupeliss, jossa spritejen piirron pit olla
rimmisen nopeaa ja turhaa piirtely tulee vltt. Skrollaus vaatii
mys tllaisissa peleiss tehoja ja muuttujia spriteihin tulee
huomattavasti enemmn kuin matopeliss.

Mikn ei voita kunnon suunnittelua kun koodausta sitten aletaan
tekemn. Hyvll onnella koko enginen teko on suoraviivaista koodin
kirjoittamista jos trkeimpi algoritmej on jo hahmoteltu paperilla
ja mieless on kunkin funktion toiminta ja tarvittavat muuttujat
kuhunkin tehtvn.

Kun tarpeet ovat vihdoin paperilla ja koodin kirjoitus edess voi olla
hyv viel etukteen nimet enginen lohkot ja nimet ne. Nppr tapa
jolla psee suoraan toimeen on kynnist vaikka Rhide ja lhte
lisilemn uuteen projektiin tiedostojen nimi. Tiedostoja ei
tarvitse edes olla olemassa vaan riitt ett hahmotat mit
jrjestelmn pit tehd ja minklaisiin osiin se pitisi
jakaa. Kaikkein kevyimmt enginet eivt edes paljoa tiedostoja tarvi,
nppishandleri ja timerhandleri, hiirirutiinit ja yksinkertaisemmat
grafiikkaenginet menevt ainakin thn kastiin. nienginet,
playerit ja 3D-enginet sek raskaammat grafiikkaenginet taas voivat
hyvinkin vied toistakymmentkin tiedostoa.

Hyvi jakotapoja on monia ja jrki varmaan sanoo, ett hyv jakotapa
ei ole aakkosjrjestys taikka pituusjrjestys. Hyv jakotapa voi olla
vaikka nienginen teossa ptiedosto sislten kynnistys- ja
lopetusfunktiot ja jonka .h-tiedostosta lytyvt keskeiset
datarakenteet, latausrutiinit sisltv tiedosto, universaali
efektinsoittorajapinta ja eri tiedostot jokaiselle nikortille,
modien lataus, modien soittorutiinit sisltv tiedosto jne.. Aivoja
saa, pit ja kannattaa kytt.

Trkeit suunnittelun kohteita on mys se miten ohjelma sil datansa
sek muistissa ett kovalevyll. Jo alussa fiksusti ja
laajennettavasti tehty rakenne on monta kertaa kyttkelpoisempi kuin
senhetkiseen tarpeeseen vstty kyhelm. Mys tallennus- ja
latausrutiinit kannattaa tehd erikseen eik pyrki tekemn mitn
purkkaviritelmi jotka kaatuvat vhintnkin kun haluat list uuden
ominaisuuden.

Hyv idea on mys tehd universaalit rutiinit virheist
ilmoittamiseen, muistin varaukseen ja vaikka tiedostojenkin
lukuun. Yleenskin enginen suurin osa tulisi sijoittaa keskivlille
muutaman kriittisten low-level -rutiinien jdess alapuolelle ja
ylpuolelle tuleva rajapinta ohjelmalle mahdollistaa enginen
muuttumisen radikaalistikin ilman muutoksia pohjelmaan. Low-level
-rutiinien siirto toisille nimille jo pelkill #define-lausekkeilla
(tyyliin "#define OmaFopen(a,b) fopen(a,b)") auttaa sen verran, ett
kun haluatkin muuttaa kaikki tiedostorutiinit pakattuja datatiedostoja
kyttviksi ei tarvitse muuttaa kuin pari kohtaa kaiken muun jdess
samanlaiseksi.

Kommentointi on elintrke engine tehdess, sill hyv engine voi
olla kytss pitknkin aikaa ja sitten kun se lopulta j ahtaaksi
voi huonosti kommentoineen kooderin peri hukka muuntelun
osoittautuessa mahdottomaksi yksinkertaisesti siit syyst ettei edes
tekijll ole en mitn aavistusta mit hnen koodinsa tekee. Hyv
ohjelmoija tekee sen verran lyhyit funktioita, ett niist saa selv
vhn tutkailemalla ja nime muuttujat ja funktiot kuvainnollisesti
sstelemtt turhaan nimen pituudessa (jrkevll tasolla kuitenkin,
mutta saa se nyt enemmn olla kuin Jdrwsprt()). Kun epselvemmt
kohdat viel kommentoi koodista pitisikin saada huomattavasti
paremmin selv.

Yksi hydyllinen asia voisi olla tiedostoja editoidessa kirjoittaa
tietty headeri jokaisen tiedoston alkuun. Hyvi voisi olla
copyright-ilmoitukset (joilla ei kyll omassa kytss tee mitn),
luontipivmr, viimeisen muutoksen pivmr ja muutoshistoria,
jonne kirjataan muutokset koodiin. Jlkeenpin ja bugeja etsiess
tuollaisesta on kummasti hyty, kun miettii mit onkaan tullut
lhiaikoina muunneltua.

Viimeinen asia mik koodissa pit viel huomioida on ne funktiot,
jotka tarjoavat rajapinnan, "kyttliittymn" engineen. Nm funktiot
ovat siis ne jotka tarjotaan engine kyttvlle ohjelmalle enginen
kyttn. Niden tulee olla tarpeelliksi kattavat jotta kaikkia
enginen ominaisuuksia voidaan halutessa kytt hyvksi. Hydyllist
on tehd Init- ja Deinit-funktiot, joita kutsutaan pohjelmasta
ohjelman kynnistyess ja siit poistuttaessa.

Mys funktioiden nimeminen erottamiseksi muista mahdollisista
samankaltaisista funktioista voi olla hydyllist. Kirjaston
funktioille ja globaaleille muuttujille voisi antaa jonkin etuliitteen
erottamaan ne muista ja huolehtimaan siit ettei kahdella funktiolla
ole samaa nime. Omassa grafiikkakirjastossani kytn JG-etuliitett,
jolloin funktioiden nimet ovat tyyliin JG_Draw, JG_Hide jne.. Mys
mahdollinen versionumero kirjastolle on ktev jos sit aikoo todella
kehitt kunnolla.

Sitten vain huolehtimaan siit ett enginest ei lydy
pullonkauloja. Helpointa lienee tehd enginen eniten tehoa vaativat
osat mahdollisimman nopeiksi, jolloin pohjelma on helppo tehd
korkean tason koodilla. Assembler-optimointikin voisi olla ihan kiva,
joten seuraavassa luvussa luulen ett selitn hieman sen lisilyst
DJGPP:n koodiin.

Tm luku ei nyt varsinaisesti opettanut mitn, mutta ainakin jotain
evst pitisi nyt lyty ensimmisen enginen tekoon. Katsotaan mits
thn nyt keksisikn seuraavaksi. =)


7.1 Vauhtia peliin - ulkoisen assyn kytt
------------------------------------------

No niin, assembler, tuo kielist jaloin nytt olevan tmnkertaisen
kiinnostukseemme kohteena. Vaan mik on tuo salaperinen kieli ja
miten sit kytetn. Se j ihan sinun itsesi selvitettvksi, mutta
voin kuitenkin antaa jonkinlaisia ohjeita jotta lytisit tiedon
lhteille. Ensihtn kannattaa hakea koneelleen ainakin seuraavat
opukset vaikkapa MBnetin ohjelmointialueen kautta:

ASSYT.ZIP: 
    Cyberdune (tjsp.) magazinen assykurssit kaikki samassa kasassa,
    suomeksi opettaa assemblerin perusasiat.

HELPPC21.ZIP + HPC21_P5.ZIP:
    HelpPC referenssiteos ja Pentium-update sislten mm. kaikki
    x86-prosessorikskyt, matikkaprossukskyt ja Pentiumin omat kskyt
    (kuten CMPXCHG8B tai jotain).

PCGPE10.ZIP:
    Assytutoriaali lytyy tltkin, tosin englanniksi.

3DICA*.ZIP:
    Sislt Henri Tuhkasen mainion assembler-optimointitutoriaalin.
    Ehdoton ensihankinta optimoinnista kiinnostuneelle.

Lisksi todella hyv kirja assyn opetteluun (ja ainoita suomeksi) on
kirja nimeltn 486-ohjelmointi. Tuota kaikki aina suosittelevat enk
itsekn voi kirjaa haukkua. Kirjastosta tuon saa viel kaiken lisksi
ilmaiseksi, vhintn kaukolainauksella.

Jos sinua ei assembler kiinnosta yhtn niin voit tietenkin hypt
tmn kappaleen yli, mutta varoituksen sana sit ennen: Jos aiot tehd
joskus nopean toimintapelin (lhiaikoina ainakin), niin tulet hyvin
luultavasti kaipaamaan assembler-osaamista. No tietenkin jos odottaa
tarpeeksi niin voi tehd kaiken vaikka Visual Basicin kasiversiolla,
mutta en minkn takaa ett pysyn myhemmin tutoriaalissa pelkss
C:ss. <grin>

Mutta sen jlkeen kun osaat assyn, niin alahan lukemaan pidemmlle,
sill ksittelen hieman C-kielisest ohjelmasta kutsuttavien
funktioiden tekoa assyll. En aio selitt sinulle mik on pino, sill
assyoppaista lytyy tuokin tieto. Muistiasi virkistkseni mainitsen
kuitenkin, ett tulee muistaa pinon kasvavan alaspin, eli jos haluat
varata pinosta 16 tavua niin sinun tulee vhent esp:st (extended
stack pointer) 16 tavua, ei list! Palautus taas hoituu lismll.

Eli hieman tietoa siit miten C-kielinen ohjelma kutsuu funktiota ja
mit se tekee sinun palattuasi. Eli kutsuessaan funktiota C-kielinen
ohjelma ensin pushaa parametrit pinoon lhtien parametrilistan
oikeasta laidasta ptyen lopulta ensimmiseen parametriin ja sitten
se heitt ebp:ns pinoon, kopioi ebp:n esp:hen ja lis siihen itse
kyttmns muistin mrn (eli itseasiassa vain varmistaa ett esp
osoittaa pinon plle) ja kutsuu funktiota kytten call-komentoa,
joka viel kaiken huippuna heitt senhetkisen eip:n (extended
instruction pointer) pinoon.

Huomaamme, ett kun suoritus alkaa omasta funktiostamme on asioiden
laita seuraava:

Pino sislt indeksiss 0 pinon huipun, eli tll hetkell kutsuneen
ohjelman eip:n. Sen jlkeen on ensimminen parametri, sitten toinen
parametri jne.. Mutta koska meidn tytyy aluksi tallentaa ebp pinon
plle pushaamalla se huipulle, jolloin tiedmme, ett parametrit ovat
kahden kaksoissanan (ebp ja eip), eli 8 tavun pss. Tss funktion
tarvitsema alustuskoodi:

push ebp
mov  ebp, esp

Lisksi on mahdollista varata pinosta muistia haluttu mr
vhentmll esp:t,jolloin siihen j aukko jonka alussa ebp
on. Muista kuitenkin vapauttaa muisti korottamalla esp:t. Muista
lisksi, ett koska pino menee alaspin, niin varattu muisti sijaitsee
mys esp:st alaspin, eli negatiivisiss offseteissa.

Sen jlkeen vain osoitellaan parametrej. Ensimminen parametri on
siis nyt kohdassa ebp+8 (koska kopioimme ebp:hen esp:n, jossa pino
oli), ja parametrit seuraavat jrjestyksess 4 tavun vlein
riippumatta parametrin koosta, DJGPP net sijoittelee mys nuo
mahdollisimman hyvin, toisin kuin aiemmin luulin.

Koko roska on itseasiassa hemmetin vaikea ymmrt ja olen tunnin ajan
loikkinut ympri kovalevyni etsimss tarkennuksia pinon toimintaan
ja miten C-funktiota itse asiassa kutsutaan, sill en ole koskaan
ottanut viimeisen plle selv kntjn sielunelmst.

Piirrn nyt pikkaisen kaavion siit mit tietkseni muistista lytyy
sen jlkeen, kun funktiota void func(short,long) on kutsuttu, ebp on
pushattu ja esp siirretty siihen ja pinosta varattu muistia 2 tavua:

          C     B           A                              
 ----------------------------------------------------------------
  RR RR RR MM MM BP BP BP BP IP IP IP IP 11 11 -- -- 22 22 22 22 
 ----------------------------------------------------------------

A) Ohjelmaan tullessa ESP osoittaa thn
B) Kun EBP on pushattu niin ESP osoittaa thn, samoin EBP kun ESP on
   ensin siirretty mys EBP:hen. Huomaa EBP:n ja EIP:n sijainti
   kohdasta B nhden ja parametrin 1 sijainti offsetissa 8 (viivat
   ovat kyttmttmi palasia), sek
   parametrin 2 sijainti offsetissa 10 (parametrin 1 koko on short,
   eli 2 tavua!)
C) Kun ESP:t vhennetn kahdella jotta pinosta saadaan ohjelmalle 2
   tavua muistia on meill nyt kaksi tavua muistia kytss alkaen
   offsetista EBP-2. ESP osoittaa tmn muistin alkuun, mutta
   pushailun sattuessa se lhtee vaeltelemaan yh kauemmas vasemmalle.

Palautuksessa poppaillaan kaikki, jolloin ESP on taas kohdassa C. Sen
jlkeen vapautetaan pino vhentmll ESP:t kahdella, jolloin ESP ja
EBP ovat jlleen samoja, eli kohdassa B molemmat. Nyt viel popataan
EBP, jolloin EBP on alkuperisess tilassaan, samoin kuin ESP, joka
osoittaa EIP:n kohdalle. Nyt vain ret, joka ottaa EIP:n pinosta ja
palaa thn osoitteeseen.

JES! TEIN SEN! (anteeksi tunteenpurkaus mutten uskonut saavani tt
itsekn selville ilman kenenkn apua ;)

Huomaa, ett on aina kutsuvan ohjelman vastuulla pit rekistereistn
huolta ja puhdistaa parametrit pinosta, jotka sinne on pitnyt
pushailla ennen ohjelman kutsua (niit ohjelma ei palauta).

Tss nyt tm lopullinen assyosuus, joka pit olla alussa ja
lopussa:

push	ebp
mov	ebp, esp
sub     esp, <pinon koko>

<koodia>

add	esp, <pinon koko>
pop	ebp
ret

Toisen C-funktion kutsu taas onnistuu seuraavasti, otetaan esimerkkin
vaikka foo(int,short,char,int):

push	<int>
push	<char>
push	<short>
push	<int>
call	_foo
add	esp, 11

Nuo <int>-hommat siis tarkoittavat oikeankokoisia rekisterej tai
muistialueita. Huomaa mys lopussa esp:n palautus korottamalla sit
parametrien yhteenlasketun koon verran. Huomaa mys, ett C lis
assykoodiin aina yhden alaviivan lis, eli omien rutiiniesi
funktionimien edess pit ASM-tiedostossa olla aina yksi alaviiva
enemmn kuin mit C-kielisess. Mys C-kirjaston funktioita kutsuessa
pit muistaa, eli _printf, _puts jne.. Funktioille joiden nimiss on
C:llkin yksi tai useampia alaviivoja suoritetaan vain yhden alaviivan
eteenlisys.

No niin, nyt menee kaikki muu funktioissa, mutta viel palautus ja
structit sek reaaliluvut. No tss kaikki vh mit min siit
tiedn:

Pointtereiden ja dword (4 tavua siis) kokoisten kokonaislukujen
palautus EAX:ss. Sanojen (2 tavua, word) palautus AX:ss ja tavujen
palautus AL:ss. Reaaliluvut matikkarekisteriss ST[0]. Structeista
minulla ei ole aavistusta, sill olen kyttnyt helpompaa ja yleens
hydyllisemp tapaa vlitt ne vain structin osoitteina.

Reaaliluvut annetaan parametrein tietkseni ihan samoin kuin muutkin
parametrit.

No mutta. Kaikki tietvt nyt miten varata muistia, kutsua funktioita,
palauttaa tietoja, kytt parametreja. Mutta trkein puuttuu, sill
kukaan ei osaa tehd tiedostoja jotka voisi linkata DJGPP-ohjelman
mukaan. Siisp tihin!

Jotta objektitiedoston voisi linkata mukaan DJGPP-ohjelmaan tyty sen
olla oikeaa formaattia. DJGPP:n hyvksym formaatti tunnetaan nimell
COFF (ei kaljaa!), eli common object file format. Ainoat
kyttmistni assembler-kntjost jotka tuota tukevat ovat as ja
NASM. As on GNU assembler ja sislt TODELLA kryptisen nkist AT&T
assembleria kntvn yksikn. Mutta kerron jo etukteen, ett
AT&T-formaatti, jota DJGPP kytt itse sen Unix-taustan takia on
aivan toisen nkist kuin Intel-syntaksin assy, joten suosittelen,
ett ette kyt sit (halukkaat imuroivat tiedoston DJTUT255.ZIP)!

Paljon parempi kntj on nimeltn Netwide Assembler, lyhyesti NASM,
jonka lyt ainakin MBnetist ja tietenkin Internetist. Nimi on
NASM094B.ZIP, mutta voi kyll olla ett uudempiakin on
ilmestnyt. Jokatapauksessa kntj on aivan loistava ja sen kyttkin
on suhteellisen yksinkertaista. Kaikkein parhaiten sen kytn oppii
lukemalla NASM.DOC lpi ja tutkailemalla esimerkkikoodeja (etenkin
AOUTTEST.ASM!) hakemistosta TEST. Mutta niille jotka eivt mielelln
lue englantia on ihan pikkuinen esimerkkisorsa, jolla psee nyt
ainakin alkuun siihen asti, ett kunnon sanakirja tai tulkkaava kaveri
lytyy:

TEST.ASM:

BITS 32

EXTERN _cfunktio
EXTERN _cmuuttuja
GLOBAL _asmmuuttuja
GLOBAL _asmfunktio

SECTION .text

; int asmfunktio(int)

_asmfunktio:
	push	ebp
	mov	ebp, esp
	
	mov	eax, [ebp+8]
	add	[_asmmuuttuja], eax

        push	eax
	call	_cfunktio
	add	esp, 4

        mov	eax, [_asmmuuttuja]
	pop	ebp
	ret

SECTION .data

_asmmuuttuja	DD 0


TEST.H

extern int asmfunktio(int);

void cfunktio(int);

int cmuuttuja;


TEST.C

#include <stdio.h>

void cfunktio(int luku) {
    puts("kutsuttiin C-funktiota parametrilla %d\n", luku);
}

int main() {
    printf("asmfunktio(10) palautti arvon %d\n", asmfunktio(10));
    printf("asmfunktio(20) palautti arvon %d\n", asmfunktio(20));
    printf("asmfunktio(5) palautti arvon %d\n", asmfunktio(5));
    printf("asmfunktio(2) palautti arvon %d\n", asmfunktio(2));

    return 0;
}

H-tiedoston ja C-tiedoston varmaan ymmrrtte, mutta selvennyksen
viel assysuudesta, ett ensin asetetaan NASM 32-bittiseen
koodinknttilaan, sitten mritelln ulkoiset muuttujat _cmuuttuja
(kaksoisasna) ja _cfunktio (kaksoissana sislten rutiinin
osoitteen). Sitten koodisegmentiss (.text) on _asmfunktio, joka tekee
kuten aiemmin neuvottiin, eli tallettaa ebp:n ja kopioi esp:n
ebp:hen. Sen jlkeen se korottaa _asmmuuttuja -muuttujaa parametrill
ja kutsuu viel _cfunktio -funktiota parametrill palauttaen lopuksi
_asmmuttuja:n arvon. Datasegmentiss on varattu _asmmuuttuja
-muuttujalle tilaa kaksoissanan verran ja alustettu se nollaksi.

Sitten vain tutkimaan antaako ohjelma oikean tulosteen. En minkn
tied mutta menen katsomaan. =) Toimi ainakin minulla. Jaa ett se
kntminen NASM:illa?-) No se on tietenkin komennolla:

nasm -o jokin.o -f coff jokin.asm

No niin, nyt sinun pitisi hallita assemblerin kytt C:n kanssa
jotakuinkin vltten ja nasmilla kntelykin pitisi onnistua, sek
nasm-tiedostojen tekokin ainakin rajoitetusti. Pahoittelen ett
tarkempia ohjeita ei annettu, sill ne olisivat olleet niin pitkt,
ett katsoin oppimisen onnistuvan ilman tarkempia ohjeita. Mutta jos
kuitenkin tuntuu, ett tmn kappaleen taso leijui kilometritolkulla
tajuntasi ylpuolella niin pyydn ottamaan yhteytt, sill en
ihmettele vaikka tm olisikin vaikein osa thn asti ja kaikki apu
sen suhteen miten tt pitisi parantaa on tarpeen.

Mutta toisaalta jos et assy muuten osaa etk ole kaikkea
dokumentaatiota kaivanut esiin mit lydt voi olla ett asia on
paljon selkempi jo muutaman pivn pst. Jos ei kuitenkaan helpota
niin heit viesti tnnekin pin. Mutta nyt jatkan taas kohti uutta
tuntematonta.

Phew, tmhn ky tyst kun koko pivn kirjoittaa!


7.2 PIT - aikaa ja purkkaa
--------------------------

Hiphei taipaleemme jatkuu edelleen, vaikka kello osoitteleekin
kirjoitushetkell melkein kahtatoista. Mys ihmeellisest tekstist
voinee sen ptell etten ole vlttmtt aivan parhaimmillani ja
tervillimmillni (villimmillni?) thn aikaan pivst. No, tehn
siit vain krsitte, en min, joten jatkakaamme! ;)

Eli ihmeellinen lyhenne PIT? Mist se tulee? No tietenkin sanoista
Programmable Interval Timer, eli ohjelmoitava keskeytysajastin. Tm
on tllainen hauska piiri PC:ll, joka kykenee generoimaan ties mill
tavalla keskeytyksi. Kiinnostavaa ja tarkkaa tietoa lytyy PCGPE:st
(PCGPE10.ZIP) tiedostosta PIT.TXT, mutta me keskitymme vain
olennaiseen, nimittin systeemin omaan kelloon, keskeytykseen
8. Kerron kuitenkin hieman mill tavalla piiri laskee milloin pit
generoida keskeytys 8, ennenkuin psemme hauskaan tavaraan (eli
esimerkkikoodiin ;).

Eli PIT tikitt 1193181Hz:n taajuudella, eli suomeksi 1193181 kertaa
sekunnissa. Joka kerta se esim. vhent kanavan 0 laskuria yhdell ja
jos se on 0 niin se generoi keskeytyksen ja asettaa uudelleen laskurin
haluttuun arvoon ja lhtee laskemaan alaspin. Laskuri on kahden
tavun, eli yhden sanan mittainen ja kykenee ninollen vastaanottamaan
luvun vlilt 0-65335. Mutta erikoisuutena on se, ett jos laskurin
alustusarvo 0 ei tarkoitakaan ett keskeytyst kutsutaan jatkuvalla
sytll, vaan ett sit kutsutaan 65536:n "tikahduksen" (ei nin
myhn oikein sanat muistu mieleen) jlkeen. Normaali systeemikello
on asetettu thn kutsuntatiheyteen, eli sit kutsutaan
1193181/65536=n. 18.2 kertaa sekunnissa.

Jos siis koukutamme tmn keskeytyksen kuten olemme aiemmin tehneet
nppiskeskeytyksellekin tulee alkuperist kutsua thn tahtiin, sill
toisin kuin nppiskeskeytys, kellokeskeytys on huomattavasti
trkemmss asemassa eik sit voi hypt noin vain yli (ainakin
DOS:in kello pyshtyy koko ajaksi =). Jos me siis koukutamme
keskeytyksen tulee sen olla tmntyylinen:

funktio kellokeskeytys

    <tee jotain>

    laskuri = laskuri + tikkej_per_kutsu;

    jos (laskuri on suurempi tai yhtsuuri kuin 65536)

        laskuri = laskuri - 65536

        kutsu_vanhaa();

    muuten

        kuittaa_keskeytys();

    end jos

end funktio

Tikkej_per_kutsu on siis uusi mr tarvittavia tikkej jokaisen
keskeytyksen vliss. Jos vaikka haluaisimme ett omaa kelloamme
kutsutaan 100 kertaa sekunnissa, niin meidn pitisi asettaa PIT:ille
laskurin alustusluvuksi 1193181 / 100 = n. 11931. Sitten vain joka
kutsulla listn laskuria sen mukaan montako tikki on kulunut
edellisest vanhan kellon kutsusta ja jos se on alkuperinen 65536 tai
suurempi, niin vhennetn siit tm luku ja kutsutaan vanhaa
keskeytyst. Jos se on viel alle 65536, niin lhetetn tuttuun
tapaan tavu 0x20 porttiin 0x20.

Kellokeskeytyksen <tee jotain> -kohdan voi ja kannattaakin yleens
korvata laskurilla, jota korotetaan jatkuvasti. Tt voi kytt
vaikka ajanottoon tai muuhun hydylliseen, kuten nemme myhemmin.
Kaikki tuntuisi olevan toteutusta vailla - MUTTA.

Ongelmaksi muodostuu vanhan kutsuminen. Kun keskeytys generoidaan niin
senhetkinen koodisegmentti ja -osoitin (eli CS+EIP) kipataan pinoon,
samoin kuin liput ja kutsutaan ksittelij. Vastaavasti iret
keskeytysksittelijn lopussa ne otetaan sielt pois ja niiden avulla
palataan jatkamaan keskeytynytt ohjelman suoritusta samasta tilasta.

Mutta kun kutsumme vanhaakin ksittelij vliss, niin pinosta pois
otto tapahtuu kahdesti, mik eteen? Selv on, ett ohjelma kaatuu jos
ei tt ongelmaa korjata. Mutta htiin saapuu Kaj Bjrklund uljaalla
inline assembler-ratsullaan pelastaen meidt pulasta! Meidn tarvitsee
vain kellokeskeytyst asetettaessa ottaa talteen alkup. handlerin
koodiselektori ja offsetti sek tallentaa ne 64-bittiseen muuttujaan
(long long). Sitten vain kytetn seuraavanlaista inline-ptk:

__asm__ __volatile(
"pushfl
 lcall %0
" 
: 
: "g" (oldhandler));

Edellinen koodinptk tekee samat temput ennen funktion kutsumista
kuin mit sanoin normaalisti tehtvn, eli heitt liput pinoon ja
lcall pist sinne CS:n ja EIP:nkin, joten iret vanhassa
timer-rutiinissa palaakin omaan koodiimme ja kaikki toimii hienosti,
kun if...else huolehtii siit ettei outata kahdesti porttiin 0x20!
Hienoa! Nyt meill onkin oikeastaan kaikki tarvittava tieto handlerin
tekoon:

#include <dos.h>
#include <dpmi.h>
#include <go32.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/nearptr.h>

_go32_dpmi_seginfo info;
_go32_dpmi_seginfo original;

volatile long long OldTimerHandler;
volatile int TicksPerCall, OriginalTicks, Counter;

static volatile void TimerStart() {}

void TimerHandler() {
    Counter++;
    OriginalTicks+=TicksPerCall;
    if(OriginalTicks>=65536) {
        OriginalTicks-=65536;
        __asm__ __volatile__ ("
            pushfl
            lcall %0
        "
        :
        : "g" (OldTimerHandler));
    } else {
        outportb(0x20, 0x20);
    }
}

static volatile void TimerEnd() {}

void SetTimerRate(unsigned short ticks) {
  outportb(0x43, 0x34);
  outportb(0x40, ( ticks & 0x00FF ) );
  outportb(0x40, ( ( ticks >> 8 ) & 0x00FF ) );
}

void InitTimer(int tickspersecond) {
    __dpmi_meminfo lock;

    lock.address = __djgpp_base_address + (unsigned) &TimerStart;
    lock.size = ((unsigned)&TimerEnd - (unsigned)&TimerStart);
    __dpmi_lock_linear_region(&lock);

    Counter=0;
    OriginalTicks=0;
    TicksPerCall=1193181/((unsigned short)tickspersecond);
  
    disable();

    _go32_dpmi_get_protected_mode_interrupt_vector(0x0008, &original);

    OldTimerHandler=((unsigned long long)original.pm_offset) +
                    (((unsigned long long)original.pm_selector)<<32);

    info.pm_offset=(unsigned long int)TimerHandler;
    info.pm_selector=_my_cs();
    _go32_dpmi_allocate_iret_wrapper(&info);

    SetTimerRate(TicksPerCall);
    _go32_dpmi_set_protected_mode_interrupt_vector(0x0008, &info);
    enable();
}

void DeinitTimer() {
    disable();
    SetTimerRate(0);
    _go32_dpmi_set_protected_mode_interrupt_vector(0x0008, &original);
    enable();  
}

Muu mennee ihan hyvin tajunnan perlle asti, mutta InitTimer-rutiinin
alku voi hyvinkin tuottaa ihmettely, samoin kuin kaksi tyhj
funktiota kummallakin puolella TimerHandler-rutiinia. No minps
kerron mist on kyse. Kyse on muistin lukitsemisesta, kuten ehk
komentojen nimist voi ptell. Normaalisti DPMI-palvelin (jos se
siihen kykenee) voi swapata levylle koodia ja dataa jos silt tuntuu,
mutta kun muistialue lukitaan niin sit ei swappaillakaan
minnekn. l huoli jos epilet ettet olisi osannut noita tehd itse,
sill minkin varas- kytin apunani libc:n lhdekoodeista lytyv
koodinptk ja Kaj Bjrklundin esimerkkikoodia.

No nyt vain sitten esimerkkiohjelma, joka nytt hieman mihin
timer-rutiini pystyy:

#include <stdio.h>
#include <conio.h>

extern void InitTimer(int);
extern void DeinitTimer();
extern volatile int Counter;

int main() {
    InitTimer(100);
    while(!kbhit()) {
        printf("Counter=%d\r", Counter);
        fflush(stdout);
    }
    getch();
    DeinitTimer();

    return 0;
}

Nin. Seuraavassa luvussa esittelen ennen nukkumaanmenoani (ellei joku
tule ajamaan minua unten maille ennen kuin ehdin kirjoittaa seuraavan
luvun =) kiinnostavaa kyttkin tlle, joten pysyk kanavalla!


7.3 Miten peli toimii yht nopeasti kaikilla koneilla
-----------------------------------------------------

No thn on useita tapoja, mutta lhes kaikissa tarvitaan ajanottoa ja
ninollen edellisen luvun ajastinrutiini pohjusti varsin mukavasti
tmn luvun aihetta (josta tulee luultavasti todella lyhyt). Idea on
siis se, ett jokaisella koneella peli pyrisi yht nopeasti. No
helpommin sanottu kuin tehty.

Varmastikin kytetyin ja toimivin on menetelm, jota kutsutaan
hienosti termill "frameskip", eli kuvien yli hyppiminen. Ilkka
Pelkonen kytti siit brutaalia termi harppominen, mutta koska
minulle tulee siit mieleen vain pitkjalkaiset laihat
kumisaapasjalkaiset miehet niin kytn englanninkielist termi
(Ilkka, kyll min kyttisin edes "loikkimista", siit tulee edes
kengurut mieleen ;).

Eli idea on, ett kaikki muu tehdn joka framelle, mutta piirtminen
jtetn vliin jos ollaan "aikataulusta jljess". Niinp kun meill
on nyt ajastinrutiini voimme kytt tllaista systeemi:

plooppi

    ksitteleframe

    vhenn timerlaskuria

    jos timerlaskuri = 0

        piirr

    tai jos timerlaskuri < 0
      
        odota kunnes timerlaskuri >= 0

end plooppi


Eli itseasiassa edellisen luvun laskuria vhennetn itse peliss koko
ajan pyrkien pitmn se nollassa, mutta jos piirron aikana on ehtinyt
menn useampi frame sivu suun niin ksitelln framea ja vhennetn
timerlaskuria niin kauan ett ollaan taas saatu "kiinni" oikea tahti
ja voidaan pivitt seuraava ruutu. Mys toinen mahdollisuus, eli
"ylinopea" kone tytyy huomioida odottelemalla jos pyyhkistn jo
aikataulusta ohitse.

Valitettavasti tll alle 18.2:n framen nopeudet eivt toimi, joten
sellaisiin tapauksiin pit kehitell erikoisratkaisuja (fixed-point
-laskuri esimerkiksi, joka korottuu vain puolella joka vuoro tms.).

On mys muita mahdollisuuksia toteuttaa frameskip, kuten siirtmll
ksitteleframe -funktio suoraan timeriin, joka ei tosin mielestni ole
hyv ratkaisu, mutta joka toisaalta on tietyll tavalla selv
laskurien jdess pois. Mutta mit teetkn kun kesken ruudulle
piirron pivitetn aluksien paikkaa? Ei ole en kenestkn,
(varsinkaan pelaajasta) kivaa siin vaiheessa.

Toinen paljon toimivampi vaihtoehto on kytt kulunutta aikaa
iknkuin kertoimena tehtviss. Eli jos vaikka joka vuorolla pit
siirt sprite 1 eteenpin, niin siirretn joka framella sprite
1*kulunut aika verran eteenpin. Tm valitettavasti vaatii paljon
tihemmn ajastimen kutsun kuin sellaiset 70 kertaa sekunnissa
toimiakseen hyvin ja lisksi fixed-point -matikka on yleens aika
vlttmtn tmnkaltaisessa toteutuksessa.

Mutta, aihe ei ole vaikea ja varmasti osaat ptt minklaisen
toteutuksen teet itse peliisi. Min hivyn nukkumaan ja jtn sinut
oman onnesi nojaan. it!


7.4 Yleist asiaa pelin levityksest
------------------------------------

Tss luvussa olisi tarkoitus hieman valaista pelinteon toista puolta,
eli sen joka ei sisll ohjelmointia, vaan dokumentaation kirjoitusta,
grafiikan piirtoa, musiikkia, pelin levityst ja ties mit. Taidan 
kyll olla aika turha tst kauheasti puhumaan, sill valmiiksi emme
ole saaneet kuin vasta yhden pelin ja toinen on kovasti tekeill, kunhan
laiska kooderimme (min) lytisi jostain aikaa kirjoittaa koodia.

Ensimminen homma pelin teossa olisi varmaan ptt mink tyyppisen
pelin tekee. Parasta on valita sellainen pelityyppi, jonka uskoo 
pystyvns toteuttamaan. Ensimmisen projektina kannattaa varmaan 
tehd jokin yksinkertainen kaksiulotteisen toimintapelin, vaikkapa 
sitten sen inikuisen matopelin. Sitten vasta pikkuhiljaa kun kokemusta
kertyy niin kannattaa jatkaa vaikeammilla projekteilla.

Nimi lienee toinen huolenaihe kun pelityyppi ja sen ppiirteet ovat
tiedossa. l mielelln nime ohjelmaa samannimiseksi kuin jokin
olemassaoleva tuote. Esimerkiksi matopeli, jonka nimi on Windows voi
aiheuttaa liev nr jos se levi laajemmalle (tosin yleens 
ensimminen peliprojekti ei levi kauhean laajalle, mutta mist sit
koskaan kuitenkaan tiet).

Sen jlkeen olisi varmaan parasta alkaa pelin teko. Sen lisksi, ett
pelin engine tytyy saada kuntoon olisi mys hyv tehd siihen 
grafiikkaa. Musiikki ja niefektitkin olisivat varsin mukava idea,
jos kunnianhimoa lytyy tarpeeksi. Levityksess on useita
nikirjastoja, jotka tarjoavat enemmn tai vhemmn toimivan
ratkaisun niongelmiin. Enginen lisenssit kannattaa varmaan kuitenkin
tarkistaa hieman tavallista tarkemmin, kun joidenkin mukana tuppaa
olevan varsin kirjavia kyttehtoja (suurin osa kielt kaupallisen
kytn).

Jos grafiikka tai musiikki ei itselt suju on tietenkin mahdollista
hankkia joku kaveri tai vaikka aivan tuntematonkin mukaan projektiin
tekemn grafiikkaa ja sveltmn musiikkia. Pelin ollessa sitten
muiden osien osalta kasassa alkaakin kannattaa miettimn levityst
ja dokumentointia, jotka ovat muun kokonaisuuden kanssa mys trkeit.

Normaalisti kytettyj levitystyyppej on kolme, PD, FW ja SW (ja
tysin kaupallinen levitys, mutta tt tutoriaalia ei kyll sellaisen
tekijille ole tarkoitettu). PD (Public Domain) tarkoittaa, ett
luovut kaikista oikeuksistasi ohjelman suhteen, eli muut saavat 
tehd ohjelmallasi mit ikin keksivt, vaihtaa nimen ja levitt tai
myyd miten haluavat.

Hieman rajoitetumpi muoto on FW (FreeWare), jossa pidt
tekijnoikeutesi tuotokseesi ja saat itse sanella ehdot miten sit
levitetn. FreeWare -tuotteista ei kuitenkaan saa peri mitn (sill
se on termi jota kytetn ilmaisesta tuotteesta). SW (ShareWare) taas
on levitystyyppi, jossa kyttj saa kokeilla ohjelmaa tietyn ajan ja
sitten vasta ptt mit tekee ohjelman kanssa.

Ensimminen tehtv pttesssi minktyyppinen ohjelmastasi tulee
on mietti mihin ohjelmasi pystyy. Jos tuotos on ensimminen pelisi
ja harjoitusty voi hyvinkin olla jrkev antaa koko ohjelma 
lhdekoodeineen muiden levitykseen. Tllaiset julkistukset ovat aina
harvinaisia ja ohjelmasi ehk levi tll tavoin paremmin. Jos
olet kuitenkin sit mielt, ett et halua muiden kyttvn pelisi
miten haluavat kannattanee levitysmuodoksi laittaa FW.

Sharewarena tuotetta kannattaa levitt vasta jos todella olet panos-
tanut siihen vain siin mieless, ett saat siit rahaa tai jos olet
sit mielt, ett ohjelmasi on merkittvsti parempi kuin kilpailevat,
kaupalliset tai SW-tuotteet. Shareware-ohjelmaksi ei kuitenkaan
kannata laittaa sit ensimmist matopeli tai jotain bugista
viritelm, jos haluaa silytt maineensa. =)

Sharewareakin on kolmea tyyppi, nimittin tiukka aikarajoitettu
shareware, aikarajoitettu shareware ja rajoittamaton shareware. Tiukka
aikarajoitettu SW on tyypillisesti kuten kaikki mahdolliset Windows-
viritellyt HTML-editorit, joissa 99% on jokin viritelm, joka terminoi
ohjelman ennemmin tai myhemmin (yleens ennemmin). Lysemmsti
aikarajoitetut ohjelmat ovat siit kiitollisia, ett niiden toimivuus
silyy aikarajan jlkeenkin. Rajoittamattomat ovat sitten tietenkin
ne kaikkein mukavimmat ja niiden toimintaperiaate ei en yleens
olekaan antaa kyttjn kokeilla ohjelmaa, vaan rahaa pyydetn siit,
ett kyttj ottaa kyttns kaikki ohjelman toiminnot.

Jos peli on aivan ehdottoman huippu niin voi yritt levitt sit
tysin kaupallisesti, mutta se vaatiikin sitten yleens aika lailla
kokemusta ja tietenkin hieman hyv tuuria.

Sama mink tyypin levitykseen sitten ptyy, niin kannattaa varmaan
kirjoittaa hieman teksti, jossa kerrot miten haluat ohjelmaasi
levitettvn. PD-tyypill et tarvitse ehk kuin tekstitiedoston, jossa
ilmoitat luopuvasi kaikista oikeuksista ja kaikesta vastuusta ohjelman
suhteen. SW:n ja FW:n kanssa kannattaakin sitten panostaa
lainopilliseen puoleen hieman enemmn.

Trkein on ilmoittaa selvsti peliss, ett tekijnoikeudet kuuluvat
sinulle tai useammalle henkillle ja kertoa ehdot joiden rajoissa
ohjelmaa saa levitt. Trkein rivi lienee tmnkin dokumentin alusta
lytyv:

Copyright (C) Joonas Pihlajamaa 1997. All rights reserved.

Tekijnoikeuksien merkki, (C) ei toistu oikein tietokoneella, sill
sen pitisi itseasiassa olla ympyrn keskell oleva C. Ninollen
Copyright-teksti alussa voi olla varsin hydyllinen. Sen jlkeen tulee
tekijn nimi ja loppuun yleens vuodet joiden aikana olet tuotteen
tekijnoikeuksia pitnyt hallussasi (eli kytnnss min aikana olet
peli tehnyt). Jos tekijit on useita kannattaa varmaan pelata varman
plle ja selitt tarkemmin ketk henkilt ovat tehneet mitkin.

Sitten vain pern kaikki ehdot, joita haluat pelisi levitettess
noudatettavan. Suhteellisen kattava vastaava lytynee suomeksi
tmnkin dokumentin alusta (porsaanrei'ist saa kyll vapaasti
ilmoittaa ;), sek kotisivujeni levitysehdoista, joiden parissa vietin
runsaasti aikaa pyrkien saada siit niin vaikean kuin mahdollista.

Jos noiden tekeminen tuntuu turhalta, niin kannattaa muistaa, ett jos
joskus satut joutumaan kahnauksiin ohjelmasi vrinkytsten tai sen
aiheuttamien ongelmien kanssa, niin tuo teksti saattaa olla ainoa
apusi. Ilman teksti on paljon hankalampaa sanoa oikeudessa, ettet ole
vastuussa ohjelman aiheuttamasta sydmentahdistimen pyshtymisest,
toisin kuin jos olisit kirjoittanut ehtoihin, ett et ole vastuussa
moisista vahingoista.

Mit sinun tulisi ilmoituksessa mainita olisivat seuraavat:

0. Mihin kaikkeen ehdot ilmoituksessa ulottuvat
1. Miten ohjelmaa ja sen tiedostoja saa kytt
2. Mist olet vastuussa
3. Mit tehd jos ei suostu ehtoihin ja milloin katsotaan kyttjn
   suostuneen niihin
4. Miten ohjelmaa saa levitt
5. Miss ohjelmaa saa levitt

Kun lainopillinen puoli ja itse peli on kunnossa lienee jljell vain
levityspuoli. Se onkin suhteellisen helppoa. Lhetys pariin suosittuun
purkkiin (MBnettiin =) ja kenties Internetiinkin lis varmasti
levimist aivan toisin kuin kavereille antaminen. Mainostustakin voi
harrastaa, mutta kannattanee pit se kohtuullisissa rajoissa, ettei
ohjelmasi saa negatiivista julkisuutta hiritsevst mainonnasta. :)

Jos ohjelmasi on SW-tuote, niin lienee viel yksi kohta, nimittin
rekisterinnit ja pivitykset, sek mahdollisien lisominaisuuksien
"vapautus" (enabling, tt se on kun lukee liikaa englanninkielist
materiaalia). Rekisterimtn versio kannattaa pit niin paljon
ominaisuuksia sisltvn, ett siit todella on jotain iloa, mutta
pit niin paljon hyvi ominaisuuksia rekisteridyss versiossa, ett
rekisterimtn kyttj nkee saavansa rekisterintirahoilleen
vastinetta rekatessaan pelin. Mys mahdolliset ilmaiset/alennetut
pivitykset tai muut vastaavat etuisuudet tulevaisuudessa voivat
jonkin verran avittaa, mutta muista, ett suurin osa kyttjist etsii
vlitnt hyty, eik paljoa vlit tulevien pelien
rekisterintihintojen alentumisista.

Kannattaa mys harkita mill tavalla hoidat rekisterinnit. Maksaminen
pit tehd helpoksi (ja mielelln halvaksi), sill suurin osa
rekisterijist on kuitenkin laiskaa porukkaa ja mahdollisuus
rekisterity tuoliltaan nousematta voi olla hyvinkin suuri
etu. Maksutapoina kannattaa ainakin huomioida suoran kteisen lisksi
pankkisiirrot, jotka ovat viitteiden kanssa varsin nppr tapa
rekisterid. Mys postiennakko on hyv tapa, vaikka sill on
suhteellisen korkeat kustannukset se on kuitenkin nppr keino
varsinkin vhn tyyriimmille ohjelmille (20 markan
rekisterintihintaan saman verran lis voi pelottaa ostajia).

Rekisteridyn version lhetykseenkin on useita mahdollisuuksia. Itse
olen miettinyt nit ja tss on muutama, mist valita, osa helppoja
toteuttaa ja osa vaikeita:

1. Rekisterintiavain
  + Pieni, nppr lhett vaikka shkpostilla
  - Todella helppo kopioida
  - Helppo murtaa
2. Rekisterity EXE
  + Suhteellisen pieni, mennee suurempiin shkpostilaatikkoihin
  + Varma, vaikea murtaa
  - Lhes yht helppo kopioida
3. Rekisterity versio
  + Helppo toteuttaa
  + Ihan pikkuisen vaikeampi kopioida
  + Varma, vaikea murtaa
4. Rekisterity versio ja avain
  + Varmin menetelmist, vaikea kopioida, helppo toteuttaa

Toisaalta kannattaa muistaa, ett jos sin jaksat laittaa sen
disketeill postissa ei se ole kenellekn ongelma laittaa
viidellekymmenelle koneelle ja vaikka valmistaa diskcopyll rekatusta
versioista piraattiversioita jatkolevitykseen, eli kopiointi on aina
aika helppoa. Toisaalta kopiointia haittaa ainakin hieman erillisen
annettava avain tai installointiohjelmassa rekisterijijn nimen ja
tunnuksen pyytminen jne.

Mys voi pit mieless, ett mit enemmn turvatoimia sit hankalampi
se on rekisterijlle. Kohtuus kaikessa niin pysyvt rekkaajatkin
tyytyvisin.

Siin lienevt ne trkeimmt asiat, joita kannattaa pit mieless
peli tehdess. Lisksi tietenkin lytyy kokonaisia kirjoja pelinteon
taiteesta ja niiden suunnittelusta, mutta tmn luvun ptarkoitus on
ollut valaista pelinteon kytnnllisempi puolia. Nyt tm
tutorialisti lhtee lukemaan ruotsin kokeisiin!


7.5 Interpolointi ja viivoja
----------------------------

Ilja Brysy taisi tss kuukausi sitten patistaa minua neuvomaan miten
DJGPP:ll piirretn viivoja. No, psin plkhst lupaamalla
kirjoittaa siit jutun sitten Laamatuttiin. No mink taakseen jtt
sen edestn lyt, eik tmkn kerta nyt olevan mikn poikkeus.
Kuitenkin tll kertaa selitn muutakin kuin sen viivanpiirron, nimittin
selitn mit tarkoittaa interpolointi, sek miten ja mihin sit voi
tietokoneella kytt.

Eli termimme on interpolointi. Inter voisi latinassa tai jossain muussa
kieless hyvinkin tarkoittaa vliss, ainakin interpolointi tarkoittaa
jotain thn hyvin liittyv. Interpolointi on nimittin sit, ett kun
tiedossamme on kaksi pistett, niin voimme "arvata" sinne keskelle ret-
tmsti uusia pisteit, jotka kaikki kuuluvat samalle suoralle. Tm
on ns. lineaarista interpolointia, eli interpoloidaan pisteit samalle
suoralle. Tllaisesta toimenpiteest hyv esimerkki voisi hyvinkin
olla viivanpiirto, sill siinhn meill on kaksi pistett, ja meidn
tytyy saada niiden vlill tarpeellinen mr pisteit viivan
esittmiseksi.

No niin, tiedmme siis mit on interpolointi. Se on siis pisteiden
lismist kahden tunnetun pisteen vlille. Vaan miten noiden pisteiden
sijainti sitten pitisi laskea? No, miettikmme tilannetta, jossa meill
on kaksi pistett, a ja b, joiden koordinaatit ovat vastaavasti (ax,ay)
ja (bx,by). Nyt me laskemme niden vlill yhden pisteen. Ensimminen
tehtv lienee laskea, kuinka pitksti meill on matkaa x- ja y-suunnassa.
Nit lukuja nimitetn yleisesti delta-arvoiksi. Ne lasketaan
seuraavasti:

delta_x = | bx - ax |
delta_y = | by - ay |

Miss merkit "|" tarkoittavat itseisarvoa, siis "| a |" luetaan
"a:n itseisarvo". C:ll funktio on abs, tai fabs, jos kytmme
floatteja.

No niin, tiedmme kuinka kaukana pisteet ovat toisistaan, mutta mit
ihmett sitten oikein teemme tll uudella, kiinnostavalla tiedolla?
No jatketaanpas hieman viivanpiirron kehittely. Jos haluamme
katkeamattoman viivan, niin meill pit olla yht monta pikseli kuin
viivan pidemmn akselin pituus on. Eli jos delta_x on suurempi kuin
delta_y, niin tarvitsemme delta_x:n verran pikseleit. Tilaanteen
ollessa pinvastainen on tarvittavien pikselien mr tietenkin
vastaavasti delta_y.

Sitten pidemmlle toteutukseen. Kun nyt tiedmme montako pikseli
tarvitsemme ja kummassa suunnassa, niin voimmekin suunnitella
seuraavanlaisen piirtorakenteen:

jos delta_x >= delta_y niin

  y = ay

  y_korotus = delta_y / delta_x

  looppaa x vlill ax...bx

    piste ( x, y, vri )

    y = y + y_korotus

  end looppi

muutoin

  x = ax

  x_korotus = delta_x / delta_y

  looppaa y vlill ay...by

    piste ( x, y, vri )

    x = x + x_korotus

  end looppi

end jos

Nyt te tietenkin kysytte: "Mit tuo tekee?" No, olen ilke ja kerron
teille. Koska meidn tytyy piirt pidemmn akselin verran
pikseleit, niin se tarkoittaa, ett piirtosilmukan tytyy korottaa
pidemmn akselin koordinaattia yhdell ja lyhemmn jollain pienemmll
kuin yhdell. Jos alkaisimme piirtelemn lyhyemmn akselin mukaan,
niin viivan toinen akseli harppoisi yli 1 pikselin askelia ja viivaan
jisi reiki.

Eli jos...muutoin -rakenne valitsee pidemmn akselin. Sitten
alustetaan lyhyemmn akselin aloituskoordinaatti ja korotus jo
valmiiksi. Koska tiedmme, ett looppi korottuessaan yhdell tulee
toistamaan sen sisll olevan koodin yht monta kertaa kuin pidemmlle
akselille tulee pikseleit (jos delta_x on pidempi akseli, niin
delta_x kertaa) niin voimme helposti laskea paljonko lyhyemmll
akselilla tytyy liikkua yhden kierroksen aikana. Tm korotus
saadaan siis jakamalla lyhyen akselin pituus pidemmn akselin
pituudella.

Ette varmaan ymmrtneet mitn, joten parasta ottaa esimerkki. Meill
on viiva pisteest (10, 10) (eli siis ax=10 ja ay=10) pisteeseen (30,
20) (eli taas bx=30 ja by=20).

delta_x = | bx - ax | = | 30 - 10 | = | 20 | = 20
delta_y = | by - ay | = | 20 - 10 | = | 10 | = 10

Huomaamme, ett delta_x on pidempi ja meidn tytyy piirt delta_x
kappaletta pikseleit saadaksemme yhtenisen viivan. Valitsemme siis
pseudo-koodistamme jos-osaa seuraavan ptkn, sill  lause
'delta_x >= delta_y' on tosi.

y = ay = 10
y_korotus = delta_y / delta_x = 10 / 20 = 0.5

Nyt kun siis looppaamme x:n vlill 20...30, niin joka x:n korotusta
yhdell seuraa y:n korotus 0.5:ll. Nin siis x ja y menevt:

  x  | y
 ---------
  10 | 10
  11 | 10.5
  12 | 11
  .. | ..
  29 | 19.5
  30 | 20

Huomaa, ett koska piirrossa pit kytt kokonaislukuja, niin nuo
desimaaliosan sisltvt y-koordinaatit pyristyvt aina alaspin,
jolloin piirtokoordinaatit ovat:

  x  | y
 ---------        -- (10, 10)
  10 | 10           --
  11 | 10             --
  12 | 11               --
  13 | 11                 --
  14 | 12                   --
  15 | 12                     --
  .. | ..                       --
  27 | 18                         - (30, 20)
  28 | 19
  29 | 19
  30 | 20

Vasemmalla siis taulukko loopissa kiertess x- ja y-arvoista ja
sitten oikealla viiva, jonka nkinen tuosta suurinpiistein tulee.
Mutta, hienoa muuten, mutta pari ongelmaa on
ratkaisematta. Selvitettymme pidemmn akselin ja laskettuamme
lyhyemmlle akselille tarvittavan koordinaatin korotuksen voimme kyll
piirt viivan noin, mutta ongelmia seuraa heti, jos ensimminen piste
on toisen pisteen oikealla- tai alapuolella. Sill korotus on aina
positiivinen, kun sek jaettava ett jakaja ovat
positiivisia. Ongelmia aiheuttaa mys se, ett jos pidemmn akselin
ensimminen koordinaatti on suurempi kuin jlkimminen, niin
korotuksen tilallahan pitisi olla vhennys!

No, hieman lislogiikkaa ja hyvin menee. Teemme nimittin sill
tavalla, ett jrjestmme pidemmn akselin ensimmisen koordinaatin 
aina pienemmksi kuin toisen. Eli jos ensimminen piste onkin toisen
oikealla-/alapuolella, niin funktiomme vaihtaa pisteiden
paikkoja. Sama viiva se on silti, mutta loopissa ei tarvitse mietti
onko se ensimminen pienempi tai suurempi, sill se on aina pienempi.

Ja kun viel poistamme pyristykset alusta, niin jos lyhyemmn akselin
pituus on negatiivinen, niin sen jako pidemmn akselin pituudella
tuottaa negatiivisen korotuksen (y_korotus ja x_korotus). Ja jo
ala-asteellahan on opetettu, ett negatiivisen luvun lisys on sama
kuin vastaluvun vhennys. (eli suomeksi: 10 + (-10) = 10 - 10)

Eli upea pseudorutiinimme kokonaisuudessaan:

funktio viiva( int ax, int ay, int bx, int by, char vri )

  float x, y, x_korotus, y_korotus, delta_x, delta_y

  delta_x = bx-ax
  delta_y = by-ay

  jos |delta_x| >= |delta_y| niin

    jos delta_x < 0 niin

      vaihda( ax, bx )
      vaihda( ay, by )

    end jos

    y = ay

    jos delta_y == 0 niin

      y_korotus = 0

    muutoin

      y_korotus = delta_y / delta_x

    end jos

    looppaa x vlill ax...bx

      piste( (int)x, (int)y, vri )

      y = y + y_korotus

    end looppaa

  muutoin

    jos delta_y < 0 niin

      vaihda( ax, bx )
      vaihda( ay, by )

    end jos

    x = ax

    jos delta_x == 0 niin

      x_korotus = 0

    muutoin

      x_korotus = delta_x / delta_y

    end jos

    looppaa y vlill ay...by

      piste( (int)x, (int)y, vri )

      x = x + x_korotus

    end looppaa

  end jos

end funktio

Tuo tarkistus nollasta pidemmn akselin kohdalla ('jos delta_x == 0'
sek 'jos delta_y == 0')  siksi, ett pystyviivan kanssa pit korotus
olla 0, eik jako nollalla tule kysymykseen muutenkaan, sill se
kaataa ohjelman. Itseisarvot vertailussa 'jos |delta_x|>=|delta_y|'
pit olla siksi, ett emme kyttneet niit aiemmin lainkaan.
No juu, voin kyll lyd vetoa, ettei se toimi, mutta kirjoitetaanpas
silti kauniilla C-kielell puhtaaksi:

void vaihda( int *a, int *b ) {
  int temp;

  temp=*a;
  *a=*b;
  *b=temp;
}

void viiva( int ax, int ay, int bx, int by, char vari ) {
  float x, y, x_korotus, y_korotus, delta_x, delta_y;

  delta_x = bx-ax;
  delta_y = by-ay;

  if( fabs(delta_x) >= fabs(delta_y) ) {

    if( delta_x < 0 ) {
      vaihda( &ax, &bx );
      vaihda( &ay, &by );
    }

    y = ay;

    if( delta_y == 0 ) {
      y_korotus = 0;
    } else {
      y_korotus = delta_y / delta_x;
    }

    for( x=ax; x<=bx; x++ ) {
      putpixel( (int)x, (int)y, vari );
      y = y + y_korotus;
    }

  } else {

    if( delta_y < 0 ) {
      vaihda( &ax, &bx );
      vaihda( &ay, &by );
    }

    x = ax;

    if( delta_x == 0 ) {
      x_korotus = 0;
    } else {
      x_korotus = delta_x / delta_y;
    }

    for( y=ay; y<=by; y++ ) {
      putpixel( (int)x, (int)y, vari );
      x = x + x_korotus;
    }

  }
}

Tuon testaamiseksi paras on omat silmt ja niinp yhdistin
viivanpiirtorutiinin ja hiiriesimerkin pyynnn yhteen
tiedostoon. Hiirell pyriv viivanpiirtj lytyy EXAMPLE-hakemiston
alta tiedostosta LINE.C. Nyt varmaan olisi paras, ett selvitt
itsellesi miten interpolointi viivanpiirrossa toimii ja miten rutiini
yleenskin toimii. Interpolointi on siis yksinkertaisesti pisteiden
laskemista kahden pisteen vlille ja tietokoneella se tehdn yleens
siten, ett otetaan koordinaatit ja jaetaan niiden vlill oleva tila
n kappaleeseen jakolaskulla ja sitten vain loopataan n kertaa
korottaen koordinaatteja tll luvulla. Viivanpiirrossa, kuten mys
monessa muussa hommassa jrjestetn asiat siten, ett toinen
korotettavista on 1 ja toinen sitten mit tarve vaatii.

Kannattaa mys muistaa se, ett esitelty viivanpiirtorutiini on, ellei
hitain, niin kuitenkin todella takkuinen. Ensimmisen voisi aloittaa
muuttamalla float-muuttujat fixed-pointiksi. Sitten mys omat rutiinit
pysty- ja vaakasuuntaisille sek diagonaalisille (45 asteen kulma)
viivoille. Mys "pitkille" ja "leveille" rutiineille voisi tehd
jotain optimointia. Erilliset rutiinit molemmille tai jokin kikka
mill yhdist logiikkaa voisi hyvinkin nopeuttaa. Sitten todellisille
nopeuskiihkoilijoille assyoptimointi tai ehk mieluummin Bresenhamin
viivanpiirron opettelu (lytyy PCGPE:st) voisi olla
tarpeen. Bresenhamia en ala opettamaan, kun en itsekn rutiinin
toimintaa ymmrr. Nopea se on joka tapauksessa.

Mys tutkiminen paperilla voi auttaa. Jos kuitenkin tuntuu, ett jokin
ji epselvksi, niin sitten vain postia. En nimittin tied kuinka
eptydellinen selityksest tuli, kun itse olen asian kanssa takunnut
niin kauan, ett sen osaa etu- ja takaperin. Interpolointi on parasta
olla hanskassa, sill sit tarvitaan mys esim. kaikkeen polygonien
piirtoon liittyvss. Mutta min jatkan seuraavaan aiheeseen, nhdn
siell!


7.6 Vapaa skrollaus
-------------------

Tnn teemme sit yls ja alas, sivulle ja toiselle sek useampia
yht aikaa. Se ei ole mit luulet, vaan se on vapaasuuntaista
skrollausta, tarvittaessa vaikka osiin jaetulla ruudulla jokaisessa
omaan suuntaansa.

Aihe on helppo. Niin helppo, ett min tein sellaisen ilman mitn
ongelmia. Ja se on sitten aika helppoa. Mutta jotta ne jotka eivt
osaa/jaksa itse paneutua ongelmaan paria minuuttia enemp omien
aivojen voimin saavat tss tyhjentvn selityksen. Skrollaushan on
pienen palasen nyttmist suuremmasta kokonaisuudesta. Ruutu on
iknkuin ikkuna suurempaan ruutuun. Kuten alhaalla nkyy:

+---------------------------+   Kuvan pisteet vain hahmottavat
| .  .  .  .  .  .  .  .  . |   nytettv aluetta. Ne eivt
| (x,y)  .  . KOKO.  .  .  .|   merkitse mitn. :)
|.  o-----+  .  .  .  .  .  |
| . |     |.  NYTETTV  . |
|  .|RUUTU| .  .  .  .  .  .|
|.  |     |  .ALUE .  .  .  |
| . +-----+.  .  .  .  .  . |
|  .  .  .  .  .  .  .  .  .|
+---------------------------+

Ruutu on yleens koko nyttruudun kokoinen, tai sitten jos
nyttoruutu on jaettu useaan osaan niin sen osan kokoinen, johon
ikkuna piirretn. Kyse on siis vhn samantapaisesta toiminnasta,
kuin bittikarttojen kanssa. Bittikartoissa vain piirretn ruutua
pienempi kuva ruudulle, kun skrollauksessa luetaan ruutua suuremman
kuvan osa ruudulle. Piirrossa aloitamme nytettvn alueen kohdasta
(x,y) (merkitty kuvassa kirjaimella 'o' ruudun ylkulmaan). Sitten
vain kopioimme ruudun leveyden verran pikseleit nytettvst
alueesta ja siirrymme taas seuraavalle riville. Yksinkertaista, mutta
helppoa! Jatkamme tt kunnes olemme saaneet ruudun tyteen. Helppoa!

Skrollaavassa peliss tytyy ottaa nyt huomioon, ett spritej ja
muita ei en piirret kaksoispuskuriin, vaan thn nytettvn alueen
puskuriin. Sen koko voi sitten olla mit vain maan ja taivaan vlill
- ainakin lukualueen rajoissa, kuitenkin. Skrollauksessa nytt onkin
nyt vain ikkuna liikkuvaan ja elvn pelimaailmaan. Fiksu kooderi
tietenkin piirt vain nkyviss olevat asiat, mutta sellaiset
hienoudet jvt ohjelmoijan ptksen varaan.

Aika heitt editorisi ruudulle hieman pseudoa:

char alue[640][400]
char ruutu[320][200]

funktio pivit( int yl_x, int yl_y )

  int rivi, sarake

  looppaa rivi vlill 0...199

    looppaa sarake vlill 0...319

      ruutu[ rivi ][ sarake ] = alue[ yl_y + rivi ][ yl_x + sarake ]

    end looppaa

  end looppaa

end funktio


Nyttk vaikealta? Ei pitisi, ei ainakaan minusta nyt. :)
Mutta pistetn vhn vaikeammaksi. C-toteutuksessa kun meill
kuitenkin on vain yksiuloitteinen taulukko, niin sijoituksessa pit
osoite laskea ksin:

ruutu[ rivi * 320 + sarake ] = 
  alue [ (yl_y + rivi) * 640 + yl_x + sarake ];

Toisena tuo on toivottoman hidasta. Kannattanee sst sisempi looppi
ja kopioida memcpy:ll koko rivi kerralla:

memcpy( &ruutu[ rivi * 320 ], 
        &alue[ (yl_y + rivi) * 640 + yl_x ],
        320 );

Nin saamme seuraavanlaisen C-kielisen kyhelmn:

char alue[640*400];
char ruutu[320*200];

void paivita( int yla_x, int yla_y ) {
  int rivi;

  for(rivi=0; rivi<200; rivi++)
    memcpy( &ruutu[ rivi * 320 ], 
            &alue[ (yla_y + rivi) * 640 + yla_x ],
            320 );
}


Eri kokoisten nyttalueiden/virtuaaliruutujen (tai miksi niit nyt
haluatkin sitten kutsua) toteuttaminen ei paljoa vaadi. Puskurin koko
vain muokkaukseen ja offsetin ((yla_y + rivi) * 640 + yla_x) laskuun
pikku muutos ja se onkin siin. Sitten viel pit hoitaa niin, ett
piirrettvn alueen alakulma ei mene virtuaaliruudun ulkopuolelle, eli
tarkoitan tt:

+-----------+
|           |
| VIRTUAALI |
| RUUTU  +--+--+
|        |  |  |
+--------+--+  |
         |RUUTU|
         +-----+

Jos yla_x tai yla_y kasvaa niin suureksi, ett yla_x+320 tai yla_y+200
menisi ruudun yli, niin silloin kaivetaan tavuja varatun muistialueen
ulkopuolelta aiheuttaen joko ihmeellist kyttytymist tai koneen
kaatumisen. Joten pidetnps koordinaatit kurissa!

Mit tuo onkaan mit kuulen? (olemattomia?) Totta, taisin luvata
muutakin kuin koko ruudun skrollausta. No, se ei ole vaikeaa. Kun
ylemmss esimerkiss me piirsimme koko ruudulle, niin olisimme
tietenkin sen sijaan voineet aloittaa ruudultakin jostain muualta kuin
oikeasta ylkulmasta ja ikkunan koko olisi voinut olla vaikka 100x100.
Kun ikkunan koko on vhemmn kuin koko nyttruudun koko se tarkoittaa
mys sit, ett ikkunoita mahtuu ruudulle tarvettaessa
useampia. Tllainen onnistuu funktiolla, joka ottaa parametreinn
virtuaaliruudun aloituspisteen lisksi mys aloituspisteen
nyttruudulla ja ikkunan korkeuden ja leveyden.

Tosi kooderi osaa tietenkin toteuttaa tuollaisen pienell
miettimisell. Ja koska minkin olen sellainen, niin olen tehnyt
yhdistetyn nppinesimerkin ja skrollausesimerkin joka lytyy mys
EXAMPLE-hakemiston alta tiedostosta, tll kertaa nimen SCROLL.C alta.

Tss vaiheessa tytyy viel varoittaa siit, ett memcpy on syntisen
hidas tapa kopioida muistia. Optimointi assyll tai jopa C:ll voi
nopeuttaa toimintaa, jos muistia heitelln 4 tavun palasissa. Mittaa
kuitenkin mahdollinen nopeushyty, ettet vahingossa laita hitaampaa
korvaavaa rutiinia! Sitten vain sisistmn luvun asiaa, olikos se
nyt niin vaikeaa? Viiden sekunnin sormienvenyttelytaun jlkeen onkin
sitten vuorossa sinit ja kosinitit, sek plasmaa.


7.7 Sinit ja kosinit sek plasmaa
---------------------------------

No nyt hieman kertausta yhdeksnnen luokan matematiikasta. Sinit ja
kosinit. Mit ne sitten ovat ja mit niill tehdn? Ennenkuin
vastaan, niin tutustukaamme Suorakulmaiseen Kolmioon:

   o          a = kateetti
   |\         
   | \        b = kateetti
 a |  \  c
   |   \      c = hypotenuusa
   |    \
   |     \   
   |     /\   * = tss nurkassa on kulma alpha
   o-------*  
       b

No kaikki varmaan osaavat jo pythagoraan lauseen c^2 = a^2 + b^2. 
Mutta sinit ja kosinit ovatkin jotain ihan uutta, ainakin
8-luokkalaisille ja ysins aloittaneille, kenties:

sin alpha = a/c
cos alpha = b/c
tan alpha = a/b

Vain kaksi ensimmist oikeastaan kiinnostavat meit, sill niit
yleens kytetn. Nyt tiedmme siis, ett sini jostain kulmasta on
yht kuin kateetin a ja hypotenuusan osamr ja kosini vastaavasti
kateetin b ja hypotenuusan. Vaan mit h**vetti oikein teemme tll
tiedolla? No KYTMME HYVKSEMME!

Nimittin kiinnostavaa on, ett jos tiedmme c:n, eli hypotenuusan
pituuden ja kulman, niin voimme laskea vastaavan suorakulmaisen
kolmion molempien kateettien pituudet. Pyrittelemll hieman tavaraa
puolelta toiselle (kai yhtln ratkaisu oli jo seiskalla?):

a = sin alpha * c  ; sini
b = cos alpha * c  ; kosini

Kiinnostavaa on mys, ett jos kuviossa thdell merkitty krki on
ympyrn keskipiste ja c ympyrn sde, niin 'sin alpha * c' antaa
ympyrlt kulman alpha kohdalla olevan pisteen y-koordinaatin ja
kosini sitten vastaavasti x-koordinaatin, kas nin:

      --^--       Kuten kaaviosta nkee, niin ympyrn steest
    --  |  --     muodostuu hypotenuusa ja kun tiedmme kulman
   -    |    X    alpha voimme selvitt kateettien pituudet,
  -     |   /|-   jotka samalla ovat pisteen X koordinaatit
  -     |  / |-   kuviossa. Eli 
 -      | /  | -    
 -      |/\  | -    X = (cos alpha * radius, sin alpha * radius)
 <------o----+->
 -      |      -  Viivanpiirron kehittely tst ei olisi vaikeaa,
 -      |      -  tarvitaan vain looppaus vaikka 360 astetta
  -     |     -   ja joka kerralla lasketaan pisteen koordinaatit
  -     |     -   laskurin osoittamalle kulmalle (0...359), jolloin
   -    |    -    ruudulle piirtyy kaunis ympyr steeltn
    --  |  --     <radius>.
      --v--
 
Olet mys varmaan pelannut autopeli nimeltn Slicks, tai jotakin
luolalentely (esim. Auts, V-Wing, Kops, Rocket Chase, Kaboom,
Spruits, Wings, PP, Turboraketti, A-Wing, ...). Tllaisissa peleiss,
joissa tytyy pysty liikkumaan muuallekin kuin yls, alas, oikealle
ja vasemmalle tytyy mys pysty liikkumaan muihin ilmansuuntiin.
Jos ajattelemme tietokoneen koordinaatistioa, niin aste 0 osoittaa
oikealle, 90 alas, 180 vasemmalle ja 270 yls. Nyt jos haluamme tiet
paljonko pit alusta siirt x-suunnassa ja paljonko y-suunnassa
nopeuden ollessa vaikkapa 5 vasemmalle alaviistoon (siis 90+45=135
astetta) saamme seuraavan lausekkeen:

x_nopeus = (cos 135)*5
y_nopeus = (sin 135)*5

Kaikki nytt helpolta. Osaamme piirt ympyrn, laskea tarvittavan
x- ja y-nopeuden tiettyyn suuntaan kohdistuvalle liikkeelle ja vaikka
tehd pyrivn thden viivanpiirtorutiinien avulla. Vaan viel hieman
pit pinnist pstksemme tavoitteeseemme C:ll. Alla muutamia
totuuksia sinist ja kosinista:

 1) Ne palauttavat 99.99% tilanteista arvon joka on yli -1 ja alle 1,
    joten jos leikitn kokonaisluvuilla saadaan tulokseksi 0

 2) Koska arvoalue on niin pieni, tytyy aina kytt joko liukulukuja
    (float) taikka fixed point -lukuja. Fixedeinkin siet kytt
    monta bitti desimaaliosalle, jottei tule karkeita muotoja.

 3) Kuten muitakin matikkatavaroita kyttvt funktiot, mys sinit ja
    kosinit vaativat math.h:n ja libm.a:n (knnsoptio -lm) mukaansa
    toimiakseen.

 4) Parametrina funktioille sin() ja cos() annetaan luku RADIAANEINA,
    muunto tapahtuu seuraavasti:

      radiaanit = 3.1415 * 2 * kulma / MAX_KULMA

    MAX_KULMA on sama kuin suurin_mahdollinen_kulma+1. Eli
    normaalistihan se on 360, mutta tietokoneella kytetn usein
    256-, 512- ja jopa 1024-asteisia kulmia, sill ne ovat huomattavan
    helppoja laskea. Etenkin 256-asteinen on nppr, sill kun
    suurinta kulman arvoa 255:tt korotetaan ja laskuri on tyyppi
    unsigned char, niin se pyrht automaattisesti ympri, takaisin
    nollaan. Huomaa mys, ett 360-asteisillakin ympyrill maksimi
    kulma on 359!

Nyt kun tiedt kaiken trken, niin olet valmis kyttmn taitojasi
kytnnss. Mutta viel yksi asia: Taulukointi. Sini ja kosini ovat
molemmat luvattoman hitaita suorittaa, joten parasta on tehd
lookup-taulukko niille. Eli teemme 256-alkioiset taulukot sek sinille
ja kosinille ja laskemme niihin molempiin valmiiksi arvot sinist ja
kosinista kulmille 0...255 (kytmme siis 256-asteista ympyr):

int loop;
float sintab[256], costab[256];

for(loop=0; loop<256; loop++) {
  sintab[loop] = sin(3.1415*2*(float)loop/256.0);
  costab[loop] = cos(3.1415*2*(float)loop/256.0);
}

Nyt ei sitten tarvita turhia vntelehtimisi minkn
radiaanikonversion kanssa tai muutakaan yht epmiellyttv, vaan
toiminta on suorasukaista. Laittakaamme aluksemme kohti kaakkoa:

x_suunta = costab[45];
y_suunta = sintab[45];

Jotain oli viel... aijuu, se plasma! No jotkut ovat ehk tmn
tehneet jo ja toiset ovat tehneet ainakin
paletinpyritysplasman. Mutteivt viel sit aitoa ja oikeaa. Vaan nyt
tulee asiaan muutos. Liikkuva plasma on oikein mukava olla olemassa ja
tss tulee idea lyhykisesti:

 1) Tehdn 6 kappaletta eri "korkuisia" sinikyri (siis hypotenuusan
    pituus / ympyrn sde / sinin kerroin) ja jotka mielelln alkavat
    eri kohdista ja joissa aallon pituus on eri (eli toinen kyr on
    kuin 256-asteista ympyr varten tehty ja toinen taas kuin
    128-asteista jne.). Mys kosinia kannattaa kytt. 

    Idea on kuitenkin se, ett jokainen kyr tallennetaan omaan
    taulukkoonsa ja ett jokaista kyr voidaan "monistaa" perkkin,
    eli jos samaa kyr piirretn kaksi perkkin ei kyr katkea
    kesken, siis:

    VRIN:

    \              \         \
     \              \         \
      \     /        \     /   \     /
       \---/          \---/     \---/

    OIKEIN:

    
    \       /---   \       /--- \       /---
     \     /        \     /      \     /
      \---/          \---/        \---/

    Eli koska kyr joudutaan toistamaan perkkin niin jos se ei
    palaa lhtpaikkaansa loppuun menness tulee sahalaitaa. Plasman
    tapauksessa tuloksena on epmiellyttvn nkisi loikkauksia
    muuten pehmeiss vriliu'uissa.

    Alla esimerkki kuuden erilaisen kyrn alustuksesta ja
    generoinnista:

    float wave1[256], wave2[256], wave3[256], 
          wave4[256], wave5[256], wave6[256];
    int loop;

    for(loop=0; loop<256; loop++) {
      wave1[loop]=cos(3.1415*2* (float) loop             /256) * 25.0 + 25.0;
      wave2[loop]=cos(3.1415*2* (float) (loop%128)       /128) * 15.0 + 15.0;
      wave3[loop]=cos(3.1415*2* (float) (255-loop)       /256) * 17.5 + 17.5;
      wave4[loop]=sin(3.1415*2* (float) (loop%64)         /64) * 22.5 + 22.5;
      wave5[loop]=cos(3.1415*2* (float) ((128-loop)%128) /128) * 20.0 + 20.0;
      wave6[loop]=sin(3.1415*2* (float) loop             /256) * 25.0 + 25.0;
    }

    Koska sin ja cos palauttavat mys negatiivisia arvoja, tytyy
    niihin list sama luku kuin kerroinkin, jotta ne olisivat aina
    positiivisia. Ninollen kaikkien aaltojen summa on pahimmillaan
    kerrointen summa * 2, suomeksi maksimissaan 250.

 2) Kun aallot ovat tallessa aletaan liikuttamaan niit eri
    suuntiin. Kytnnss tm hoituu kyttmll yht laskuria
    jokaista aaltoa kohti, joka kertoo senhetkisen aallon alun
    sijainnin.

    Sitten vain aletaan piirtmn. Kolme ensimmist aaltoa ovat
    vaaka-aaltoja ja kolme viimeist pystyaaltoa. Tm tarkoittaa
    sit, ett kolmen ensimmisen aallon alkion numero riippuu
    vritettvn pikselin x-koordinaatista ja kolmen viimeisen indeksi
    on riippuvainen y:st. Kun indeksi viel typistetn vlille
    0...255 and-funktion maskilla 0xFF niin voimme laskea jokaiselle
    aallolle oikein indeksin:

    wave1[ ( ind1 + x ) & 0xFF ]
    wave2[ ( ind2 + x ) & 0xFF ]
    wave3[ ( ind3 + x ) & 0xFF ]
    wave4[ ( ind4 + y ) & 0xFF ]
    wave5[ ( ind5 + y ) & 0xFF ]
    wave6[ ( ind6 + y ) & 0xFF ]

    Sitten vain ynntn jokaiselle pikselille aallon senhetkiset
    alkiot yhteen ja saatu luku tungetaan ruudulle pikselin
    vriarvona. Koska x- ja y-koordinaatit liikuttavat tasaisesti
    aaltojen indeksi eteenpin syntyy ynnmll tasainen kumpuileva
    vrimaasto. Eli piirtolooppi:

    for(x=0; x<320; x++) for(y=0; y<200; y++) {
      dblbuf[y*320+x] = 
        wave1[ ( ind1 + x ) & 0xFF ]+
        wave2[ ( ind2 + x ) & 0xFF ]+
        wave3[ ( ind3 + x ) & 0xFF ]+
        wave4[ ( ind4 + y ) & 0xFF ]+
        wave5[ ( ind5 + y ) & 0xFF ]+
        wave6[ ( ind6 + y ) & 0xFF ];
    }

    Jonka ulkopuolella korotetaan ja vhennetn aaltojen
    aloituskohtia (ind1-ind6) ja tt toimintaa toistetaan niin kauan
    kunnes painetaan nappia. Tydet sorsat EXAMPLE-hakemistosta
    tiedostosta PLASMA.C.

Siin olikin tmn kappaleen asia. Nyt taidan katsoa lksyni ja tehd
esimerkit loppuun. Viel pitisi paletin kvantisointi saada tehty
ennen joulua, katsotaan ehdink ajoissa, pakko kai kyll varmaan on. :)
No niin, nyt vain piirtelemn ihme kyri ja kuvioita paperille ja
ruudulle, jota sinin ja kosinin syvin olemus selvi tydellisesti.


7.8 Paletin kvantisointi - Median cut
-------------------------------------

Nyt ollaankin sitten kyynrpit myden mudassa. Paletin kvantisointi
lienee sellainen temppu, jota eivt kaikki kokeneemmatkaan osaa, ja
kaiken lisksi se on suhteellisen vaikea homma. Joten kiinnittk
turvavynne ja valmistautukaa yritykseeni selvent hieman asiaa
tuntemistani kvantisointitavoista helpomman, tai ainakin nopeamman,
osalta.

Eli mist nyt sitten puhumme? Paletin kvantisointi on sit, ett kun
sinulla on vaikkapa 6 erilaista PCX-kuvaa, joissa on yhteens 1321
erilaista vri, niin esittksesi nm 1321 erilaista vri ruudulla
tytyy sinun KVANTISOIDA palettisi. Kvantisointi on siis vrimrn
tiputusta. Ja nyt seuraa se, miten sen teemme.

Ensin hieman funktioiden ideasta. Kuvittelemme vrit kuutiona, jossa
x-y-z -akseliston tilalla onkin r, g ja b. Meill on siis
ns. rgb-avaruus, jossa vrin sijainnin kuutiossa kertoo punaisen,
vihren ja sinisen komponentin mr. Jos jokainen koordinaatti on
vlill 0...63 (kuten normaaleissa PCX-kuvissa), niin meill on
64*64*64 pikseli sisltv kuutio, jonka srmn pituus siis on 64
pituusyksikk.

Tmn monimutkaisen ajatusrakennelman pohjalle perustuu
algoritmimme. Kun teemme taulukon, joka sislt kaikki erilaiset
vrit on taulukon jokainen alkio (rgb-tripletti) piste
kuutiossa. Funktiomme etsii sen akselin (siis r, g tai b), joka on
"pisin", eli suomeksi katsotaan jokaisen vrikomponentin pienin ja
suurin esiintyv arvo. Sitten funktio jakaa vrikuution kahtia
tsmlleen siten, ett puolet pisteist/vreist j toiselle ja
puolet toiselle puolelle. Ei siis suurimman ja pienimmn vriarvon
puolesta vlist!

Kun koko kuutio on jaettu, niin kutsumme vain samaa jakofunktiota
kummallekin pienemmlle kuutiolle, jossa molemmissa on nyt siis yht
monta vri. Nm funktiot etsivt oman palasensa pisimmn
vrikomponenttien vlin ja jakavat kuution kahtia kutsuen itsen
molemmille kuutioille.

Funktio, joka kutsuu itsen on ns. rekursiivinen funktio ja se on
nppr monessa asiassa. Jos piirrt paperille yhden laatikon ja siit
lhtemn kaksi laatikkoa, joista kummastakin lhtee kaksi laatikkoa,
joista jokaisesta lhtee kaksi laatikkoa jne., niin saat huomaat, ett
joka rekursiotasolla funktioiden "mr" kaksinkertaistuu. Jos siis
asetamme rekursiorajaksi 3, niin vrit jakautuvat 2^3:n, eli
kahdeksaan pienempn kuutioon. Kvantisointi 256:een vriin vaatii
siis kahdeksan rekursiotasoa, jotta saataisiin kuutio 256:een osaan.

Nyt te, jotka saitte ahaa-elmyksen (jokaisen pitisi saada ;) menette
tekemn oman funktionne. Tyhmemmille ja kenties niille, jotka
haluavat saada viel hieman varmennusta tulee kuitenkin viel
teknisempi selostus, jonka teossa apuna on kytetty skenelehti
Imphobian osan 10 sisltm informaatiota. Kiitoksia Fakerille.

Ensimmiseksi teemme iiison taulukon, jonka koko on kaikkien
mahdollisten vrien yhteenlaskettu mr. Jos valitsemme 64 svy
jokaiselle vrikomponentille saamme siis kooltaan 64x64x64 kokoisen
vrikuution. Varaamme tlle muistia:

unsigned char *PaletteArray=(unsigned char *)calloc(64*64*64, 1);

(huomaa kaikkien alkioiden nollaus alussa) Kun haluamme list vrin
kvantisoitavien joukkoon, merkkaamme yksinkertaisesti tmn kuution
vastaavan pikselin ykkseksi. Nin meill on kvantisoinnin alkaessa
kuutiossa ykkst kytettyjen vrien kohdalla ja voimme koostaa niist
npprsti vrilistan sislten kaikki kvantisoitavat
vrit. Koordinaatin kuutiossahan voimme laskea vaikka kaavasta:

r*64*64 + g*64 + b

No niin, sitten itse kvantisointirutiiniin. Funktion tehtv on siis
ottaa kaikki pikselit tietylt vrikuution osakuutiolta ja katsoa mik
vrikomponentti vaihtelee eniten (eli tummimman ja vaaleimman vrisvyn
ero on suurin). Osakuution kuvailemiseksi tarvitsemme tietenkin rajat
kuutiolle, eli tummimman ja vaaleimman mukaan otettavan svyn kustakin
vrikomponentista, eli:

int RedSubspaceBottom;
int GreenSubspaceBottom;
int BlueSubspaceBottom;
int RedSubspaceTop;
int GreenSubspaceTop;
int BlueSubspaceTop;

Nm funktiot ovat siis punaisen, vihren ja sinisen alimmat sallitut
pitoisuudet ja vastaavasti kolme viimeist korkeimmat sallitut. Hyv
idea on laittaa tllaisen kuution tiedot yhteen rakenteeseen. Sekaan
laitamme viel tilaa funktion laskemille kunkin vrisvyn
optimiarvoille, joka siis lasketaan sitten, ett jos kuutio
halkaistaan tmn svyn kohdalta, j molemmille kuution puolikkaille
yht monta vri:

int OptimalRed;
int OptimalGreen;
int OptimalBlue;

Nm kaikki on esimerkiss laitettu structiin BORDERS. Nyt kun meill
on ptev rakenne kuution mrittelemiseksi, niin voimmekin alkaa
pohtimaan kytnnn toimia, mit rekursiivisen kuutionjakajamme tulee
toteuttaa. Idea on seuraava:

1) Tyhjennetn punaisten, vihreiden ja sinisten vrikomponenttien
   laskurit (RedCount[64], GreenCount[64] ja BlueCount[64]).

2) Lasketaan kuution rajojen sisll jokaisen vrikomponentin svyn
   mr looppaamalla kaikki kvantisoitavat vrit lpi ja katsomalla,
   ovatko vrin rgb-arvot parametrina annetun Borders (tyyppi
   BORDERS) sisll ja jos ovat, niin korotetaan vastaavia punaisen,
   vihren ja sinisen laskureita:

   RedCount[red]++;
   BlueCount[blue]++;
   GreenCount[green]++;

   Lisksi tytyy pit yll tietoa pienimmst ja suurimmasta mukaan
   otetusta vrikomponentin svyst, eli tyyliin:

   jos red < PieninPunainen
     PieninPunainen = red
   tai jos red > SuurinPunainen
     SuurinPunainen = red

3) Nyt kun svyt on laskettu, seuraakin jnnittv vaihe. Muutamme
   kunkin vrisvyn mrt sisltvn taulukon juoksevaksi laskuriksi,
   eli tss nette muutoksen:

   Indeksi   0 1 2 3 4 5 6 7 8
   Aluksi    0 0 3 1 0 2 2 0 1
   Nyt       0 0 3 4 4 6 8 8 9

   Tmntyyppinen rutiini toimii:

   for(loop=1; loop<64; loop++)
     RedCount[loop]+=RedCount[loop-1];

   Nyt vrisvytaulukossa on siis tietyn indeksin kohdalla, ei
   suinkaan sen svyn mr, vaan siihen vrisvyyn 'menness'
   olleiden vrien mr. Nyt viel etsitn se 'optimaalinen'
   katkaisukohta kulkemalla kohti taulukon loppua, kunnes olemme
   ohittaneet (noin) puolet pikseleist, eli kun laskuri on suurempi
   kuin RedCount[63]/2 (joka on siis kaikkien mukana olevien vrien
   mr jaettuna kahdella). Onnistuu esim. seuraavasti:

   for(loop=0; loop<63; loop++) {
     if(RedCount[loop+1]>(RedCount[63]/2)) {
       Borders.OptimalRed=loop;
       break;
     }
   }

   lk ihmeess kysyk miksi se on tuollainen. Minulla oli aiemmin
   jotain ongelmia toisenlaisen lhestymistavan kanssa ja tein
   tuollaisen idioottivarman systeemin.

   Tm toistetaan tietenkin kaikille vrisvyille.

4) Nyt vasta kivaa tuleekin. Funktiolle parametrina annettu
   rekursiotason laskuri tarkistetaan ja toimitaan sen mukaan. Jos
   taso on 0, niin olemme siin pisteess, ett kuutioita ei en
   jaeta. Voimmekin kirjoittaa Borders-rakenteen optimaaliset
   vrisvyt (OptimalRed, OptimalGreen, OptimalBlue) lopullista
   paletinmuodostusta odottamaan.

   Esimerkiss funktio saa parametrinaan osoittimen
   BORDERS-taulukkoon, sek laskurin, joka kertoo montako ollaan jo
   tytetty. Niinp tallennus onnistuu varsin vaivattomasti:

   memcpy(&BorderTable[TablesUsed[0]], &Borders, sizeof(BORDERS));
   TablesUsed[0]++;

   Jos on kuitenkin niin onnettomasti, ettei viel olla lopussa niin
   tehtvmme on silti helppo. Etsimme pisimmn akselin vhentmll
   alussa kermmme suurimman ja pienimmn vriarvon sisltvt
   muuttujat toisistaan:

   red=SuurinPunainen-PieninPunainen;
   green=SuurinVihre-PieninVihre;
   blue=SuurinSininen-PieninSininen;

   Esimerkiss nm muuttujat tottelevat lyhyempi nimi sr, br, sg,
   bg, sb ja bb.

   Sitten vain katsotaan mik on pisin akseli ja tehdn uudet
   pikkukuutiot npprsti kahteen pienempn ja jaetaan kuutioiden
   vriavaruudet siten, ett toisen ylrajaksi tulee optimivri-1 ja
   toisen alarajaksi optimivri. Tm yl- ja alarajojen muuttaminen
   siis _vain_ pisimmn vriakselin arvojen kohdalta. Esimerkist
   lydt koodin miten tm on toteutettu. Sitten vain kutsumme
   itsemme molemmille pienemmille kuutioille, yht matalemmalla
   rekursiotasolla ja annamme logiikan hoita loput.

Tst puuttuu viel tarkistus, josko kuutioon kuuluu en vain 1 vri,
jolloin tehdn siit suoraan paletin vri ja palataan rekursiossa
ylspin (ks. esimerkkiohjelma).

Kun itse rekursiivinen funktio on valmis, tytyy viel hieman laittaa
lihaa ymprille. Tarvitsemme ohjelman, joka muuttaa alussa neuvotulla
tavalla varatun vrikuution vrivaraukset (eli ykkset vrin kohdalla)
normaaliksi rgb-triplettitaulukoksi, varaa muistia
BORDERS-rakenteille, joihin optimaaliset vrit tallennetaan, laskee
tarvittavan rekursiotason ja lopuksi hoitaa alussa ykkst ja nollaa
sisltneen vrikuution sisltmn vastaavan sijainnin kvantisoidun
vrin.

Viimeksimainittuun voisimmekin perehty hieman tarkemmin. Kun oikein
kutsuttu rekursiivinen funktio loppuu ja palaamme takaisin, on meill
siististi koko kuutiomme jaettu 256:een (yleens) pienempn
vrikuutioon. Emme kuitenkaan viel tied mik vri tarkoittaa
millkin vlill olevia svyj, joten teemme viel yhden
homman. 

Looppaamme jokaisen BORDERS-rakenteen lpi ja laitamme looppimuuttujan
mukaisen arvon alussa varattuun PaletteArray-muuttujaan kaikkiin
rakenteen ilmoittamiin pikseleihin. Eli piirrmme kuution sisn
SubspaceBottom ja SubspaceTop -muuttujien rajoittamalle alueelle
pienemmn kuution vrill, jonka rakenteen indeksi taulukossa
ilmoittaa ja talletamme rakenteen Optimal-tripletin palettiin indeksin
kohdalle.

Kuten aiemmin mainittiin, joissakin tapauksissa paletti menee siten,
ett ennen rekursiotason 0 saavuttamista on jljell vain 1
vri. Tss tapauksessa BORDERS-rakenteisiin ei talletetakaan tytt
256:tta vri optimisvyineen, joka taas tytyy ottaa huomioon
palettia tehtess. Eli ei mitn looppia vlill 0..256, vaan vlill
0..N, jossa N on se laskuri, jota korotetaan aina kun rekursiivinen
funktio tytt yhden BORDERS-rakenteen. Esimerkkiohjelmassa
'TablesUsed'.

Pseudona se menisi jotenkin nin:

looppaa loop vlill 0...TablesUsed

  looppaa r vlill border[loop].RedSubspaceBottom .. 
                    border[loop].RedSubspaceTop

    looppaa g vlill border[loop].GreenSubspaceBottom ..
                      border[loop].GreenSubspaceTop

      looppaa b vlill border[loop].BlueSubspaceBottom ..
                        border[loop].BlueSubspaceTop

        PaletteArray[r*64*64+g*64+b]=loop;

      end looppaa

    end looppaa

  end looppaa

  paletti[index].red=border[loop].OptimalRed;
  paletti[index].green=border[loop].OptimalGreen;
  paletti[index].blue=border[loop].OptimalBlue;

end looppaa

Sitten vain palauttamaan syntynyt paletti. Alustusfunktiomme on
muuttanut paletinvaraustaulukon taulukoksi, josta voidaan rgb-arvojen
avulla hakea oikea vri (colortorgb = PaletteArray[r*64*64+g*64+b]) ja
palauttanut tarvittavan paletin, jotta vri mys nytt joltakin.

Paljon mainostettu Esimerkkiohjelma lytyy EXAMPLE-hakemistosta
nimell QUANTIZ.C. Koodi on kieltmtt vhintn viisi kertaa
vaikeampaa kuin aiemmat esimerkit, mutta kyll tytyy mynt, ett
kvantisointi asianakaan ei ole lheskn niin helppoa kuin
viivanpiirto.

Kvantisoinnin hydyist voidaan olla monta mielt, mutta yksi asia on
varma. Jos ei kunnollista vrimr omaavaa nytttilaa ole
saatavilla, niin kyll kvantisoitu paletti aina pihitt kotikutoisen
2-3-2 -jrjestelmn (2 bitti punaiselle ja siniselle ja 3 vihrelle).
Lisksi kvantisoinnin tuloksena syntyvn kuution avulla voi tehd
monta kivaa asiaa, kuten esimerkiksi motion blurin (vri on uuden ja
vanhan pikselin rgb-arvojen sekoitus) tai jotain muuta yht
hydyllist.

Yrit sisist asia. Jos ei mene kaaliin sitten milln (= mieti
kauemmin kuin 15 minuuttia), niin ilmoittele hmrist kohdista. Asia
ON vaikea, mutta mielestni selitin sen melkein ymmrrettvsti. Ja
ne, jotka ymmrsivt idean ja tekivt oman rutiinin (vain hullut
kyttvt esimerkkiohjelman koodia ;) saavat vain kirist niit
turvavitn, sill ensi luvussa hieman vaikeampaa kvantisointia!
Silti jo tm tapa, etenkin nopeutensa ja suhteellisen hyvn
tuloksensa ansiosta on varsin hyv.


7.9 Lis paletin kvantisointia - Local K Mean
----------------------------------------------

Niille, jotka nauroivat itsens ulos edellisen luvun
esimerkkiohjelmasta lydn nyt luu kerralla kurkkuun. Tt lhemmksi
tydellisyytt ette pse - ainakaan tss luvussa. Tm algoritmi on
niin hidas, ett edellinen versio on thn verrattuna kuin rasvaamaton
salama. Mys 3Dicassa on selostettu ppiirteittin tm tekniikka ja
kumarrankin kohti Sampsa Lehtosta, sill muokkailen hnen selostustaan
hieman.

Perusidea tmn takana, toisin kuin kuutioihin jakavassa
rekursiivisessa versiossa, on pallomainen
ajattelutapa. Vrikuutiossamme onkin nyt Palloja, joiden sijainti on,
kuten edellisesskin, vrin rgb-arvo. Koko taasen mrytyy sen
mukaan, kuinka monta tmn vrist pikseli lytyy kvantisoitavasta
kuvasta. Jos et kyt kuvia tai et jostain syyst halua laskea mukaan
pallojen vetovoimaa, johon niiden koko vaikuttaa, niin vrin mr
kuvassa on aina 1, jolloin asialla ei kaavoissa ole merkityst.

Niden vripallojen seassa liikkuu sitten paletin verran
palettipalloja, eli yleens 256 kappaletta. Nill palloilla ei ole
kokoa. Vripallot vetvt puoleensa nit kelluvia palloja sen mukaan,
kuinka suuria ne ovat ja nm paletin vrej esittvt pallukat
liikkuvat sitten niden mukana.

Vitsin on se, ett vrit ovat kuin palloja vetvi kappaleita ja
palettipallot pyrkivt sijoittumaan optimaaliseen paikkaan
vripallojen vliin. Koska jokaisella kerralla pallot liikkuvat vain
hieman, tulee kertoja luultavasti aika useita, ennenkuin palettipallot
ovat saavuttaneet optimaalisen sijaintinsa, joista tulee sitten
kvantisoidun paletin rgb-arvot. Pallojen yhteenlaskettua liikett
kytetnkin laskemaan sit, milloin pallot ovat tarpeeksi lhell
parhaita sijaintipaikkojaan (=liike edelliseen pient). Mit pienempi
liikkeen pit vuorolla olla loppumisen tapahtumiseksi, sit kauemmin
homma kest ja sit parempi tulos tulee.

Koska vripallot vetvt vain lhint palettipalloa, niin jokin pallo
voi jd ilman vetovoimaa. Tss tapauksessa pallo heitetn jonkin
vrin lhelle tai kohdalle, jotta tmkin vrlle tielle eksynyt vri
saadaan kyttn.

Ja kuten Ilkan editoima selostuskin tekee, menemme sitten teknisempn
puoleen. Niin tein minkin tt opetellessani, joten lk hvetk
lukea tt ennenkuin yrittte tehd oman versionne rutiinista.

Kvantisoinnin aluksi teemme histogrammin, eli kyrn, joka ilmoittaa
kunkin vrin mrn kuvassa. Esimerkiss kytmme sanan kokoista
laskuria 15-bittisille pikseleille (5 bitti jokaiselle
vrikomponentille), jolloin taulukon koko on 2^15 * 2 = 65536 tavua.
Nollaamme sen aluksi ja sitten korotamme jokaista tietyn vrin
esiintym kohti histogrammin tt kohtaa yhdell. Tietyn
rgb-tripletin sijaintihan on taas r*32*32 + g*32 + b.

Seuraavana sitten teemme taulukon niist vreist, joita todella
kuvassa on. Tallentaa tytyy rgb-tripletin lisksi jokaisen vrin
mrn, jonka saamme nyt histogrammista, joka taasen on 0 jos ei
tietty vri ole lainkaan. Lehtonen suosittelee seuraavanlaista
rakennetta:

typedef struct {
  unsigned char R; /* vriarvo */
  unsigned char G; /* vriarvo */
  unsigned char B; /* vriarvo */
  unsigned long count; /* Vrimr kuvassa */
} colorListStruct;

colorListStruct colorList[32768];

Muistia sst tietenkin mys jos laskee vrit ja varaa sitten
staattisesti muistia systeemille:

colorListStruct colorList=
  (colorListStruct *)malloc(sizeof(colorListStruct)*colors);

Lisksi tytyy viel tallettaa kaikkien eri vrien mr kuvassa,
vaikka muuttujaan colorListCount. Sitten seuraavana peruspaletti:

unsigned long palette[256][3]; /* 3 = R,G & B */

Ja muuttujien lisminen vain lisntyy... Teemme viel
vrilaskuritaulukon, johon summaamme palettipalloa kutsuneiden vrien
rgb-arvot kerrottuna vrin mrll. Tarvitsemme siis suht' suuren
lukualueen. Ja sitten viel laskuri vrien yhteismrlle.

unsigned long colorSum[256][3]; /* 256 vri, 3 = R,G & B */
unsigned long colorCount[256]; /* Voidaan yhdist kyll
                                  colorSummiinkin */

Ja lopuksi viel pisteen i:n plle liskisemme laskurin, joka
laskee paletin muutoksen edelliseen.

unsigned long variance;

Sitten vain kvantisoimaan. Jlleen rankasti kopioituna Sampsalta
tarvittavat askeleet. Mits teki niin hyvn jutun tst. :) Eli itse
kvantisointirutiini:

  1) colorSum ja colorCount -laskurien nollaus ja paletin tytto
     colorList:in ensimmisill (256:lla) vrill.

  2) Lpikydn colorList:in vrit. Vrien mrhn lytyi
     muuttujasta colorListCount, kuten aiemmin kerrottiin.
     Loopataan c vlill 0 .. colorListCount-1

      a) Otetaan colorList:ist vri c

      b) Etsitn lhin vri palette-muuttujasta. Tuloksena numero
         vlill 0..256. Etisyys avaruudessahan on r- g- ja
         b-etisyyksien neliiden summan nelijuuri. Eli 

         delta_r = abs( r2-r1 )
         delta_g = abs( g2-g1 )
         delta_b = abs( b2-b1 )

         sqrt( delta_r^2 + delta_g^2 + delta_b^2 )

         Meidn tytyy loopata joka vri ja laskea tm etisyys ja
         verrata sit siihen menness lytyneeseen lyhimpn 
         etisyyteen ja jos uusi vri on lhempn tallennamme tmn
         numeron ja etisyyden ja jatkamme.

         Optimointikikkoina se, ett koska toinen potenssi on aina
         positiivinen, putoaa itseisarvo (abs) pois. Ja koska

         a^2 < b^2  <=>  a < b

         Niin nelijuuriakaan ei tarvita. Esimerkkiohjelman
         Dist-funktion ydin on seuraava:

         for(loop=0; loop<Wanted; loop++) {
           dist=Dist(r, g, b,
                     Palette[loop][0], Palette[loop][1], Palette[loop][2]);
           if(dist<shortest) {
             shortest=dist;
             sl=loop;
           }
         }

         Jossa dist vain palauttaa tuon delta_r^2 + delta_g^2 + delta_b^2.

      c) Listn lhimmn vrin colorSum-taulukkoon vripallon
         rgb-arvot kerrottuna vripallon pikseleiden mrll (count
         kappaletta tt vrisvy). (x=lhin vri, c=looppi)
         
         colorSum[x][0] += colorList[c].R * colorList[c].count;
         colorSum[x][1] += colorList[c].G * colorList[c].count;
         colorSum[x][2] += colorList[c].B * colorList[c].count;

      d) Sitten tytyy viel tallettaa montako kertaa niit rgb-arvoja
         sinne ynnttiinkn.

         colorCount[x]+=colorList[c].count;

  3) Nollataan liikelaskurimuuttujamme variance.

  4) Kydn lpi kaikki vrit peruspaletista (c = 0..255)

      a) Jos vrin vrilaskuri colorCount on suurempi kuin nolla, niin
         vri oli lhinn ainakin yht vripalloa. Nyt vain laskemme
         keskiarvon kaikista kutsuneista vreist ottamalla keskiarvon
         niist. Koska colorSum sislt aina n kappaletta vrin c
         rgb-arvoja ja colorCount sislt tmn luvun n niin
         keskiarvo saadaan yksinkertaisesti:

         palette[c][0] = colorSum[c][0] / colorCount[c];
         palette[c][1] = colorSum[c][1] / colorCount[c];
         palette[c][2] = colorSum[c][2] / colorCount[c];

         Jos et oikein ymmrtnyt ideaa, niin otetaan
         esimerkki. Paletin vri 5 on lhinn kahta vripalloa:
         palloja A = (10, 20, 0) ja B = (10, 20, 5). Vri A on
         kvantisoitavassa kuvassa 100 ja vri B 200.

         Rutiinimmehn ensin lis colorSum:iin vrin A rgb-arvot
         kerrottuna vrin A mrll kuvassa:

         colorSum[5][0] = A.R * A.count = 10 * 100 = 1000
         colorSum[5][1] = A.G * A.count = 20 * 100 = 2000
         colorSum[5][2] = A.B * A.count = 0 * 100 = 0
         colorCount[5] = 100

         Nyt meill on siis vrin A rgb-arvot 100-kertaisena
         tallessa. Koska vri B on kaksinkertaisesti, saamme sen
         200-kertaisena summataulukkoomme:

         colorSum[5][0] = 1000 + 10*200 = 3000
         colorSum[5][1] = 2000 + 20*200 = 6000
         colorSum[5][2] = 0 + 5*200 = 1000
         colorCount[5] = 100 + 200 = 300

         Nyt laskemme sitten lopullisen vrin paletille:

         palette[5][0] = 3000 / 300 = 10
         palette[5][1] = 6000 / 300 = 20
         palette[5][2] = 1000 / 300 = 3.33...
       
         Huomaamme, ett koska vri B oli kaksi kertaa enemmn, on
         rgb-arvokin lhempn vri B. Nin siis se vri, jota on
         kaikkein eniten, vaikuttaa suurimpana uuden vrin
         rgb-arvoihin. Helppoa, eik totta?

      b) Mittaamme paletin muutoksen alkup. vriin:

         temp = 0;
         temp += abs( R - palette[c][0] );
         temp += abs( G - palette[c][1] );
         temp += abs( B - palette[c][2] );
         variance += temp;

      c) Kirjataan vri lopullisesti uuteen palettiin.

         palette[c][0] = R;
         palette[c][1] = G;
         palette[c][2] = B;

  5) Nyt uusi paletti on taas generoitu ja muutos muuttujassa
     variance. Nollaamme nyt colorSum- ja colorCount -taulukot.

  6) Jos variance-muuttujan kertoma paletin muutos on viel yli
     sallitun rajan, niin hyppmme kohtaan 2. Jos taas olemme jo
     sallitun rajan alla, niin lopetamme. Tten mit pienempi
     MAX_VARIANCE on, sit useammin pyrimme looppia ja sit kauemmin
     tm kest.

Siinp se. Vaan ei kuitenkaan. Hitain osuus nimittin alkoi
nyt. Kvantisoitu paletti on kiva, vaan miss onkaan toivottu
vrikuutio? Tai edes oikeat indeksit kullekin histogrammin vrille? Ei
missn. Paletti on optimaalinen, mutta ei kerro lainkaan, mitk vrit
sit ovat lhinn. Jos haluamme tiet jollekin rgb-arvolle lhimmn
vrin tytyy meidn yksinkertaisesti loopata lvitse paletin kaikki
256 vri ja selvitt mihin etisyys on lyhin. Ja koska haluamme
vrikuution outoihin tarkoituksiimme, merkitsee se tss tapauksessa
256:n vrin lpikynti 32768 kertaa...

Siit vain laskemaan pojat. :) Fiksu idea voisikin olla tallentaa 64
kilon tulos tiedostoon, ettei sit tarvitse pelin kynnistyess
laskea. Ssttte hermojanne kummasti. Tosin, itse kytin kuutta
bitti vrille, jolloin sain huimaavan 262144-tavuisen vrikuution. =)

No nyt jokainen varmasti osaa tehd tuosta toimivan
kvantisointisysteemin. Ja esimerkkisorsa lytyy sitten
tiedtte-kyll-mist hakemistosta nimell QUANT2.C. Ja hyv joulua ja
onnellista uutta vuotta vain kaikille, sill tss kohtaa saan
kakkosversion valmiiksi, juuri tapaninpivn!


7.10 VESA 2.0, rakenteet
------------------------

Ohhoh, vaikuttaa taas silt ett on aika alkaa puurtamaan kun
kesloman viimein alettua minunkin osaltani (nelj ekaa viikkoa
kestiss), en en keksi hyvi selityksi myhstymiselle. Eli
homman nimi on SVGA-ohjelmointi ja tavan nimi VESA 2.0. Lhdekoodiakin
tulee juuri sopivasti jotta kauan toivottu asia, 640x480 -kokoisten
PCX-kuvien lataus ja nyttminen ruudulla onnistuu. Ja jotta homma
olisi hieman haastavampi, muutamme 256-vrisen paletillisen PCX-kuvan
viel 16-bittiseksi ennen nyttmist.

Vaan ensin tongimme hieman menneisyytt. Ihmisten alkaessa hiljalleen
kypsymn 640x480 16-vriseen tilaan alkoivat nytnohjainvalmistajat
tekemn nyttkortteja, jotka tukivat 640x480-tilaa 256
vrill. Sitten pikkuhiljaa jokaiselta valmistajalta alkoi tippumaan
uusia kortteja, jotka pystyivt yh parempiin nytttiloihin ja olivat
mahdollisimman toimimattomia toistensa kanssa. Samoin on asian laita
nykynkin, suoraan korttia ohjelmoimalla pstn hyviin nopeuksiin,
mutta valitettavasti ei ole kovin mukavaa jos ohjelmasi toimii esim 2%
maailman SVGA:n alla toimivasta konekannasta, toistaiseksi.

Tilanteen muuttuessa yh sekavammaksi markkinoilla htiin riensi
VESA-niminen standardointijrjest (Video Electronics Standards
Assocation tjsp.) ja luotiin yhteninen rajapinta jota kytten
voitaisiin ohjelmoida kaikkia sit tukevia kortteja. Ja kehityksen
kehittyess VESA on muodostunut varsin suosituksi tavaksi kytt
korkeampia nytttiloja. Etenkin versio 2.0 tarjoaa lhes lymttmn
tavan tehd nyttvi graafisia sovelluksia. Ja miten tm meihin
liittyy? No me tietenkin opettelemme kyttmn tt rajapintaa!

Heti alkuun totean, ett nm kappaleet ksittelevt VESA 2.0:llaa
erittin puutteellisesti. Oletan ett nytnohjaimesi on VESA
2.0-yhteensopiva ja tukee juuri mrtty tilaa, 640x480 16-bittisill
vreill. Jtn 90% standardin funktioista ja mahdollisuuksista
kyttmtt. Tydellisen, lhes 1400-rivisen englanninkielisen
dokumentaation lyt esim. MBnetist nimell VBE20.ZIP tai Scitech
softwaren kotisivuilta, joka dokumenttia tietkseni levittkin,
osoitteessa www.scitechsoft.com. Scitech Display Doctor, eli entinen
UniVBE on mys SE ohjelma jos nytnohjaimesi VESA 2.0-tuki on
vajaavainen (esim. oman korttini 20 VESA 2.0-nytttilaa laajenevat
tuon myt 58:aan).

Toisaalta kun VESA 2.0:llan info-rakenteen osaa ja ymmrt
moodi-infon rakenteen ja tiet miten VESA-yhteensopiva tila
asetetaan, pystyy tekemn lhes millaisen ohjelman tahansa, uupumaan
j vain mahdollinen hardware-tason tuki skrollaukselle ja muulle
vastaavalle (jota todellinen guru ei tietenkn tarvitse).

Tm luku kattaa kahden olennaisimman VESA 2.0:llaan liittyvn
rakenteen mritelmn ja kaikkien kenttien selostuksen. Myhemmiss
luvuissa tutkimme hieman kytt. Tss ensimminen, ns. VESA
information struct, suoraan VESA-enginestni revittyn:

/* DJGPP laajentaa esim. tavun kokoiset rakenteen kentt 4-tavuisiksi
   nopeuden takia ilman tt mrett, jos tm puuttuu
   palautettavista rakenteista on tieto tysin pin seini. Voit
   kokeilla. */
#define PACKED __attribute__ ((packed))

typedef unsigned long int dword; /* Kaksoissana 4 tavua */
typedef unsigned short int word; /* Sana 2 tavua */
typedef signed long int s_dword; /* Etumerkillinen kaksoissana 4 tavua */
typedef signed short int s_word; /* Etumerkillinen sana 2 tavua */
typedef unsigned char byte;      /* Tavu */
typedef unsigned bit;            /* Bitti */

typedef struct {
  byte sign[4]            PACKED;
  word version            PACKED;
  dword OEMstring         PACKED;

  bit fixedDAC : 1        PACKED;
  bit VGAcompatible : 1   PACKED;
  bit RAMDACtype : 1      PACKED;
  bit reserved1 : 29      PACKED;

  dword videomodeptr      PACKED;
  word totalmemory        PACKED;

  word OEMsoftwarerev     PACKED;
  dword OEMvendornameptr  PACKED;
  dword OEMproductnameptr PACKED;
  dword OEMproductrevptr  PACKED;
  byte reserved2[222]     PACKED;
  byte OEMdata[256]       PACKED;
} VesaInformation;

sign tytetn merkkijonolla "VESA", jos nytnohjaimesi tukee
VESA-tiloja. Jos halutaan ns. laajennettu tietokentt, tm tulee
ennen kutsua asettaa olemaan "VBE2", jolloin VESA 2.0-toteutus
ymmrt tytt uudet VESA 2.0:llan mukanaan tuomat kentt.

version taas on BCD-muotoinen versionumero (kytetn vain heksoja
0h-9h, eli versio voi olla 0000h - 9999h). Ylemmt kaksi heksaa ovat
suurempi versionumero (haluamme sen olevan vh. 02h) ja alempi
pienempi (edellisess versiossa 1.2 olisi luku 0102h).

OEMstring on reaalitilan (seg:off, eli segmentti ja offsetti)
osoitin NULL-ptteiseen valmistajan mrittelemn merkkijonoon.
OEMsoftwarerev kertoo version-kentt vastaavasti versiotietoa, ja
OEMvendornameptr, OEMproductnameptr ja OEMproductrevptr taasen
ovat reaalitilan osoittimia valmistajan ja tuotteen nimeen sek
tuotteen versionumeroon. VESA 2.0-yhteensopivien toteutusten tulisi
laittaa merkkijonot OEMdata-alueelle (ei ROM-muistiin tai muuallekaan
infoblokin 512 tavun ulkopuolelle), jolloin kopioidessasi infoblokin
ohjelman omaan datasegmenttiin DOS-muistialueelta voidaan nm
pointteritkin muuttaa toimiviksi suojatun tilan osoitteiksi nytlle
tulostamista varten.

fixedDAC on 0 jos DAC on aina kuusi bitti per vrikomponentti, ja 1
jos se on vaihdettavissa kahdeksaan bittiin komponenttia kohden. DAC
hoitaa palettia.

VGAcompatible on 0 jos kontrolleri on VGA-yhteensopiva ja 1 jos
ei. Eli VGA-portit toimivat ja normaalit videotilat mys. Varmaan
jokaisessa PC-nytnohjaimessa 0.

RAMDACtype on 0 jos RAMDAC on "normaali", 1 tarkoittaa ett
VESA-keskeytyksess 09h tulee asettaa blank bitti. Kytnnss tm ja
fixedDAC koskevat sinua vain jos kytt paletti-tilaa (256 vri),
johon en tss luvussa ainakaan puutu. Blank-bitti palettia
asetettaessa est "lumisateen" ruudulla ajoittamalla paletin
muutokset muualle kuin piirron ajalle.

videomodeptr on kaikkein trkein info version-kentn jlkeen, sill se
osoittaa tilojen numerot sisltvn listaan, joka lopetetaan -1:ll
(0xFFFF). Ohjelman tehtv on tarkistaa onko jokin moodi todella
olemassa, esimerkiksi UniVBE asettaa minulla tuonne tiloja joita ei
ole olemassa. Lis virheentarkastuksesta alempana.

totalmemory kertoo kytss olevan muistin mrn 64kilon palasissa,
eli 1 mega esimerkiksi on 16.

No nyt viel se toinen trke rakenne, moodi-info, jota voi pyyt
halutulle tilalle jos vain tiet sen numeron. Ja ne numerothan
lytyivt jo info-blokista, joten nyt vain rakennetta tutkimaan. Tm
rakenne on hirvi:

typedef struct {
  bit modesupported : 1         PACKED;
  bit reserved : 1              PACKED;
  bit TTYsupported : 1          PACKED;
  bit colormode : 1             PACKED;
  bit graphicsmode : 1          PACKED;
  bit notVGAcompatible : 1      PACKED;
  bit notVGAwindowmemory : 1    PACKED;
  bit linearmodeavailable : 1   PACKED;
  bit reserved1 : 8             PACKED;

  bit Arelocatablewindows : 1   PACKED;
  bit Areadablewindow : 1       PACKED;
  bit Awritablewindow : 1       PACKED;
  bit reserved2 : 5             PACKED;

  bit Brelocatablewindows : 1   PACKED;
  bit Breadablewindow : 1       PACKED;
  bit Bwritablewindow : 1       PACKED;
  bit reserved3 : 5             PACKED;

  word windowgranularity        PACKED;
  word windowsize               PACKED;
  word windowAsegment           PACKED;
  word windowBsegment           PACKED;
  dword windowfunctionptr       PACKED;
  word bytesperscanline         PACKED;

  word horizontalresolution     PACKED;
  word verticalresolution       PACKED;
  byte characterwidth           PACKED;
  byte characterheight          PACKED;
  byte planes                   PACKED;
  byte bitsperpixel             PACKED;
  byte banks                    PACKED;
  byte memorymodel              PACKED;
  byte banksize                 PACKED;
  byte imagepages               PACKED;
  byte reserved4                PACKED;

  byte redbits                  PACKED;
  byte redshift                 PACKED;
  byte greenbits                PACKED;
  byte greenshift               PACKED;
  byte bluebits                 PACKED;
  byte blueshift                PACKED;
  byte reservedbits             PACKED;
  byte reservedshift            PACKED;

  bit programmablecolorramp : 1 PACKED;
  bit reservedbitsusable : 1    PACKED;
  bit reserved5 : 6             PACKED;

  dword physicalbasepointer     PACKED;
  dword offscreenmemoryoffset   PACKED;
  word offscreenmemorysize      PACKED;
  byte reserved6[206]           PACKED;
} VesaModeInformation PACKED;

modesupported kertoo onko tila edes tuettu. (1 = tosi, 0 = eptosi)

TTYsupported kertoo tukeeko toteutus tekstin tulostusfunktioita

colormode on 1 jos tila on vritila, 0 jos mustavalkoinen

graphicsmode on 1 jos tila on grafiikkatila, 0 jos tekstitila

notVGAcompatible on 1 jos tila ei tue VGA-rekistereit ja IO-portteja

notVGAwindowmemory on 0 jos banked-tilat ovat mahdollisia, 1 jos eivt

linearmodeavailable on 1 jos LFB on, 0 jos ei. Lis termeist alempana

A- ja B-alkuiset kolme kentt, relocatablewindows, readablewindow ja
writeable window kertovat, voiko ikkunaa A tai B (riippuen nimen
alkukirjaimesta) siirt, voiko sit lukea ja voiko siihen
kirjoittaa. Nm tiedot ovat ns. banked-tiloille, jossa videomuistia
katsotaan yleens reaalitilassa olevista "ikkunoista", joita voidaan
sitten siirrell. windowgranularity kertoo kilotavuina, kuinka
pieniss askelissa ikkunaa voidaan siirt nyttmuistissa ja
windowsize kuinka suuri ikkuna on. windowAsegment ja B-vastaava
kertovat CPU-osoiteavaruudessa ikkunoiden osoitteet, A000h:ta
nytti minulla, eli ihan normaali VGA-muistisegmenttihn tuo yleens
on (muista vain ett segmentti kerrotaan kuudellatoista jotta saadaan
suojatun tilan offsetti, ja muista ett selektori on _dos_ds, eli
pit kytt movedata, _farpoke* tai dosmemput-komentoja).

En puutu banked-tiloihin tss luvussa, vaan oikaisen ja kerron
linear-tiloista. Banked-tiloissa tarvitaan paljon logiikkaa jotta
voidaan selvitt millaisissa ptkiss pit nyttmuistissa liikkua,
kumpaa ikkunaa A vai B voi liikuttaa vai voiko molempia, saako niihin
kirjoittaa jne.  Banked-tilassa kaksoispuskurin kopiointi ruudulle
tehdn siten, ett siirretn ikkuna muistin alkuun, kirjoitetaan
ikkunan pituuden verran kaksoispuskurista, siirretn ikkunaa
eteenpin granularity-muuttujan sallimissa rajoissa pitkin
nyttmuistia (esim. 64 kiloa kerrallaan), kirjoitetaan seuraava pala
jne. 

Tarvittavalla lykkyysosamrll ja ehk hitusella englannin kielen
taitoa varustettu yksil kyll pystyy tekemn banked-tuen
halutessaan. windowfunctionptr on reaalitilan osoitin (seg:off)
rutiiniin, joka vaihtaa nopeasti ikkunan sijaintia. Valitettavasti
tllaisen funktion kutsumiseen tehtvt valmistelut ovat sen verran
massiivista luokkaa (lue: en jaksa alkaa perehtymn asiaan), ett en
ala niit tss esittelemn.

bytesperscanline on hydyllinen tieto mys LFB:t kytettess. Se
kertoo montako tavua yksi rivi nytttilassa vie. Yleens on
turvallista olettaa ett se on suoraan vaakaresoluutio kerrottuna
tavuilla per pikseli, mutta joillakin korteilla, esim. Matroxilla
kuulemma nuo joskus ovat jotain ihan muuta. Tm poistaa sinulta
mahdollisuuden kytt koko kaksoispuskurin kopioimisen kerrallaan
nyttpuskuriin, mik tietenkin hidastaa ohjelmaa. Kannattaa ehk
tehd kaksi piirtofunktiota, joista vain toinen ottaa huomioon tmn
seikan.

horizontalresolution ja verticalresolution kertovat tilan vaaka- ja
pystyresoluution. Tekstitilassa nm arvot ovat rivein,
characterwidth ja characterheight -muuttujien ilmoittaessa merkin
leveyden ja korkeyden pikseleiss.

planes kertoo muistitasojen mrn tss tilassa. Kytnnss nill
on vli vain 16-vrisiss tiloissa ja mode-x: vastaavissa
VESA-tiloissa. Normaalisti tm on 1.

bitsperpixel kertoo montako bitti pikselille tss tilassa on
varattu. 16-vrisess tm on 4, 256-vrisess 8, 16 tai 15
highcolor-tiloissa ja 24 tai 32 truecolor -tiloissa. Jos kytt
tasoja, on bittien mr / taso yleens suoraan bitit/tasojen_mr,
eli 4-tasoinen 256-vrinen tila olisi 2 bitti per taso.

banks kertoo montako scanline-bankkia kussakin tilassa
on. Esim. CGA:ssa kaksi ja Herculeksella nelj. Yleens tm on 1

memorymodel kertoo mink tyyppinen muistimalli tilassa
on. Muistimalleja on 8:

00h Tekstitila
01h CGA
02h Hercules
03h Plane-tyyppinen
04h Pakattu pikseli (yleinen 256-vrinen)
05h Non-chain 4, 256-vrinen
06h Suora vrimalli (jota me tulemme kyttmn)
07h YUV (vrimalli, tyyliin RGB)
08h-0Fh = Varattuja VESA:n mriteltviksi
10h-FFh = Varattuja valmistajan mriteltviksi

banksize kertoo aiemmin kuvattujen scanline-bankkien mrn. Jos
jollakulla on jotain aavistusta nist scanline-bankeista, tasoista ja
plane- pakatun pikselin, non-chain ja YUV-muistimalleista, olen
kiinnostunut tietmn, tss moodi-info -rakenteessa on minulle
ainakin lhes outoja kentti ihan tarpeeksi.

imagepages kertoo montako ylimrist ruutua nyttmuistiin mahtuu,
eli ohjelma voi ladata useampia ruudullisia muistiin ja vaihdella
niden vlill.

Sitten tuleekin varmaan meit eniten kiinnostava osa. Suoran
vrimallin tiloissa (memorymodel = 06h) 15-, 16-, 24- ja 32-bittisiss
tiloissa kytetn aina tietty mr bittej ilmaisemaan kutakin
vrikomponentteja. Esimerkiksi 16-bittisess tilassa yleens on 5
bitti punaiselle, sitten 6 vihrelle ja viel 5 siniselle. Nin
meill on 2^5 = 32 svy punaiselle ja siniselle ja 2^6 vihrelle
(jonka eri svyj silm parhaiten erottaa). Jotta voimme koota ne
meidn tytyy viel shiftata bittej oikeille paikoilleen:

#define RGB16(r,g,b) ((r<<11) + (g<<5) + b)

Shiftaukset ja bittien mr vrikomponenttia kohden vaihtelevat ja
moodi-infossa jokaiselle vrikomponentille on mritetty bittien mr
ja shiftaus (redbits, redshift, bluebits...). Jos ohjelma ottaisi
kaikki mahdolliset yhdistelmt huomioon, olisi homma luultavasti
tuskallisen hidasta. Onneksi kuitenkin on varsin turvallista olettaa
seuraavia asioita:

15-bittinen vrimalli on 5:5:5, eli kaava on (r<<10)+(g<<5)+b

16-bittinen vrimalli on 5:6:5, eli kaava on (r<<11)+(g<<5)+b

24-bittinen vrimalli on 8:8:8, eli kaava on (r<<16)+(g<<8)+b

32-bittinen vrimalli on sama kuin 24, mutta 8 ylint bitti jvt
kyttmtt ja nin ollen kaikkien pikselien muistiosoitteet ovat
neljll jaollisia, paljon helpompaa kuin 24-bittisten tilojen
kolmella jaolliset.

Joskus tietenkin poikkeuksia voi olla ja on parasta tarkistaa ennen
moodin asettamista, tsmvtk shiftaukset ja bitit oletuksiin ja
tulostaa vaikka virheilmoitus, jos nin ei ky. Jos haluaa ett
ohjelma varmasti toimii kaikilla korteilla, voi tehd yleisluontoisen,
mutta kyllkin lyttmn hitaan muuttujia kyttvn systeemin.

Tiivistelmn shift- ja bits-kentist, ett tee pts bitsperpixelin
mukaan ja tarkista ennen moodiin siirtymist viel ett shiftaukset
ovat oikeita moodi-infossa ja jos ne eivt tsm, l asetakaan
tilaa.

programmablecolorramp kertoo voiko vriramppia ohjelmoida. Jos arvo on
0 ei sit voi muuttaa, mutta jos se on ohjelmoitava, voit st
punaiselle, vihrelle ja siniselle haluamasi muotoisen
vrirampin. Voit esim. tehd siten, ett jos punaiselle on 64 svy
niin sen sijaan ett 0 on ei yhtn ja 64 on maksimi, saavutetaan
maksimi jo 32:ssa ja loppu on tasaisen punaista. Tst on hyty
kytnnss vain gamma-korjauksessa ja joissakin
erikoisefekteiss. Jos asia kiinnostaa kannattanee tutustua
VBE20.TXT:n keskeytykseen 09h.

reservedbitsusable on 1 jos yli jvt (esim 32-bittisess tilassa
yleens 8 ylimmist) bitit ovat kytettviss.

physicalbasepointer on LFB:n aloitusoffset fyysisess
muistiavaruudessa (eli alkaen muistin alusta aina maksimiin 4
gigaan). Tm on trkeimpi kentti jos et kyt banked-tiloja.
Jos LFB ei ollut tuettu tm kentt on 0.

offscreenmemoryoffset kertoo kuinka pitkll nyttmuistin alusta on
ohjelman kytettviss oleva ylimrinen nyttmuisti ja
offscreenmemorysize kertoo montako kilotavua siin on.

Huhhuh. Tm ky tyst. Pahoittelen jos niss kenttien selostuksissa
on jotain epselvyyksi, ilmoitathan asioista joita et ymmrr
minulle, en tied kuinka selv tm on sellaiselle joka ei viel
VESA-tiloja ole ohjelmoinut.


7.11 VESA 2.0, ensimmiset keskeytykset
---------------------------------------

No niin, struktit ovat siis hallinnassa? Sitten hommiin. VESA 2.0:llan
kytt on tiivistettyn seuraavanlaista:

1) Otetaan talteen info-structi ja tarkistetaan ett kortti on
   VESA 2.0-yhteensopiva (versionumerosta)

2) Luetaan structin lopusta tuettujen VESA-tilojen numeroiden lista

3) Tutkitaan mik tuetuista numeroista on se tila jonka haluamme (eli
   mennn yksitellen lvitse kaikki, pyydetn moodi-info, tutkitaan
   ja jos ei ole oikea, jatketaan eteenpin).

4) Lydettymme oikean asetetaan tila.

Sitten hieman VESA-rajapinnan toiminnasta. VESA 2.0 on, kuten
edeltjns, vanhan video-keskeytyksen 10h alle tehty joukko
keskeytyksi. Tunnus VESA-keskeytykselle on arvo 4Fh
ah-rekisteriss. Al-rekisteriin asetetaan halutun toiminnon numero
(joista kolme ensimmist, 00h-02h selitetn tss). Systeemi toimii
mys 16-bittisiss sovelluksissa, mik tarkoittaa kytnnss sit,
ett kun keskeytykselle annetaan muistialueen osoite, jonne
informaatiorakenne tulee kirjoittaa, tytyy sen sijaita
perusmuistissa. Keskeytyksen kutsun jlkeen ax:ss palautetaan
seuraavanlainen palautusarvo:

AL == 0x4F: Funktio on tuettu
AL != 0x4F: Funktio ei ole tuettu

AH == 0x00: Funktiokutsu onnistunut
AH == 0x01: Funktiokutsu eponnistui
AH == 0x02: Softa tukee funktiota, mutta rauta ei
AH == 0x03: Funktiokutsu virheellinen nykyisess nytttilassa

Kaikki AH:n arvot muut kuin 0 tytyy tulkita yleisin virhetiloina,
sill myhemmiss VESA:n versioissa virhemrittelyj saattaa tulla
lis. Jotta voisimme kytt normaalien rekisterien lisksi
tarvittavia segmenttirekisterej, kytmme __dpmi_int:i ja rakennetta
__dpmi_regs. Alla esimerkkimalli VesaInt-komennosta, jolle annetaan
vain toiminnon numero.

int VesaInt(byte function, __dpmi_regs *regs) {
  regs->h.ah=0x4F;
  regs->h.al=function;
  __dpmi_int(0x10, regs);

  if(regs->h.al != 0x4F) {
    puts("Funktio ei tuettu");
    return -1;
  }

  switch(regs->h.ah) {
    case 0x00:
         break;
    case 0x01:
         puts("Funktiokutsu eponnistui!");
         return 1;
    case 0x02:
         puts("Softa tukee funktiota, mutta rauta ei!");
         return 2;
    case 0x03:
         puts("Funktiokutsu virheellinen nykyisess videotilassa!");
         return 3;
    default:
         puts("Tuntematon virhe!");
         return 4;
  }

  return 0;
}

__dpmi_regs-rakenteen h-kentn alta lytyy tavun kokoiset palat ja
x-osasta 16-bittiset rekisterit (ainakin). Muita emme tarvikaan.

Nyt olemme tarpeeksi evstettyj kutsumaan funktiota 0h, joka
palauttaa VESA-infoblokin. Ah tytetn 4Fh:lla, al asetetaan nollaksi
ja es:di asetetaan osoittamaan puskuriin minne infoblokki sijoitetaan.

Jotta saisimme info-struktuurin talteen, tytyy meidn ensin varata
muistia megan alapuolelta tarvittavat 512 tavua (keskeytys jolla info
palautetaan haluaa reaalitilan osoitteen ja tmn takia meidn tytyy
varata DOS-muistia). Sek ohjelman omalta muistialueelta saman
verran tilaa, emme halua ksitell tietoja dos-muistissa jonkin
farpeekb:n avulla. Lisksi sign tytyy asettaa VBE2:ksi, jotta
keskeytys tiet ett haluamme version 2 mukaista tietoa.

Alla esimerkkikoodista pala, joka varaa dos-muistin, asettaa
tarvittavat asiat ja kutsuu keskeytyst:


int VesaInit() {
  __dpmi_regs regs;

  dosbuffer=(dword)__dpmi_allocate_dos_memory(64, (int *)&dosselector);

  if(dosbuffer==-1) {
    puts("Ei tarpeeksi perusmuistia VESA-infoblokille!");
    return 1;
  }

  dosbuffer*=16; /* muutetaan lineaariseksi osoitteeksi (seg*16) */

  vesainfo=(VesaInformation *)malloc(sizeof(VesaInformation));
  memcpy(vesainfo->sign, "VBE2", 4);

  dosmemput(vesainfo, sizeof(VesaInformation), dosbuffer);

  regs.x.es=dosbuffer/16;
  regs.x.di=0;
  if(VesaInt(0x00, &regs)) {
    puts("Virhe VESA-keskeytyksess!");
    return 1;
  }

  dosmemget(dosbuffer, sizeof(VesaInformation), vesainfo);

  if(strnicmp(vesainfo->sign, "VESA", 4)!=0 || vesainfo->version<0x0200) {
    puts("not found!");
    return 1;
  }

  puts("found!");
  return 0;
}

void VesaDeinit() {
  __dpmi_free_dos_memory(dosselector);
}

Kuten selvsti nkyy, homma on varsin helppoa. Varataan muistit,
laitetaan "VBE2"-pala, kopioidaan DOS-muistiin, asetetaan rekisterit,
keskeytys, kopioidaan takaisin omaan muistiin ja se on siin. Tutkimme
palautusarvon ja jos onnistuimme voimme jatkaa moodi-infojen
tiirailuun.

Moodi-infon lukemiseksi vain matkaamme lvitse halutun
alueen. Kaikkein varmin tapa tutkimiseen on hakea tieto
perusmuistista, jos jostain syyst lista ei olisikaan infoblokin
alueella, vaan jossain muualla. Kytmme vain dosmemget:i niin
monesti ett vastaan tulee -1 ja joka arvolle katsomme moodi-infon.
Allaoleva esimerkki etsii 640x480-tilan 16-bittisill vreill ja
asettaa tilan, varaa kaksoispuskurin ja palauttaa sen osoitteen tai
NULL jos ilmaantui virhe. Varsinainen monitoimitykalu, siis.

Ennen kuitenkin tutustumme ksitteeseen LFB, sill se on se mit
kytmme. Edellisiss versioissa kytettiin VGA-muistia, joka osoitti
aina haluttuun palaan videomuistia. Muistiin tytyi siis kyd ksiksi
64 kilon palasissa, mik oli varsin tuskallista touhua. Versio 2.0 toi
kuitenkin mukanaan suojatun tilan kyttjille uuden asian,
LFB:n. Systeemi on sellainen, ett videomuisti sijoitetaan jonnekin
osaan muistiavaruutta. Homma on siis sama kuin osoitteen 0xA0000
kanssa, mutta nyt paikka on yleens jossain 300 megan paikkeilla tai
kauempana ja kokoa on 64 kilon sijasta nyttmuistin verran, omalla
koneellani 4 megaa. 

Osoitteen saimmekin jo infoblokissa, mutta jotta voisimme kytt tt
osoitetta, tytyy muistisuojauksista pst eroon. Tarvitsemme siis
selektorin joka osoittaa halutun muistiosoitteen alkuun ja joka on
asetettu toimivaksi tarvittavan pitklle matkalle, jottemme saa
segmentation faultia muistialueen ohi kirjoittamisen takia
kopioidessamme kaksoispuskuria ruudulle. Alla suoraan jostain pllitty
funktio (kiitoksia tekijlle) mappaamiseen ja mappauksen poistoon:

/* Funktio ottaa fyysisen osoitteen muistiavaruudessa (physaddr) sek
   koon tavuissa (size) ja palauttaa linear-muuttujassa varatun alueen
   lineaarisen offsetin (linear), sek selektorin jota kytetn kun
   halutaan ksitell muistialuetta (segment, tt kytettess offset
   aina 0). Funktio palauttaa 0 jos onnistui, 1 jos ei */
int VesaMapPhysical(dword *linear, s_dword *segment,
                    dword physaddr, dword size) {
  __dpmi_meminfo meminfo;

  meminfo.address = physaddr;
  meminfo.size = size;

  if(__dpmi_physical_address_mapping(&meminfo) != 0)
    return 1;

  linear[0]=meminfo.address;
  __dpmi_lock_linear_region(&meminfo);
  segment[0]=__dpmi_allocate_ldt_descriptors(1);

  if(segment[0]<0) {
    segment[0]=0;
    __dpmi_free_physical_address_mapping(&meminfo);
    return 1;
  }

  __dpmi_set_segment_base_address(segment[0], linear[0]);
  __dpmi_set_segment_limit(segment[0], size-1);

  return 0;
}

Eli kytnnss tarvitaan vain palautettua segmentti, offset segmentin
alla on suoraan (y*leveys+x)*tavuja_per_pikseli, eli mitn lukujen
lisyksi ei tule, kuten asian laita VGA-tilojen kanssa on (0xA0000).
Sitten tietenkin vapautus loppuun:

/* Tm taasen vapauttaa muistin ksittelyyn varatut kahvat, kutsutaan
   kun palataan VESA-tilasta. */
void VesaUnmapPhysical(dword *linear, s_dword *segment) {
  __dpmi_meminfo meminfo;

  if(segment[0]) {
    __dpmi_free_ldt_descriptor(segment[0]);
    segment[0]=0;
  }

  if(linear[0]) {
    meminfo.address=linear[0];
    __dpmi_free_physical_address_mapping(&meminfo);
    linear[0]=0;
  }
}

Hieno homma, vaan mitenks nit kytetn? No nemme kohta senkin,
hieman vain krsivllisyytt. Ensin tutkimme funktiot 01h ja 02h.

01h palauttaa cx-rekisteriss annettavan moodin tiedot, puskurin
ollessa jlleen es:di. Voimme kytt mainiosti alustusfunktiossa
varattua muistialuetta dosbuffer. Kytt on naurettavan helppoa:

/* Palauttaa 1 jos moodi ei ole olemassa */
int VesaGetModeInfo(word mode, VesaModeInformation *info) {
  __dpmi_regs regs;

  regs.x.cx=mode;
  regs.x.es=dosbuffer>>4;
  regs.x.di=0;
  if(VesaInt(0x01, &regs))
    return 1;

  dosmemget(dosbuffer, sizeof(VesaModeInformation), info);

  return 0;
}

Sitten vain tutkimme halutut arvot moodi-infosta ja jos oikea on
kohdalla, asetetaan tila. Keskeytyksen numero on 02h ja bx:ss
annetaan tarvittava tieto moodista. Mukaan pakataan tieto haluammeko
lineaarisen tilan vain banked-tilan ja josko nyttmuisti tulee
tyhjent ennen vaihtoa. Bitit on jrjestelty nin:

0-8   Moodin numero
9-13  Nollaa (sstetty tulevaisuutta varten)
14    0 jos kytetn banked-tilaa, 1 jos lineaarinen, eli LFB-tila
15    0 jos tyhjennetn nyttmuisti, 1 jos ei

eli vaikkapa:

#define MODEFLAG_BANKED   0x0000
#define MODEFLAG_LINEAR   0x4000
#define MODEFLAG_CLEAR    0x0000
#define MODEFLAG_PRESERVE 0x8000

/* Jlleen ei-nolla arvo tarkoittaa virhett */
int VesaSetMode(int mode) {
  __dpmi_regs regs;

  regs.x.bx = mode | MODEFLAG_LINEAR | MODEFLAG_CLEAR;
  if(VesaInt(0x02, &regs))
    return 1;

  return VesaMapPhysical(&vesalfb_linear, &vesalfb_segment,
                         vesamodeinfo[modenum].physicalbasepointer,
                         vesamodeinfo[modenum].bytesperscanline*
                         vesamodeinfo[modenum].verticalresolution);
}

No niin, mits tss enn on jljell. No ihan oikeassa olet, eip
kai mitn. Vai hh? Ai mik? Esimerkki?!? No kai se nyt viel thn
mahtuu. Tydelliset sorsat ja mrittelyt voit kaivaa tiedostosta (kai
nyt flipin osaa tehd kuka tahansa kun tiet selektorin ja
nyttmuistin koon?) VESA20.C. Ja sitten miten homma todella hoidetaan
voit lukea seuraavasta luvusta. Mutta se moodin asetus:

/* Palauttaa 0 jos onnistui */
word * VesaSet640x480_16() {
  VesaModeInformation modeinfo;

  s_word mode=0;
  dword addr=vesainfo->videomodeptr;

  while(mode!=-1) {
    dosmemget(addr, 2, &mode);
    addr+=2;
    if(mode!=-1) {
      /* Jos virhe tulee jatketaan seuraavaan */
      if(VesaGetModeInfo(mode, &modeinfo))
        continue;
      
      if(modeinfo.linearmodeavailable &&
         modeinfo.horizontalresolution==640 &&
         modeinfo.verticalresolution==480 &&
         modeinfo.bitsperpixel==16) {
        if(VesaSetMode(mode, &modeinfo))
          return NULL;

        vesascreen = (word *)malloc(640*480*sizeof(word));
        return vesascreen;
      }
    }
  }
  return NULL;
}

Ja viel se deinitti taitaapi puuttua.

void VesaReset() {
  textmode(0x03);
  VesaUnmapPhysical(vesalfb_linear, vesalfb_segment);
  free(vesascreen);
}

Sitten vain niputetaan kaikki mit on vastaan tullut, listn hieman
suolaa ja nautitaan PCX-kuvan kera. Hyv ruokahalua!


7.12 Miten se todella pitisi tehd
-----------------------------------

Aiemmat kaksi lukua vain raapaisivat pintaa VESA-ohjelmoinnin
saralla. Trkeimmt funktiot kuitenkin on selostettu ja niiden
pohjalta on jo varsin helppoa tehd oma engine. Esimerkkikoodia ei
kannata suoraan kytt, sill se on kytnnss vain omasta
enginestni kokoon parsittu kevytversio, joka sislt tarpeeksi
esimerkkej eri asioiden teosta, jotta oman systeemin teko
helpottuisi. Tss luvussa hieman siit miten jrjestelmn voisi
toteuttaa.

Ensimmiseksi kannattaa erotella VESA-rutiinit jrkeviin
palasiin. Itsellni esimerkiksi yhdess tiedostossa on Vesa-rutiinit
inforakenteiden lukemiseksi muistiin ja olennaisimmat funktiot, kuten
VesaInt. Toinen osa sitten hoitaa graafisen puolen, eli asettaa
halutun nytttilan, ja hoita nytnpivityksen. Luonnollisesti
funktioiden mrittelyt ja rakenteet ovat omissa .h-tiedostoissaan ja
koodi ja muuttujat taas .c-osissa. Ei ole yhtn tyhm idea tehd
kirjastosta yht pakettia, esim libvesa.a, jonka DJGPP:n
lib-hakemistoon sijoittamisen jlkeen voi sisllytt johonkin
ohjelmaan pelkstn parilla #include-lauseella ja -lvesa
-parametrilla.

Toiseksi erittin trke asia on tehd systeemist tarpeeksi joustava,
jotta siit olisi todella jotain hyty. Nykyiselln nytnohjainten
kirjo ja resoluutioiden mr on niin suuri, ett jo tst syyst
VESA-esimerkki lienee ensimmisi tutoriaalin ohjelmia, joka ei tule
koskaan toimimaan kaikilla koneilla. Hyv jrjestelm hoitaa asiat
siten, ett kutsuva ohjelma on tyystin tietmtn siit mit raudassa
on. Unelmasysteemi on sellainen, ett initialisoit moottorin alussa ja
deinitialisoit lopussa. Kytn aikana sinulla on puskuri jonne voit
laittaa grafiikan ja ksky jolla tavara heitetn nytlle. Ja
systeemin tulisi toimia nin vaikka alla ei edes olisi todellista
VESA-yhteensopivaa rautaa.

Miten tmn pystyy sitten saavuttamaan? No initit ja deinitit on
helppo hoitaa, mutta ett viel universaali piirtotapa, vaikka alla
olisi ihan toinen resoluutio ja vrimr kuin mit ohjelma luulee,
onko tm mahdollista? Vastaus on myntv. Eik ratkaisu edes ole
kovin vaikea. Taikasana: funktio-osoittimet (C++:ssalla
virtuaalimetodit ja eri resoluutioiden periyttminen perusluokasta).

Oma systeemini sislt tllaisen muuttujan:

void VESAREFRESH (*VesaScreenRefresh)()=NULL;

Kytnnss VESAREFRESH on vain mritelty tyhjksi (#define
VESAREFRESH), mutta yll esitetty systeemi osoittautuu aika
kytnnlliseksi kun se haluttu tila ei lydykn. Systeemi toimii
nin:

Oletetaan ett pelini on tarkoitus toimia 320x200-resoluutiossa
32-bittisill vreill. Systeemi on unelma, koska yksi pikseli on
dwordin kokoinen (=nopeaa) ja jokainen vrikomponentti on tavun verran
ja ylimmisen tavun jdess tyhjksi. Vaan, ongelmana on, ett vain
hyvin harvalla on 32-bittinen halutun resoluution nytttila. No,
ongelma on helposti ratkaistu: 

Yhden flipin sijasta tehdnkin _useita_ pivitysrutiineja. Yksi
muuntaa vrit 24-bittisiksi lennossa (kytnnss yht nopea kuin aito
32-bittinen tilakin), toinen muuttaa ne 16-bittisiksi, yksi voi jopa
kytt korkean resoluution 32-bittist tilaa emuloimaan joistakin
korteista puuttuvia 320x200-kokoisia korkeavrisi tiloja (oma
Matroxini esim. tukee normaalisti tiloja vain resoluutiosta 640x480
ylspin). Varalle voidaan viel tehd kvantisoitua tilaa tai
harmaasvyj kyttv, 100% VGA-yhteensopiva flippi, joka kytt
256-vrist tilaa. Initin aikana vain asetetaan VesaScreenRefresh
osoittamaan siihen pivitysfunktioon mit asetettu nytttila vastaa.

Ja kun flippi hoitaa kaksoispuskurin muuntamisen sellaiseen muotoon
ett se on nytettviss sill hetkell kytss olevalla parhaiten
oikeaa vastaavalla nytttilalla, ei ohjelman tarvitse kuin piirt
tavara vesascreen-puskuriin, joka on aina saman suuruinen ja jossa on
aina sama vrimr, sek kutsua VesaScreenRefresh-funktiota. Nin
funktion ollessa oikea flippi tulee tavara ruudulle vaikka kyttjll
ei sattuisikaan olemaan 320x200 32bit -tilaa, vaan esim. 320x200
24bit. Ihanaa. Ja tss ptk omasta koodistani:

int VesaLowresInit(int flags) {
  int loop;

  if(!(vesaflag & VESA_INITIALIZED))
    VesaError("Vesa low-resolution mode init", "Engine not initialized!");

  vesascreen=(pointer)Jmalloc(320*200*sizeof(dword));
  memset(vesascreen, 0, 320*200*sizeof(dword));

  for(loop=0; loop<vesamodes; loop++)
    if(vesamodeinfo[loop].linearmodeavailable &&
       vesamodeinfo[loop].horizontalresolution==320 &&
       vesamodeinfo[loop].verticalresolution==200 &&
       vesamodeinfo[loop].bitsperpixel==32) {
      printf("Initializing mode 320x200 32-bit colors...\n");
      vesacurmodeinfo=&vesamodeinfo[loop];
      VesaScreenRefresh=VesaLowres_320x200x32;
      VesaSetMode(loop);
      vesamode=VESAMODE_320x200x32;
      return vesamode;
    }

  for(loop=0; loop<vesamodes; loop++)
    if(vesamodeinfo[loop].linearmodeavailable &&
       vesamodeinfo[loop].horizontalresolution==640 &&
       vesamodeinfo[loop].verticalresolution==400 &&
       vesamodeinfo[loop].bitsperpixel==32) {
      printf("Initializing 640x400 to 320x200 emulation"
             " with 32-bit colors...\n");
      vesacurmodeinfo=&vesamodeinfo[loop];
      VesaScreenRefresh=VesaLowres_640x400x32;
      VesaSetMode(loop);
      vesamode=VESAMODE_640x400x32;
      return vesamode;
    }

  for(loop=0; loop<vesamodes; loop++)
    if(vesamodeinfo[loop].linearmodeavailable &&
       vesamodeinfo[loop].horizontalresolution==640 &&
       vesamodeinfo[loop].verticalresolution==480 &&
       vesamodeinfo[loop].bitsperpixel==32) {
      printf("Initializing 640x480 to 320x200 emulation"
             " with 32-bit colors...\n");
      vesacurmodeinfo=&vesamodeinfo[loop];
      VesaScreenRefresh=VesaLowres_640x480x32;
      VesaSetMode(loop);
      vesamode=VESAMODE_640x480x32;
      return vesamode;
    }

  for(loop=0; loop<vesamodes; loop++)
    if(vesamodeinfo[loop].linearmodeavailable &&
       vesamodeinfo[loop].horizontalresolution==320 &&
       vesamodeinfo[loop].verticalresolution==200 &&
       vesamodeinfo[loop].bitsperpixel==24) {
        printf("Initializing mode 320x200 24-bit colors...\n");
        vesacurmodeinfo=&vesamodeinfo[loop];
        VesaScreenRefresh=VesaLowres_320x200x24;
        VesaSetMode(loop);
        vesamode=VESAMODE_320x200x24;
        return vesamode;
    }

  printf("Initializing mode 320x200 with greyscale palette...\n");
  VesaScreenRefresh=VesaLowres_320x200x8;
  textmode(0x13);
  VesaGreyscalePalette();
  vesamode=VESAMODE_320x200x8;
  return vesamode;
}

Ja jokainen voi arvata kuinka monelta harmaalta hiukselta systeemi on
minut sstnyt, kun toinen demokooderimme omistaa nytnohjaimen,
jossa on vain 24-bittisi tiloja ja minulla on nyttis, joka taasen
tukee vain 32-bittisi.

Vihjeen nopeaan greyscale-flippiin on, ett kun kerran komponentteja
on kolme ja jakaminen muilla kuin kahden potensseilla on tuhottoman
hidasta, kyt paletista vain ensimmiset 192 vri musta-valkoinen
liukuun (asteen muutos kolmen vrin jlkeen), ja jaa komponenttien
summa kolmen sijasta neljll. Voi ehk olla hydyllist vnt
nousukyr hieman siten, ett vaaleampiin svyihin pstn
nopeammin.

Sen lisksi ett moottori tukee useita eri nytttiloja saman puskurin
esittmiseen, olisi virheensietokyvyn olla niin hyv kuin se voi
olla. Esimerkkiohjelman sietokyky on jo aika korkea, mutta parempikin
se viel voisi olla. Ongelma on mys se milloin ei en voida
jatkaa. Jokin pelin alkulogon nyttminen korkeammassa resoluutiossa
ei viel exit:ti vaadi, mutta jos koko peli vaatii paljon vrej ja
tarkkuutta, ei pelkk virheilmoitus riit.

VESA-enginen seuraksi voi olla hyv idea kert mys mittava joukko
sekalaisia apufunktioita, kuten spritejen piirrot, motion blur ja
muuta sellaista pient kivaa. Ehk jopa kuvatiedostojen saumaton
integrointi voisi olla hydyllist, sill sekalaisten PCX-laturien ja
piirtorutiinien kaapiminen kovalevyn uumenista alkaa viimeistn
silloin olla tuskastuttavaa, kun kytt 32-bittisi tiloja joskus,
toisinaan 16-bittisi ja vlill vain korkean resoluution 256-vrisi
tiloja. Hyv grafiikkamoottori sst vaivalta ja sinne muutokset on
helppo tehd keskitetysti, jonkin nerokkaan optimoinnin lisminen on
helpompi tehd yhteen kirjastoon kuin jokaiseen optimoitua funktiota
kyttvn ohjelmaan.

En min taida enemp porista, lhettkps ihmiset kommentteja tst
SVGA-osiosta, sill jlleen kirjoittelen tt side silmill, en min
tied ymmrrttek tst mitn, minhn vain teen tt. ;-D Nyt
taidankin siirty aloittelemaan "Asioiden taustaa"-osiota. Lykky tyk
grafiikkasysteemien tekoon.


8.9 Polygoneista ja niiden fillauksesta
---------------------------------------
8.8 Lis kivaa - zoomaus
-------------------------
8.7 Prekalkattuja pintoja - ja tunneli
--------------------------------------
8.6 Plasma tekee comebackin - wobblerit
---------------------------------------
8.5 Musiikkijrjestelmist
--------------------------
8.4 Vektorit pelimaailmassa
---------------------------
8.3 Motion blur - sumeeta menoa
-------------------------------
8.2 Lpinkyvyys ja sen vaihtoehto - shadebobit
-----------------------------------------------
8.1 Datatiedostot - miten?
--------------------------

No niin, on aika siis viimein aloittaa se mit pitkn aikaa jo olen
suunnitellut. Eli lukijoiden pakottaminen koodaamaan rutiininsa
varmasti itse. :) Tst lhtien ei koodia heru yht tai kahta rivi
enemp, joudutte tekemn tuttavuutta libc:n dokumentaation kanssa
enemmnkin (komento 'info libc').

Eli homman nimi on datatiedostot. Ja helppoahan tm. Idea on luoda
oma pieni "levyjrjestelm" ja laittaa tiedostot sen
alaisuuteen. Kytnnss FAT:in tyyppist varausyksikk-systeemi ei
kannattane luoda, ellei aio mys kirjoittaa paljon tiedostoja ja
list datatiedostossa oleviin tiedostoihin uutta tavaraa. Yleens
riitt pelkk read-only jrjestelm, johon vain pakataan senhetkiset
erilliset tiedostot haluttaessa.

Kaikkein yksinkertaisin on tehd headeri, joka sislt tiedoston
nimen, sen koon ja sijainnin datatiedostossa. Datatiedoston rakenne on
vain sellainen, ett ensin tulee tiedostojen mr datatiedostossa, ja
sit seuraa mrn verran mainitun kaltaisia headereita, jotka luetaan
muistiin. Sitten vain kun ohjelma haluaa lukea jonkinnimisen tiedoston
datatiedostosta, etsitn tiedostonime vastaava headeri ja mennn
fseekill headerin kertomaan sijaintiin ja luetaan tiedot. Luku voi
olla puhdas "kaikki-tai-ei-mitn", eli ett tiedosto luettaisiin vain
kokonaisuudessaan puskuriin joka palautetaan (tyyliin
bufferi=GetFile("dummy.fil")). 

Toinen mahdollisuus on luoda normaaleja f*-funktioita vastaavat
funktiot datatiedostossa seikkailemiseen (minulla esim. on jfopen,
jfclose, jfread, jfgetc ja jfseek). Jos aiotaan tukea usean tiedoston
yhtaikaista aukipitoa ja liikkumista yhden tiedoston sisll
muihinkin suuntaan kuin eteenpin tytyy mys toteuttaa
tiedostokahva-jrjestelm. Tmkin on helppoa, kahvahan on kytnnss
vain rakenne, joka kertoo miss kohdassa tiedostoa ollaan ja kuinka
pitk tiedosto on jne. Yksinkertaisimmillaan systeemisssi kahva
sislt tiedostoheaderin numeron, josta voidaan noutaa pituus ja
nimi, sek senhetkisen sijainnin. Lukufunktiot ottavat sitten huomioon
sen mist kohdasta nyt pitisi lukea.

Jos koodaat kahvat siten, ett kytt fseeki joka lukukerralla
siirtyksesi oikeaan paikkaan ja pivitt sitten kahvan sijaintia
luetun palan koon verran eteenpin, ky tavupohjaisille lukijoille
kalpaten. Normaalisti fgetc nimittin lukee hieman eteenpin ja
seuraavalla kutsukerralla luettava tavu on jo puskurissa tallessa ja
se tulee nopeasti muistista. Vaan fseekin jlkeen puskuri tyhjtn ja
pdyt todella lukemaan tiedostoa tavu kerrallaan, etk esim. 256
tavua kerrallaan, kuten asian laita normaalisti puskuroituna
olisi. Vaikutus nkyy esim. demossamme Bleediss - 20 kertaa normaalia
hitaampi PCX-laturi.

Nopeuden vuoksi kannattaa joko toteuttaa itse puskurointi, eli kahvaan
mys pieni, esim. 20-tavuinen puskuri, joka kertoo seuraavien tavujen
sislln. Nin todellinen levyhaku tapahtuu 20 kertaa harvemmin. Tai
ainakin tarkistaa ettei fseekata ellei oikean datatiedostoon
osoittavan kahvan sijainti ole muuttunut.

Hyv idea edellisten estmiseksi voi mys olla yksinkertainen
jrjestelm, jossa fopen korvataan funktiolla, joka avaa uuden
FILE-kahvan datatiedostoon ja siirt sen osoittamaan tiedoston
aloituspaikkaan datatiedostossa. Nyt lukufunktioihin tarvitaan vain
tarkistus, ettei lueta tiedoston pituutta pidemmlle eteenpin
(LueKirjain vain lisisi sijaintilaskuria yhdell, palauttaisi EOF:in
jos oltaisiin tiedoston lopussa ja muussa tapauksessa palauttaisi
suoraan fgetc(handle):n). Fseek-funktiokin vain muuttaisi sijainnin
tiedostossa sijainniksi datatiedostossa (lisisi alkuoffsetin jne.).

EXE-tiedostoon tallettaminenkin on helppoa. Tunget vain datan EXE:n
loppuun, sitten headerit ja lopuksi tiedostojen mrn. Eli knnt
normaalin datatiedoston toisinpin ja ltkiset EXE:n pern. Lukiessa
sitten kytt aluksi fseek(handle, -4, SEEK_END), jolloin pset
ksiksi neljn viimeiseen tavuun jotka sisltvt headereiden mrn,
sitten seekkaat taas taaksepin pstksesi headerien alkuun ja luet
itsesi loppuun (tai siis, nelj tavua vaille, headerit loppuvat ja
viimeisen tulisi viel niiden mr).

Tiedostojen lisminen fiksusti datatiedostoon on itseasiassa astetta
vaikeampi homma kuin lukeminen (jos siis teet sen helpolla tavalla, et
kyt puskurointeja tai monimutkaista logiikkaa). Jrkev voi olla
tehd ohjelma, jolle annetaan esim. tiedoston nimi jossa on lista
mukaan otettavista tiedostoista. Sen jlkeen ohjelma lukee jokaisen
tiedoston nimen headeriin ja pituuden, sek laskee offsetit.
Ensimmisen tiedoston sijainti datatiedostossa on tiedostojen mrn
kertovan muuttujan ja headerien jlkeen, eli laskennallisesti:

sizeof(headerien_mr)+sizeof(header)*headerien_mr

Seuraava sijaitsee sitten edellisen koon verran edellisen jlkeen,
kolmas on toisen koon verran toisen jlkeen jne. Kun headeriin on nin
laskettu koon lisksi sijainti, niin voidaan kirjoittaa tiedostoon
headereiden mr, sitten headerit ja lopuksi listn tiedostot
samassa jrjestyksess kuin niiden headerit tiedoston
jatkoksi. EXE:een listess tapa tietenkin olisi knteinen, ensin
tiedostot loppuun, sitten headerit ja lopuksi tiedostojen mr. Tai
toinen mahdollisuus EXE-pohjaisessa olisi vain list datatiedosto
EXE:n loppuun ja viimeiseksi viel offsetti siihen kohtaan miss EXE
aiemmin loppui ja josta data nyt alkaa headerin mrineen ja
headereineen.

No sep oli siin, ei tss oikeastaan mitn vaikeaa pitisi olla,
kun vain ei anna tmn luvun sekoittaa pt.


8.9 Polygoneista ja niiden fillauksesta
---------------------------------------
8.8 Lis kivaa - zoomaus
-------------------------
8.7 Prekalkattuja pintoja - ja tunneli
--------------------------------------
8.6 Plasma tekee comebackin - wobblerit
---------------------------------------
8.5 Musiikkijrjestelmist
--------------------------
8.4 Vektorit pelimaailmassa
---------------------------
8.3 Motion blur - sumeeta menoa
-------------------------------
8.2 Lpinkyvyys ja sen vaihtoehto - shadebobit
-----------------------------------------------

Lpinkyvyys on hieno efekti. Puoliksi lpi nkyv esinett
piirrettess piirretn puoliksi esine ja jtetn puolet taustasta
jljelle. Todellinen lpinkyvyys muodostetaan siten, ett
valon lpisemttmyys ilmoitetaan arvolla vlill 0..1 (jossa 0 on
nkymtn ja 1 tysin nkyv), ja "pikselin" vri lasketaan kaavasta:

transparency * object_pixel + ( 1 - transparency ) * background_pixel

Valitettavasti tytyy todeta, ett SVGA-tiloissa, joissa tt
kytetn, tytyy jokainen vrikomponentti ensinnkin laskea erikseen
ja toisekseen ett kertolasku ei ole kovin nopeaa hommaa. Vaikka
kuinka optimoit, j kteen joka tapauksessa yksi kertolasku, yksi
addi ja shiftaus oikeaan (ja tm vain jos trans*obj voidaan laskea
etukteen, eli objekti ja lpinkyvyys ei muutu). 

Tosin jos muistia kulutetaan kaikille kytetyille lpinkyvyyksille ja
prekalkataan taustatkin pstn yhdell yhdeenlaskulla, joka on
nopeampaa kuin alhaalla esitettv ratkaisu. Tmkin hidastuu siin
tapauksessa ett objektin lpinkyvyys voi vaihdella eri kohdissa,
jolloin joudutaan vaihtamaan lhdepikselin puskuria koko ajan. Ja
tietenkn pllekkiset lpinkyvt esineet eivt onnistu ellet laske
joka objektia kaikkiin mahdollisiin lpinkyvyyksiin. Joka taas on
hidasta.

Mutta kuten nimelt mainitsemattomassa peliss Stan sanoo "But wait,
there is more!". Ja sen 'enemmn' nimi on shadebobit. Nm 90-luvun
alkupuolen demoista vakioefektin lytyvt vekkulit ovat varsin halpa
tapa toteuttaa maittavia lpinkyvyysefektej minimimrll
kertolaskuja, eli ilman ainoatakaan mullia. Ja mitenk tm onnistuu?

Shadebob eroaa normaalista spritest vain siten, ett sit ei piirret
plle, vaan se listn. Jos sinulla on puhtaan sininen tausta ja
lenntt pll punaista shadebobbia, niin bobin kohdalle syntyy
violettia, sill (255,0,0) + (0,0,255) = (255,0,255)
(rgb-triplettej). Esim. flaret ja muut toteutetaan usein
shadebobeilla.

Yksi ongelma kyll on tsskin tekniikassa. Ylivuodot. Joku teist
varmaan on nhnyt efektin joka nytt silt ett ruutu
"palaa". Kytnnss tm on shadebobbi, joka on liian kauan samassa
paikassa, vriarvot kohoavat viimein 255:een ja kun ne menevt yli ne
pyrhtvt ympri, ollaan takaisin nollassa ja jlki on
karua. Valkoisen kirkkaan tpln keskell epmrinen musta roso ei
oikein ole hienoa. Nopean shadebob-piirturin teko C:ll on jo taidetta
ja assyllkin hommaa on ihan liikaa. Ehtolause on periaatteessa
tllainen:

jos shadebob + tausta > ylraja
  tausta = ylraja
muuten
  tausta += shadebob

Assyll voidaan kytt hyvksi carry-flagia, joka menee plle luvun
pyrhtess ympri. Jnc hypp jos carry ei ole asetettu, jc taas
jos on. Mys adc ja sbc voivat olla mukavia, ne kun lisvt
listtvn/vhennettvn carry-flagin. Esimerkkin varsin nopeasta
shadebob-rutiinista:

  add al, bl
  jnc .eiyli
  mov al, 255
.eiyli:

Kytnnss tuo vie yhden kellon ja niiss tapauksissa kuin palamista
syntyy siltkin vltytn toisen kellon menetyksell. Ongelmallista
tosin on, ett truecolor-tiloissa (24 ja 32) tytyy jokainen
komponentti list erikseen, toisin kuin sopivaa palettia kyttvss
256-vrisess tilassa. Ja 15- ja 16-bittisiss tiloissa homma alkaa jo
muistuttamaan masokismi.

Joka tapauksessa lpinkyvyyden kaksi vaihtoehtoista menetelm ovat
molemmat varsin hydyllisi oikein kytettyn. Shadebob-systeemiss on
vain se vika, ett puoliksi lpinkyv valkoista ei ole nhtykn, ja
todellisuudessa vain punaista lvitseen pstv lasi sinisen taustan
pll on mustaa. Siksi shadebobit sopivat parhaiten valoefektien
tekoon. Tosin lpinkyvyyskin onnistuu tekemll alpha-kanava (joka on
kuten bittikartan maski, mutta kertookin kunkin pikselin
lpinkyvyyden). Piirrossa sitten taustasta vhennetn alpha-kanava
(valkoisella tausta on aina musta) ja bittikartasta 255-alpha
(mustalla ei muutu, valkoisella muuttuu "lpinkyvksi", eli nollaksi)
ja sitten vasta listn bittikarttaan. Ja tarkistuksia ei en
tarvita kun alpha-laskut ovat varmistaneet, ett taustan ja
bittikartan summa ei voi olla yli 255. Tsskin bittikartan voi
etukteen laskea oikealle lpinkyvyydelle alpha-kanavan suhteen, jos
se ei muutu.

Jnnittvi hetki tmnkin parissa, shadebobbeja ja lpinkyvyytt
kyttmll voi kehitt vaikka millaisia viritelmi, jos ette usko
niin katsokaa vaikka Orangen Mr. Black, arvatkaa kahdesti onko pyriv
lonkero-mmm muuta kuin taustakuva joka lonkeroiden kohdalta nkyy
lpi, tummentuen lonkeron reunoja kohti. Shadebobbeja tai
lpinkyvyytt, luultavasti ensimmisi. Oikaiskaa jos olet vrss.


8.9 Polygoneista ja niiden fillauksesta
---------------------------------------
8.8 Lis kivaa - zoomaus
-------------------------
8.7 Prekalkattuja pintoja - ja tunneli
--------------------------------------
8.6 Plasma tekee comebackin - wobblerit
---------------------------------------
8.5 Musiikkijrjestelmist
--------------------------
8.4 Vektorit pelimaailmassa
---------------------------
8.3 Motion blur - sumeeta menoa
-------------------------------

Motion blur ei ehk peleiss kovin hydylliseksi muodostu, mutta
demoissa se on varsin suosittu, ja kyll sit voi vaikka
autopelisskin kytt. Joka tapauksessa idea tulee nyt, joten turha
pyristell vastaan. Tm ei satu kuin hetken.

Motion blur eli liikesumennus kuten joku sen voisi suomentaa
tarkoittaa sit, ett liikkuvista tavaroista j jljet ruutuun ja ne
hvivt vasta pikkuhiljaa. Efekti on tuttu Dubiuksen ja muiden
demojen lisksi vaikkapa Jyrkist, joissa ainakin aikoinaan valoista
ji hirmuiset raidat ruutuun.

Tekniikkakin on helppo, kytnnss motion blur on melkein sama kuin
lpinkyvyys, sill toteutus sattuu olemaan sellainen, ett tietty osa
uudesta ruudusta muodostuu juuri piirretyst informaatiosta ja tietty
osa edellisest ruudusta (jossa taas oli jonkin verran sit edellist
jne. Nin syntyy pikkuhiljaa hipyv efekti). Siin se. Sekoitat
vanhaa ja uutta halutussa suhteessa ja olet valmis. Shadebobit eivt
thn ky kauhean hyvin (taino, jos vhennt edellisest aina
esim. 100 ja piirrt uuden framen skaalalla 0-155 ja list ne, niin
miks siin, itseasiassa voisi tmkin toimia). 

Yleisemmin kytetn kuitenkin aitoa lpinkyvyytt ja yksinkertaisia
perusjakoja, jotka menevt ilman kertolaskuja. Kytnnss tm on 1:1
ja pienell kikkailulla vaikka 1:3 ja 1:7 suhteet onnistuvat varsin
helposti. Lupasin olla antamatta sorsaa, joten vihjeen, ett
truecolor-tiloissa shiftaamalla ja poistamalla sopivalla and-maskilla
toisten komponenttien alueelle mahdollisesti eksyneet bitit saa
1:1-sekoituksen muutamassa kellossa. 1:3 onnistuu mukavasti
shiftaamalla neljll molemmat ja kertomalla toisen lea-ksky
kytten kolmella on homma vauhdikasta.

Jos kytt yh jotain ankeaa kvantisoitua tilaa, tai jotain
muuta palettitilaa, niin voit olla onnellinen, voit laskea helposti
etukteen 256x256-kokoisen taulukon, jonka jokainen alkio kertoo
pysty- ja vaakarivin mukaisten vrien optimaalisen sekoituksen. Ja kun
taulukko prekalkataan voit valita sekoitussuhteen ihan
vapaasti. Rutiinikin on yksinkertainen, jokaista taulukon alkiota
kohden otat paletista pysty- ja vaakarivin mukaiset vrit, sekoitat
halutussa suhteessa aidon lpinkyvyyden mukaisesti ja etsit
tulokselle kvantisointikuutiosta (tai kytten lhimmn sopivan vrin
etsint, selostettu vektoreiden ohessa seuraavassa luvussa) lhimmn
sopivan vrin. 

Tm oikeastaan tss olikin. En tied kumpi on nopeampaa, blurraus
flipin sisll (kytetn nyttpuskuria toisena), vaiko nopeamman
keskusmuistin kytt ja sitten vasta flippi ruudulle. Kokemuksia
otetaan vastaan.


8.9 Polygoneista ja niiden fillauksesta
---------------------------------------
8.8 Lis kivaa - zoomaus
-------------------------
8.7 Prekalkattuja pintoja - ja tunneli
--------------------------------------
8.6 Plasma tekee comebackin - wobblerit
---------------------------------------
8.5 Musiikkijrjestelmist
--------------------------
8.4 Vektorit pelimaailmassa
---------------------------

Vektorit ovat sen verran vekkuleja juttuja, ett ksittelen niit
lyhyesti ja vhemmn teoreettisesti tss. Krsivlliset odottavat
lukion kursseja, ja krsimttmt mutta tiedonhaluiset kaivavat
syvemmn teorian vaikka lukion matikankirjasta tai 3Dicasta, sielt
lytyy pistetulo, ristitulo ja muutkin trket tiedot. Me keskitymme
vain perusksitteeseen ja vektorien muodostamiseen, skaalaamiseen ja
yhteenlaskuun.

Olet varmaan jo kyttnyt vektoreita pariin otteeseen. Vektorit ovat
vain tapa ajatella muita kuin skalaarisia suureita
(suuruudellisia). Hyv esimerkki on jonkin esineen sijainti
ruudulla. Aiemmin olet ajatellut ett sijainti koostuu x- ja
y-koordinaateista. Mutta voit ajatella sijaintia mys vektorina, jonka
x-komponentti on x-koordinaatti ja y-komponentti ja y-koordinaatti.

Vektori on nuoli. Ja kuten nuoli, vektori voi osoittaa mihin suuntaan
tahansa ja olla min pituinen tahansa. 2d-peleiss kytt varmaan
yleens 2d-vektoreita, sill paperillekaan ei voi piirt nuolia kuin
tasossa. 3-ulotteisessa avaruudessa taas nuoli voi osoittaa pysty- ja
vaakasuunnan lisksi mys sisn ja ulos nytst ja kaikkialle niden
vlill. Vektorit ovat itseasiassa vain niputettuja koordinaatteja ja
yleens riitt ett keskitytn origokeskeisiin vektoreihin, jotka
alkavat koordinaatiston keskipisteest. Ajattele ruutupaperia, jossa
on koordinaatisto. Origokeskeinen vektori on nuoli, joka lhtee
keskipisteest ja jonka krki on miss tahansa koordinaatistossa. Sama
millaisen vektorin piirrt koordinaatistoosi on helppoa huomata ett
origokeskeinen vektori voidaan aina ilmoittaa kahdella luvulla, x- ja
y-koordinaatilla mihin nuolen krki sitten osuukin. Mys
tietokonemaailmassa vektori ilmoitetaan kahdella luvulla, ihan kuten
ennen teit x- ja y-koordinaattiesi kanssa.

Mrittely onnistuu lukutaulukkona, tai rakenteena. Suosittelen
taulukkoa, eroa structiin ei kuitenkaan nopeudessa ole:

float v[3]; /* 3-ulotteinen vektori, v[0] on x-komponentti, v[1] on y
               ja z tietenkin v[2] */

Vektoreita voi muodostaa kaikkien pisteiden vlille, muodostat vain
molemmista pisteist vektorin (kytnnss ajattelet koordinaatteja
vektorin komponentteina) ja vhennt ne toisistaan ja tuloksesta
tulee vektori, joka on yht pitk kuin mit pisteiden vlill lyhin
matka (tt voi kytt esimerkiksi kvantisoinnissa, kahden vrin
etisyys voidaan selvitt muodostamalla niiden vlille vektori ja
laskemalla sen pituus). Vektorien yhteen- ja vhennyslasku on helppoa,
jokainen komponentti vain ksitelln erikseen. Eli jos vektorin a
(pll pitisi olla viiva, mutta...) x- ja y-komponentit ovat 5 ja 2
ja vektorin b vastaavasti 3 ja 4, ovat niiden summavektorin
komponentit vastaavasti 5+3=8 ja 2+4=6. Yleens vektori ilmoitetaan
kuten koordinaatti, eli a + b = (8,6) ja a - b = (2,-2). Vektorin
pituus lasketaan kaavasta sqrt(x*x + y*y). Vektorin pituutta voidaan
skaalata jollain luvulla kertomalla jokainen komponentti erikseen
luvulla. Tllin pituus kasvaa <luku>-kertaiseksi, mutta suunta pysyy
ennallaan.

Kolmiulotteisessa erona on vain se, ett mukaan tulee
z-komponentti. Pituuden kaavaan listn + z*z ja laskuissa mys tm
komponentti pit muistaa ksitell. Helppoa kun sen osaa.

No nyt tiedt mik on vektori. Vaan mit sill tekee? Paras vastaus
on, ett ihan mit tahansa. Vaikka autopelin. Tai Quaken. Vektori on
vain ktev tapa niputtaa n-ulotteisen avaruuden koordinaatit yhteen
pakettiin. 

Joku kysyi minulta vhn aikaa sitten miten autopeliss tai
luolalentelyss tehdn liikkuminen. Olisi ollut todella helppoa
selitt asia jos olisin ollut varma ett kysyj ymmrt mik on
vektori, mutta kun jouduin selittmn asian x- ja y-koordinaateilla,
hommassa oli paljon enemmn tekemist. Selitnp nyt miten
luolalentely voitaisiin toteuttaa vektoreilla:

Aluksen sijainti on normaali 2-ulotteinen vektori. Lisksi aluksella
on nopeusvektori ja kiihtyvyysvektori. Joka framella sijaintivektoriin
listn nopeusvektori. Nopeusvektorin suunta vastaa aluksen
kulkusuuntaa (eli ihan kuten x-nopeus ja y-nopeus, vain niputettuna)
ja pituus nopeutta.  Voit hahmottaa liikett piirtmll esimerkiksi
sijaintivektorin ja sen krjest lhtien nopeusvektorin. Joka
kierroksella nopeusvektori listn sijaintiin, ja tulos on juuri se
mit saat kun piirrt nopeusvektorin suuntaisen ja pituisen jatkeen
sijaintivektorille. Kokeile vaikka alkusijaintia (5,7) ja nopeutta
(2,-1) ja lis  edelliseen jlkimminen ja piirr tm uusi vektori
(9,6). Huomaat ett uuden ja vanhan sijaintivektorin vli on juuri
nopeusvektorin suuntainen ja pituinen.

Lisksi meill on viel kiihtyvyysvektori, joka ilmoittaa mihin
suuntaan alus on kiihtymss. Tm vektori kertoo mihin moottori
millkin hetkell alusta tynt (suunta) ja kuinka nopeasti
(pituus). Rakettimoottoreilla kiihtyvyys on aina vakio, joten
kiihtyvyyden mrittminen onnistuu luomalla sinill ja kosinilla
yksikkvektori (nimitys jota kytetn vektoreista joiden pituus on 1,
joka ptee kaikkiin vektoreihin joiden x-komponentti on kosini ja
y-komponentti sini, se on niden trigonometristen funktioiden
perusluonne) ja skaalaamalla se rakettimoottorin teholla, eli esim:

kiihtyvyys[0] = cos(kulma_rad);
kiihtyvyys[1] = sin(kulma_rad);

kiihtyvyys[0] *= TEHO;
kiihtyvyys[1] *= TEHO;

Ja kiihtyvyys listn tietenkin joka vuorolla nopeuteen, eli kun
moottori ponnistelee kulkusuunnan mukaisesti vauhti kiihtyy ja jos
knnt aluksen nokan vastakkaiseen suuntaan ja painat kaasun pohjaan
(onko napeissa muka muita asentoja ?-) vauhti alkaa
hidastumaan. Tydellist.

Ja koska vektorit ovat suoraan fysiikkaa varten luotuja, on
painovoiman, aseen rekyylin, trmysten ja muiden lisminen lasten
leikki. Painovoima on vain vakiosuuntainen (alas) kiihtyvyys. Kitka
pinnasta (ei luolalentelyiss, autopeleiss kyllkin) on tietyn
prosenttimrn (1-kitkakerroin) mukainen vauhdin hidastuminen,
rekyyli ja trmykset perustuvat siihen, ett jos ammut panoksen
tiettyyn suuntaan (vektori), niin aluksen suunnanmuutos on
vastakkainen ja suoraan suhteessa massojen eroon. Esim. jos panos
painaa 1/1000 aluksesta, listn nopeuteen ammuksen suuntavektori
knnettyn (miinusmerkki joka komponentin eteen ja nuoli osoittaa
vastakkaiseen suuntaan) ja skaalattuna yhteen tuhannesosaan, eli
kerrottuna 0.001:ll. Jos olet perfektionisti voit pit lukua
panoksista ja vhent ne massasta. :) Tydellinen trmys (molemmat
kappaleet jatkavat samaan suuntaan, esim. luodit) on ihan sama, mutta
knteisesti, eli ei tarvitse knt ammuksen suuntavektoria, vaan
listn se vain massojen suhteessa. Alusten ja seinn vliset
trmykset ovat hankalampia, niiss kun pitisi molempien kolahtaa eri
suuntiin. Thn saat vapaasti kehitell omasi, luolalentelyiden
tekijt ovat tyytyneet yleens pysyttmn aluksen seinn osuessa ja
antavat alusten lpist toisensa.

Siinp kaikki trkein vektoreista. Jos ymmrrt mit ne ovat, miten
ne jakautuvat x-, y- ja z-komponentteihin ja tajuat ett skaalaamalla
muutetaan niiden pituutta, ja osaat kaiken lisksi list, vhent ja
skaalata niit, olet vahvoilla. Oppitunti on pttynyt.


8.9 Polygoneista ja niiden fillauksesta
---------------------------------------
8.8 Lis kivaa - zoomaus
-------------------------
8.7 Prekalkattuja pintoja - ja tunneli
--------------------------------------
8.6 Plasma tekee comebackin - wobblerit
---------------------------------------
8.5 Musiikkijrjestelmist
--------------------------

Ne jotka haukkovat kotikatsomoissaan henke jo kuin kalat, saavat
aloittaa hengittmisen jlleen. Tiedossa ei viel ole nikorttien
saloja, eik edes vaivaisia miksauksen perusteita, ne taidan laittaa
vasta 3.0-versioon, jos sellaista edes kannattaa siin vaiheessa alkaa
tekemn. Nyt kuitenkin esittelen lyhyesti NE soittosysteemit, jotka
tll hetkell minun henk. koht. mielipiteeni mukaan ovat hyvi
vaihtoehtoja kun musiikkia pit alkaa kuulumaan, muusikkojen tai
kohdeyleisn vaatimuksesta.

Alkusanoina totean, ett kaikkein paras on tietenkin tehd oma
systeemi. Mutta se suurin kompastuskivi on siin, ett levityksess
olevien tasoisten (paraskaan ei soita IT- ja XM-kappaleita kaikkia
oikein) "playereiden" teko vie yhdest viiteen vuotta. Tervetuloa
todellisuuteen. Vaatimukset nimittin ovat hurjat, mitn yhtenist
standardia kun ei Windows Sound Systemi lukuunottamatta DOS:in
puolella. Tai no, SB on vahva sana, Pro-mallia tukemalla tuet
luultavasti 99% tavoiteyleissi nikorteista. 

Mutta edes yhden kortin koodaukseen vaadittava tieto- ja taitomr on
sen verran suuri, ett jos viikossa saa sellaisen systeemin tehty,
ett sill voi soittaa looppaavia ja looppaamattomia sampleja,
katkottomasti, ilman naksahduksia ja vapaasti sdeltvll vauhdilla
ja voluumilla, niin voi ajatella ett kymmenesosa hommasta on jo
tehty. Sen jlkeen hyktn FMODDOC:in kimppuun ja vietetn seuraava
viikko kyhten jonkin formaatin moduuliloaderi (jos kyseess on XM tai
IT suosittelen varaamaan pari viikkoa ja purkin Buranaa). Sen jlkeen
viel pari kuukautta efektitukea vnnellen (XM on dokumentaation
saatavuudessa suorastaan kuninkuusluokkaa, herrat FT2:n tekijt kun
ovat sit mielt ett heidn trackerinsahan on melkein
itsedokumentoiva - tiedossa siis ainakin S3M- tai MOD-formaatin
dokumenttien luku ja hauskoja hetki niin heksaeditorin kuin FT2:nkin
parissa).

No nyt olette varmaan niin kauhuissanne ett tmn luvun todellinen
asiasislt menee kuin kuumille kiville. ;-D No ei, ei se
moduuliplayerin teko niin hirve hommaa ole, tytyy vain omata
itsepisyytt, taito tehd asiat tarpeeksi hyvin kerrasta (lukemalla
fmoddocin kerran lpi ennen aloitusta voi thn synty kummasti
kiinnostusta) ja paljon paljon krsivllisyytt. Optimointitaitokaan
ei olisi pahitteeksi.

Jos kuitenkin lykkt moduuliplayeriasi hieman kauemmaksi
tulevaisuuteen ja kokeilet ensin jotain muuta kuin kotikutoista
ratkaisua, kannattaa ensimmiseksi kurkata Housemarquen sivuille
(www.housemarque.com/fi) ja imuroida Midaksen uusin versio. Midas on
moduuliplayereiden ehdoton Rolls Royce, jopa IT-tuki taisi lyty, ja
XM:tkin soivat vain osaksi pieleen. Funktioita riitt vaikka muille
jakaa (tosin subrow-tarkkuista laskuria moduulin sijainnista ei saa
kyttns :), timerista lhtien vga-tilojen asetukseen ja
nyttsynkronointeihin. DirectX-tuki lytyy niinikn.

Ainoa Midaksen ongelma on se, ett sit ei EHDOTTOMASTI saa kytt
SW- tai muuhun kaupalliseen levitykseen. Ilmaisohjelmat ovat ok,
kunhan vain muistaa mainita kyttneens midasta, mutta jos otat siit
rahaa, otat Midaksen mys pois ohjelmastasi. Kaupallisia lisenssej
tosin on mahdollista hankkia, joten postia vain housemarquelle
shkisess muodossa. Aiemmin muistaakseni rekisterintihinta oli
$500, mutta ehk se on tippunut. :)

Jos SW kiinnostaa, tai haluat vaihtoehtoisen, DJGPP-optimoidun
systeemin peliisi, on Humppa (entinen Hubrmod), eli HUbris Module
Player PAckage tarkistamisen arvoinen, lhett vaikka pelisi
lhdekoodit Kaikalle ja saat alkaa myymn peli. :) Kuulemani mukaan
XM-tukikin on jo varsin hyv ja sormeni syyhyvt pst kokeilemaan
systeemi, vaan en ole viel ehtinyt.

Toinen hieman kalliimpi, mutta pitkt perinteet moduuliplayerien
saralla omaava vaihtoehto on MikMod, josta lytyy Midaksen tapaan tuki
melkein joka laitealustalle, mukaan lukien Linux. Rekisterintihinta
oli varsin halpa, muistaakseni parikymment dollaria, ja sekin vain
siin tapauksessa ett haluat kytt playeria kaupallisiin
tarkoituksiin, ilmaislevittjt saavat kytt softaakin
ilmaiseksi. Ja kuten Humpassa, mys MikModissa lhdekoodi tulee
mukana, joten mahdollisuudet omiin viritelmiin kohoavat huimasti.

Kaikkien kolmen mukana tulee varsin laadukas dokumentaatio, tai jos ei
sellaista lydy, niin esimerkkikoodia lytyy jokaisesta. Itse olen
kokeillut nelj tai viitt eri playeria ja jok'ikisen toimimaan
saanti ei ole vaatinut muuta kuin sopivan esimerkin rtlimist
omaan kyttn sopivaksi. Uskaltakaa hyvt ihmiset kokeilla niit, ja
jos ette kerta kaikkiaan ymmrr niit englanninkielisi kommentteja
niin sanakirja tai taitava kaveri varmaan auttaa mielelln. :)

Tss kaikki tlt er, minulle saa postittaa ilmoituksia jos jokin
ehdottoman mahtava DJGPP-playeri puuttui (ei, se J. Hunterin DJGPP:lle
tehty SB Library tai mik olikaan ei ky - edes modit eivt soi siin
oikein).


8.9 Polygoneista ja niiden fillauksesta
---------------------------------------
8.8 Lis kivaa - zoomaus
-------------------------
8.7 Prekalkattuja pintoja - ja tunneli
--------------------------------------
8.6 Plasma tekee comebackin - wobblerit
---------------------------------------

Yksi varsin hulvaton demoefekti ja peleisskin kenties
hydynnettviss oleva vekkuli on nimeltn wobbler ja selostan
toiminnan lyhyesti tss. Sen sijaan ett ottaisin x:n mukaan parista
aallosta vrin ja y:n mukaan parista aallosta, listkin x-arvon
mukaisesti siniaallolla y-koordinaattia ja toisinpin. Tuloksena
syntyy ihanasti vellova efekti, jota voi kytt miten mieli sitten
tekeekn. Hyv idea on kytt 256x256-kokoista tekstuuria, jolloin
y- ja x-arvot on helppo saada menemn ympri (y&255 ja assyll
suoraan tavurekistereill) ja oikeanlaisilla kartoilla ei reinoja
huomaa. (mik olisi hyv termi englanninkieless kytetylle
tilingille? tiiliytyminen? tileytyminen?)

Voit mys kokeilla y:n lismist y:n mukaan jolloin syntyy
venytyst. En myskn tied millainen on tulos, jos et lis nit x-
ja y-arvoihin, vaan laitat ne sellaisenaan (eli ei x + ..., vaan vain
...).


8.9 Polygoneista ja niiden fillauksesta
---------------------------------------
8.8 Lis kivaa - zoomaus
-------------------------
8.7 Prekalkattuja pintoja - ja tunneli
--------------------------------------

Jlleen uutta pikkukivaa efektien saralla. Tunneli. Idea on sellainen,
ett sinulla on kaksi puskuria, samaa kokoa kuin ruutukin, sek
tekstuuri (esim 256x256). Puskurista 1 otetaan samasta kohdasta kuin
ruudulle laitettava pikselikin ensin y-arvo, ja sitten puskurista 2
x-arvo. Tunnelin tapauksessa puskuriin 1 piirretn kuvio, joka on
steittisi viivoja keskipisteest ja kuvio lasketaan siten, ett
mennn joka pikseli lpi, muodostetaan vektori pikselin ja
keskipisteen vlille (x-komponentti on x-160 ja y-komponentti y-100)
ja selvitetn vlill oleva kulma. Tm hoituu joko tangentilla,
jolloin homma on nopeampaa, tai jos et sit osaa, teet sen siten, ett
piirrt keskipisteest tarpeeksi tihen ympyrit thn tyyliin:

for(radius = 0; radius < 140; radius ++) {
for(angle = 0; angle < 2048; angle++) {
  x = (int)(cos(3.1415*(float)angle/180.0)*radius);
  y = (int)(sin(3.1415*(float)angle/180.0)*radius);
  buffer1[y*320+x] = angle/8; // 0..255
}
}

Ja toinen taas on sarja ympyrit keskipisteest, vrin ollessa
etisyys keskipisteest. Tm on helppo ratkaista nelijuurella. Eli:

for(y=0; y<200; y++) {
for(x=0; x<320; x++) {
  tx = x-160;
  ty = y-100;
  buffer2[y*320+x] = (int)sqrt(tx*tx+ty*ty);
}
}

Sitten vain piirretn tunneli:

for(loop=0; loop<64000; loop++) {
  screen[loop] = texture[ buffer2[loop] * 256 + buffer1[loop] ];
}

Ainiin, perspektiivinkin voi list vaihtamalla y-arvon (buffer2)
laskuun sqrt:n tilalle 256/sqrt:n. Tllin pit tosin varmistaa ettei
sqrt ole alle 1, sill muuten ky todella huonosti. Ja jos etisyys ei
tuollaisena tyydyt sen voi kertoa halutun suuruisella luvulla. Niin
ja tunnelin saa liikkeelle kun lis piirron aikana x- ja y-arvoihin
jotain. Tss pit kuitenkin huolehtia ettei kumpikaan mene yli 255:n
(eli kytnnss ((buffer2[loop]+yoff) & 255) + ...).

Muitakin kuvioita joissa tekstuuri liikkuu "pintaa" pitkin, voi
helposti luoda. Wormhole on yksi tllainen, eik edes hirvittvn
vaikea, mietipps vain. Ja Trauman Mindtrapissa luultavasti kytettiin
samaa tekniikkaa siin pyrivn pallon kohdassa jossa ymprill
pyritn toiseen suuntaan.


8.9 Polygoneista ja niiden fillauksesta
---------------------------------------
8.8 Lis kivaa - zoomaus
-------------------------

Jlleen sarjassa "helppo nakki kun kytt aivoja"-efektej. Tll
kertaa vuorossa vanha tuttumme reaaliaikainen suurennos. Ja miks sen
helpompaa. 

Normaalissa bittikartan piirrossahan korotat ruudun offsettia yhdell
ja bittikartan offsettia yhdell. Ents jos korottaisit bittikartan
offsettia kahdella? Bittikartta "loppuisi" puolet nopeammin, ja
tuloksena piirtisit sen puoleen aiemmasta tilasta, sek x- ett
y-suunnassa. Pienensit juuri kuvaasi kahdella. Onnea. No ent
suurennos sitten? Helppoa, korotat ruudun offsettia kahdella? Totta,
mutta tuloksena syntyy hieman reiki (aika kiva rjhdysefekti silti),
joten ehk kytmme jotain muuta. No varmaan kaikki arvasivatkin jo -
korotetaan bittikartan offsettia puolella ja pstn samaan
tulokseen.

Vastaavalla tavalla pystyt suurentamaan ja pienentmn bittikarttoja
kaikilla kahden potensseilla. Mutta pystyt kyll parempaankin ja
tiedt sen aivan varmasti. Nappaa kyttn fixed-point luvut tai
vaikka floatit, niin yhtkki voitkin tehd sama mink kokoisia
zoomauksia, portaattoman nkisi vielp! (taino, siin syntyy
sellaist rsyttv pyristysvirhekuviota, kokeile vaikka suurentaa
kuvaa pikkuhiljaa pienentmll askelta mahdollisimman vhn framejen
vliss). Helppoa kun sen osaa.

Ainoa hiritsev piirre tulee olemaan se, ett karttasi lentelevt
ruudun ylitse tai loppuvat kesken. Niinp piirrossa tytyy normaalin
for(... ; bitmap_x < x_size; bitmap_x++) -systeemin sijaan tarkistaa
pyrityksen aikana sek ruudun ett bittikartan x- ja
y-koordinaatit. Tai sitten klippaat ennen looppia, eli jos kartta
menee ruudun ylitse siirrt piirtoa alkamaan hieman myhemmin kuin
ensimmisen pikselin kohdalta. Jlleen tss tulee tehd sen verran
tarkka jrjestelm, ettei reunoilta j satunnaisesti 2-5 kappaletta
pikseleit tyhjksi.


8.9 Polygoneista ja niiden fillauksesta
---------------------------------------

Minua on pitkn aikaa ruinattu tekemn 3D:st juttua ja aina olen
knnyttnyt kysyjt 3Dican puoleen. Tai ainakin polygonijutuista. No
niin, samoin ky tll kertaa. :) Tai melkein, tss luvusta niin
lyhyesti ja ytimekksti polygonien fillaus ilman klippej ja muita
kuin vain mahdollista. Ennen kuin taaperrat luvun lpi, hakkaa phsi
tieto miten piirretn muitakin kuin suoria viivoja. Eli lue se
interpolointi-juttu lvitse.

Polygonien tyttminen on varsin helppoa. Ensin kymme lvitse
kolmioiden tyttmisen. Joka on itseasiassa _niin_ helppoa, ett
keksin lineaarisen interpoloinnin ja kolmion tyttmisen idean ihan
itse, ilman kenenkn apua. Hienoa, lohduttiko?-) No joka tapauksessa,
asiaan, eli tarkemmin sanoen flat-polyfilleriin.

Tasaisella vrill tytetyn kolmion piirto on varsin helppoa. Kun
piirtelet vaikka 27 kappaletta kolmioita paperille, niin huomaat ett
jos vedt kolmion pystytasossa katsottuna keskimmisen pisteen kautta
kulkevalla vaakaviivalla halki, saat kaksi pienemp kolmiota (tai
erikoistapauksissa vain yhden, jos sinulla on jo tasapohjainen
kolmio), joista toisessa on tasainen pohja ja toisessa tasainen
"katto". Jos viel ajattelet syntyneiden kolmioiden kylki viivoina ja
kuvittelet piirtvsi ne ylhlt alaspin interpoloiden x:, huomaat
ett olisi varsin helppoa aloittaa huipulta ja list y:t yhdell,
interpoloida hieman alku- ja loppu- x-koordinaatteja ja piirt
x-koordinaattien vlille vaakaviiva. Itseasiassa jos mietit viel
hetken huomaat, ett se olisi enemmn kuin helppoa.

Siit vain tekemn, ensimminen flat-fillerisi valmistui juuri. Eli
sorttaa pisteet y:n mukaiseen jrjestykseen (menee kolmella
if-lauseella, bubblesorttia) siten ett ensimmisen on ylin ruudulla,
sitten keskimminen ja lopuksi alin. Nyt lasket pisimmlle viivalle,
eli sille joka ulottuu ylimmst alimpaan, x-askeleen 
(kaava: (x3-x1)/(y3-y1)). Sitten tarkistat ett y1 ja y2 eivt ole
samoja ja jos eivt, lasket vastaavan x-askeleen x1:n ja x2:n vlille
ja menet loopilla vlin y1-y2. Jos y1 oli sama kuin y2 niin et tee
tuota ja jatkat suoraan vastaavaan tarkistukseen y2:n ja y3:n kanssa,
ja jlleen jos ne ovat erisuuria jatkat pidemmn viivan piirtmist
siit mihin se ji ja lasket x-askeleen viel y2:n ja y3:n vliselle
matkalle. Koko ajan piirrt vaakaviivoja pisimmn viivan
x-koordinaatin  ja ylemmn tai alemman kolmion viivan x-koordinaatin
vlille, sille korkeudelle miss y-looppisi meneekn.

Vaakaviivassa voisi olla hyv tarkistaa ett x1 on pienempi kuin x2
(jos nin ei ole, vaihda pisteet keskenn) ja onko viiva edes
ruudulla (x2 >= 0 && x1 <= 319 && y>=0 && y<=199) ja ettet ala
piirtmn ruudun ulkopuolelta (siirr x1 nollaan jos se on alle ja
laske x2 319:n jos se on yli). Sitten vain helpolla for-loopilla
viiva tyteen vri. Muista, ett se se on for(x=x1; x<=x2; x++), eli
<=, eik <!

Gouraud-kolmio on ihan samanlainen, mutta vri tytyy interpoloida
samoin kuin x-koordinaattia. Tekstuurikolmiossa interpoloidaan vrin
sijaan tekstuurin x- ja y-koordinaatteja. Huomioitavaa on, ett jos
viivanpiirrossa siirrt x1-koordinaattia -5:st 0:aan jottet piirtisi
ruudun ohitse, tytyy mys tekstuurien/gouraud-vriarvojen
alkupisteit siirt viidell askeleella eteenpin. Ja texture ja
gouraud-fillereiss hlinesskin pit interpoloida samalla tavalla
viivaa piirrettess kuin tehdn kolmion sivujen kohdalla.

Sossiin, mie meen saunaan.


8.10 Pari kivaa feikkimoodia
----------------------------

Ihmissilm on helppo huijata, se on todistettu useampaan kertaan. Ja
huijauksen kohteeksi joutuu puolueellisen tutkimuksen mukaan varsin
useassa VESA-tiloja kyttmttmss demossa. Ja miks siin. Mode-X
-tiloja en ksittele, kun en jaksanut twiikata itsekn kuin 320x400
-tilan toimimaan, nykyn ne alkavat olla hieman turhia. Mutta, demoja
tehdess etenkin arvostaa kunnon temppuja jolla saa ruudulle enemmn
vrej kuin mit siell oikeastaan voisi yht aikaa olla.

Ensimminen tapa onkin kaluttu jo lvitse, ja sen nimi on kvantisoidut
tilat. Niiss ongelmana tahtoo vain olla, ett homma ei ole kovin
vauhdikasta. Demoihin asian kyll voi hoitaa siten, ett tekee
erikoisflipin, joka laskee kaikki kytetyt vrit ja tekee niist
vrikuution ja paletin etukteen, mutta erityisen vrikkill
efekteill alkaa homma maistua puulta, tai paletti nyttmn yh
enemmn silt perinteiselt 3:3:2-ratkaisulta, eli kytetn
256-vrisen tilan pikselin kolmea ylimmist bittia punaiseen, kolmea
keskimmist vihren ja kahta alimmaista siniseen. Voit hyvin
kuvitella milt vriliu'ut nyttvt. Tytyy kuitenkin todeta, ett
onhan se kuitenkin kahdeksan/nelj eri vrikomponenttia, toisin kuin
16-vristen tilojen vrikomponenttien sekoitus, vlivrit ja
tumma/vaalea -valinta. Argh. Perfektionistit sanovat ei vreille ja
tekevt 256-vrisen tilansa tietenkin puhtailla harmaasvyill, mutta
me (vai pitisik sanoa te? ;) muut ihmiset voimme kytt erilaisia
pieni kikkoja laadun parantamiseen.

Ers hieno ja usein kytetty tapa on jakaa 256-vrinen paletti
pehmen sinisen, punaisen ja vihren liukuun ( 64 svy) ja
muodostaa vri kolmesta vierekkisest pikselist. Laadussa ei
todellakaan ole hurraamista, mutta vlttmtt asiaa ei ihan ensi
silmyksell huomaa (minua ainakin on muutaman kerran vedetty
hplst). Valkoinen vri on aika karu, mutta jhn paletista 64
svy yli ja ne voi aina kytt harmaasvyihin. Tehokasta jlke
tulee ainakin, jos twiikkaat kyttsi jonkin 320x400-tyyppisen tilan,
jolla normaali tilan 13h pikselit tuplaantuvat ja pllekkisi
pikseleit on suorastaan ihana kytt tmntyyppisiin
sekoituksiin. Jo pelkstn kyttmll kahta 3:3:2-pikseli
approksimoimaan vrilntti saadaan teoriassa 4:4:3-tila, ja
kummallisemmilla vrijaotteluilla (1 bitti ilmoittamaan onko yl- vai
alapikselin paletti ja loput 7 bitti molemmissa erikseen kyttn,
jolloin saadaan 14-bittinen tila, teoriassa, ainakin) pstn jo
todella hyviin tuloksiin.

On mys olemassa todellisten virittelytilojen maineeseen pssyj
moodeja, joissa kytetn kahta kuvaa joita vlkytetn nopeasti ja
kaiken maailman muita virityksi, mutta vauhti on yleens huono
verrattuna lhes suoraan raudalla toteutettaviin illuusioihin ja
toimivuus joidenkin tilojen kohdalla satunnainen, riippuen koneen
nyttkortista ja Uranuksen kallistumiskulmasta (joka tietenkin on
vakio). Nihin en ole erityisemmin perehtynyt, mutta jos joku on, niin
minua voi vapaasti pommittaa informaatiolla. Joka tapauksessa hus vain
keksimn erikoisia tapoja saada vrien mr nennisesti
korotettua!


9.1 Saatteeksi
--------------

Yli-guruiksi itsens tuntevat, skip it.

Nyt et ole en laama, vaan hatarasti pasiat osaava, toivottavasti
innokas peliohjelmoijan alku. Tai kenties jopa viel enemmn, jos
kaikki dokumentin asiat ovat hanskassa. Tmn dokumentin tarkoituksena
on ollut saattaa sinut vain alkuun. Ensimminen neuvoni aloittelevalle
peliohjelmoijalle, eli sinulle on, ett l jt lukemistasi
thn. Mikn ei korvaa tuhansia tunteja tutoriaalien ja kokiksen
ress vietettyj tunteja. Samaa asiaa voi opetella useasta eri
lhteest, jolloin tajuaa asiat paremmin, selkemmin ja syvemmin kuin
yhden tutoriaalin luvulla.

Toisena on se, ett englannin kieli on pakko opetella. Sen oppii
parhaiten sanakirjan kanssa tutoriaaleja kahlailemalla. Jos aiot
prjt hyvin peliohjelmoinnissa tytyy englantia osata. Jos et viel
sit osaa, niin tyskentele oppiaksesi.

Kolmanneksi kaikkein trkeimpn ovat oman jrjen kytt, rautainen
tahto ja sammumaton tiedonhalu, sek ahkeruus. Maailma on tynn
laamoja, jotka eivt osaa mitn siksi koska eivt ole tosissaan
yrittneet. Min aloitin C-ohjelmoinnin alle kolme vuotta sitten ja
pelkll kovalla yrittmisell ja innostuneisuudellani opettelin
koodaamaan. Olen lukenut tuhansia ja tuhansia rivej ohjelmointiasiaa,
enk ole viel sit joutunut katumaan.

MBnetist lytyy valtava mr lhdekoodia ja tutoriaaleja lhes
kaikkiin ohjelmoinnin haaroihin. Netist puhumattakaan. Ennenkuin
menett toivosi tai menet kysymn mistn kahlaile sielt kaikki
tarpeelliset alueet ("C/C++" ja "muut") lpi. Melkein kaikkeen pitisi
vastaus noista dokumenteista lyty (mist muualta henkilt joilta
kysytn olisivat ne saaneet selville kuin dokumenteista).

Lisksi lytyy kymmeni ohjelmointikirjastoja, joissa tulee lhdekoodi
mukana. Ja muista, ett itsekin voi tehd ptelmi ja kokeiluja. Kyll
aina jostain tarvittu tieto lytyy!

9.2 Hieman koodereiden jargonia
-------------------------------

Alta lytyy muutamia kivoja tietokoneslangin termej, joiden merkityst
on hieman valotettu lyhyell selityksell. Huomaa, ett tss on vain
tietokoneslangia, sanat optimaalinen, ideaalinen ja muu vastaava pit
yh hakea Suomen Sivistyssanakirjasta:

PIKSELI Yksi kuvaruudun piste. Piste voi olla mustavalkoinen,
    harmaasvyinen tai vrillinen riippuen nytttilasta ja sen
    muistinvienti vaihtelee 1 bitin ja 4 tavun vlill.

KAKSOISPUSKURI nyttpuskurin (ks. NYTTPUSKURI) kokoinen puskuri, jonne
    kaikki piirretn ennen ruudulle kopioimista, jolloin saadaan tavara
    mahdollisimman "tiiviss" (ei hajanaisia kirjoituksia silloin tllin)
    paketissa hitaaseen nyttmuistiin.

SEGMENTTI reaalitilassa muisti on jaettu 64 kilon kokoisin segmentteihin.
    Itseasiassa segmentti on 16 ylint tavua 20-bittisess (max. 1 mega
    osoitettavaa muistiavaruutta) osoitteessa ja thn listn sitten
    viel offsetti (ks. OFFSET). Muodostuskaava on seg*16+off.

    Mys lhdekoodista knnettviss objektitiedostoissa (ks. OBJEKTI)
    on segmenttej, mutta ne tarkoittavat tietojen silmispalasia,
    kuten koodille varattu segmentti, datalle varattu segmentti ja
    numeromuuttujille varattu segmentti. Ks. mys SEGMENTTIREKISTERIT.

OFFSET reaalitilan 20-bittisen muistiosoitteen 16 alinta bitti. Huomaa,
    ett segmenttirekisterin ollessa 16 ylint on osa osoitteista 
    "pllekkin" Nin ollen on useita tapoja osoittaa samaan kohtaan
    muistia. Katso alhaalla olevaa esimerkki:

    Huomaa, ett segmentti on yhden heksan, eli 4 bitti enemmn vasemmalla,
    sill 4 bitin bittisiirtohan vastaa kertomista 16:sta (2^4=16).

    Offsetti:       1234      6784
    Segmentti:   + 5678    + 5123
                 -------   -------
    20-bittinen:   579B4     579B4

SELEKTORI Suojatussa tilassa prosessori jakaa todellisen fyysisen muistin
    halutun kokoisiin loogisiin paloihin. Selektori kertoo prosessori mihin
    loogiseen alueeseen halutaan osoittaa. Thn liittyy hieman monimutkai-
    sempaakin asiaa, mutta sen hydyllisyys j kyseenalaiseksi normaalissa
    peliohjelmoinnissa. Loogisilla muistialueilla on mys koko tallessa ja
    jos osoitellaan tt pidemmlle muistialueella ohjelma kaatuu ja
    tulostaa virheilmoituksen. (SIGSEGV muistaakseni) Selektoreja kytetn
    usein kuten segmenttirekisterejkin (ks. SEGMENTTIREKISTERIT), eli
    ds:ss on dataselektori, cs:ss koodiselektori jne.

SEGMENTTIREKISTERIT Prosessorilla on muutama 16-bittinen segmenttirekisteri,
    jota tytyy kytt kun halutaan osoittaa tiettyyn segmenttiin tai
    suojatussa tilassa selektoriin (ks. SELEKTORI). Nit ovat cs, ds, es,
    fs, gs ja ss. Rekisteri cs sil koodin segmentti, eli koodia
    luettaessa prosessori katsoo aina segmentist 'cs' tavaraa. Cs:n pari
    on ip-rekisteri, joka on 16-bittinen ja osoittaa koodin offsetin (ks.
    OFFSET). Lhes kaikilla segmenttirekistereill on tllainen "pari", 
    joka hoitaa offset-puolen. 

    386-prosessorista lhtien niss on mys extended osa, joka on mys
    16-bittinen ja kytettess tt extended-osaa listn e-kirjain
    rekisterin eteen. Nit kytetn etenkin suojatussa tilassa, jossa
    64 kilotavua on hieman liian vhn, kun taas 32-bittinen (16-bittinen
    extended osa normaalin jatkoksi) osoite riitt varsin hyvin, ainakin
    toistaiseksi. Offset-rekisterej ip, si, di, sp ja bp vastaavat siis
    eip, esi, edi, esp ja ebp.
    
    Seuraavaksi tulee ds, eli datasegmentti, joka on ns. oletussegmentti,
    jota kytetn jos et mrittele toista segmentti assembler-kskysssi
    (esim. mov [eax], ebx on sama kuin mov ds:[eax], ebx). Tmn "pari"
    taasen on si/esi. Sitten es, fs ja gs, jotka ovat taas yleissegmentti-
    rekisterej, joista kaksi viimeist listtiin 368-prosessorin mukaan.
    Ainoastaan es:ll on pari, di/edi. Sitten lytyy ss, eli pinosegmentti,
    joka osoittaa siis ohjelman pinon (ks. PINO) segmenttiin ja tmn 
    offset-pari, sp/esp. Ylimrist bp/ebp -offsetrekisteri kytetn
    useasti osoittamaan funktion parametreihin ja omaan muistiin.

PINO Ohjelmalle on varattu pino, josta funktiot voivat varata muistia
    siirtmll ss:esp -parin osoittamaa osoitetta. Pinossa vlitetn
    mys parametrit. Pino toimii LIFO-periaatteella (last in, first out),
    joka tarkoittaa, ett viimeiseksi sinne pantu tieto tulee ensimmisen
    pois. Pinoon tallennetaan ja sielt poistetaan tavaraa push ja pop
    kskyill. Lis tietoa kannattaa katsoa ulkoista assembleria 
    ksittelevst luvusta.

OBJEKTI Tll on kyttyhteydest riippuen muutamakin merkitys, mutta tr-
    keimmt lienevt alhaalla. Tietokonemaailmassa merkitys ei ole aivan
    sama kuin tosielmss, esimerkiksi seksiobjektista puhuttaessa. =)
    
    Objekti-sanaa kytetn puhuttaessa pelien esineist, sek myskin
    spriteist ja bittikartoista, eli kytnnss objekti on jokin
    yksittinen esine (luultavasti pelien kyttm nimitys on juuri perisin
    tst bittikartta-merkityksest, muistattehan te seikkailupelit?). 
    
    Mys olioille on joskus joissain yhteyksiss kytetty nimityst 
    objekti, vaikkei se nyt olekaan en kovin yleist, ainakaan minun
    tietkseni.

    Objektitiedosto taas on yksittinen, knnetty lhdekoodi (C, assembler
    tai joku muu), jossa on tiettyj segmenttej, jotka sisltvt koodia
    ja lukumuuttujia. Lisksi tllainen objektitiedosto sislt paljon
    tietoa funktioiden nimist ja funktioiden sijainnista tiedostossa,
    joita tarvitaan linkkauksessa (ks. LINKKAUS).

LINKKAUS Tm tapahtuma on viimeinen osa ohjelman knnsvaiheessa. Siin
    konekielelle knnetyt objektitiedostot "linkataan", eli liitetn
    yhteen ajettavaksi tiedostoksi. Omien objektitiedostojesi lisksi gcc
    linkkaa mukaan standardim C-kirjaston ja muut mrittelemsi kirjastot,
    sek ns. stubin (ks. STUB).

STUB Sijaitsee EXE:n alussa ja kynnistyy ohjelman kynnistyess. Sen 
    tehtv on raivata itse ohjelmalle sen koodin tarvitsema muistialue
    ja toimittaa muistipalvelut sun muut toimintakuntoon, jotta ohjelman
    ei tarvitse huolehtia nist. Stub tavallaan hoitaa ohjelman 
    suojatun tilan autuuteen sen omalle muistialueelle, jotta itse 
    pohjelmalla on helppoa. Tst syyst perusmuistin kulutus on suojatun
    tilan ohjelmissa niin pient, sill vain stub pit saada perusmuistiin
    ja sielt ksin se sitten hinaa pohjelman jatkettuun muistin, megan
    ylpuolelle.

REAALITILA PC:n "alkuperinen" tila, jossa on segmentit ja offsetit ja 1
    megan ylraja muistinkytll. Tt ollaan kierretty ohjelmilla kuten
    EMM386 ja HIMEM, jotka vastaavasti toimittavat suojatun tilan 
    jatkomuistia reaalitilan ohjelmille (ks. EMS ja XMS).

SUOJATTU TILA Jo 286-prosessorien mukana esitelty tila, mutta paremmin
    toteutettuna vasta 386:ssa (siksi DJGPP ei toimi vasta kuin sill).
    Suojatussa tilassa on kaikki muisti kytettviss ja prosessori 
    tarjoaa useita palveluja, kuten muistin suojaus ja monta muuta kivaa
    ominaisuutta, jolla yksittiset ohjelmat eivt pse niin helposti
    kaatamaan konetta.

EMS Expanded Memory Services on tapa antaa jatkettua muistia reaalitilan
    ohjelmille. Muistinhallintaohjelma, yleens EMM386.EXE toimii siten,
    ett se kytt suojattua tilaa hyvkseen mapaten yli megan alueelta
    muistipalasia alle megan alueelle muistin kohtaan, jota kutsutaan
    nimell PAGE FRAME (katso vaikka DOS:sin helpeist). Ohjelma voi varata
    EMS-sivuja, joiden koko on 16 kilotavua ja sitten asettaa niist 
    maksimissaan 4 kerralla nkyviksi (siksi page framen koko on yleens
    4*16=64kilotavua). Nopea tapa, muttei niin nopea kuin suora muistin
    osoitus.

XMS Extended Memory Services taas on systeemi, joka perustuu siihen, ett
    jrjestelm tarjoaa joukon funktioita, joilla voit varata XMS-muistia
    ja kopioida sit perusmuisti<->jatkomuisti, ja 
    jatkomuisti<->jatkomuisti -alueilla. Ongelmana on se, ett reaalitilan
    ohjelma ei voi ksitell jatkomuistia kuin kopioimalla sen ensin
    perusmuistiin ja sitten takaisin jatkomuistiin, mik tekee tst usein
    aika hitaan tavan.

PALETTI Tm on nytnohjaimen muistissa oleva taulukko, jossa on 
    vrinumeroiden (vrin 0, vrin 1, vrin 2 jne.) vriarvot, eli se
    paljonko mikkin vri sislt punaista, vihre ja sinist. Palettia
    ei kytet high-color ja true-color tiloissa (ks. HIGHCOLOR ja 
    TRUECOLOR), vaan ainoastaan 256- ja 16-vrisiss tiloissa. Lis
    paletin asettamisesta ja lukemisesta palettia ksittelevst luvusta.

HIGHCOLOR on vritila, jossa vrej on 65536 tai joissain tapauksissa 
    32768 kappaletta, eli 16-bittinen tai 15-bittinen pikseli 
    (ks. PIKSELI). Tmn pikselin vrinumero on jaettu yleens siten,
    ett numerosta 5 bitti on tarkoitettu punaiselle, 6 vihrelle ja
    5 siniselle (koska ihmisen silm kai aistii tarkimmin vihre),
    joten erillist palettia ei tarvita. 15-bittisess tilassa 
    vastaavasti on vain 5 bitti / vriarvo. Mys jotain virityksi 
    14-bittisist tiloista taitaa olla. Ks. mys PALETTI ja TRUECOLOR.

TRUECOLOR on vritila, jossa on 16.7 miljoonaa vri, tarkemmin 2^24 
    vri. Jako on kuten high-color tiloissa (ks. HIGHCOLOR), mutta
    jokaiselle vriarvolle on 8 bitti, eli 1 tavu punaiselle, vihrelle
    ja siniselle. Ei varmaan tarvitse erikseen mainita, ett tllaiset
    tilat ovat ohjelmoijan taivas. Uudempina on mys 32-bittiset tilat,
    joissa yksi tavu kytetn tietkseni hukkaan. Tm sen takia, ett
    32-bittinen pikseli (ks. PIKSELI) on paljon helpompi ksitell, kun
    rekisterit ovat 32-bittisi, samoin kuin joidenkin assembler-kskyjen
    kyttmt alkioiden koot. 24 tai 32 bitti voidaan varmaan jakaa useilla
    muillakin tavoilla (ks. CMYK ja RGB) ja tehokkaammin kuin kaksi edell
    esitetty, mutta en tied kuinka paljon kytnnss kytetn 
    toisenlaisia bittien jakotapoja.

CMYK Cyan, Magenta, Yellow, black. Tm on yksi tapa jakaa vriavaruus, 
    eli kytetn normaalin rgb-tripletin sijasta (ks. RGB) syaania,
    magentaa, keltaista ja mustaa. Mys CMY-tyyppi on nkynyt, josta siis
    musta puuttuu. Tt ei kytet kovin paljoa pelimaailmassa, mutta 
    printtereiden ja skannereiden kanssa toiminut on varmaan tst 
    kuullut. Muistaisin, ett on viel pari tapaa jakaa vrit, jokin YMK
    tai vastaava oli ainakin, mutta tiedn selitt vain CMYK-, CMY- ja
    RGB-mallit.

RGB Red, Green, Blue. Tapa jakaa vriavaruus, eli jokainen vri sen puna-
    viher- ja sinikomponentteihin. Vhn samaan tyyliin siis kun sekoitat
    Punaisesta, sinisest ja keltaisesta vesivrit ja muun vastaavan niin
    tietokoneella ja televisioissa kytetn tt tapaa. Tied sitten 
    miksi vihre, luultavasti se soveltuu paljon helpommin sdeputkelle.

RGB-AVARUUS Vriavaruus ajatellaan kuutioksi, jossa XYZ-akseliston
    korvaa RGB-akselisto. Kuutio on rajallinen ja rajat asettavat
    vrikomponenttien minimi- ja maksimiarvot. Esim. normaalin
    VGA-paletin vrit voidaan ajatella pisteiksi kuutiossa, jonka
    alakulma on (0,0) ja vastakkainen kulma (63,63).

KVANTISOINTI Paletin kvantisointi on tapa optimoida kytss olevaa 
    palettia. Useasti tarvittaisiin enemmn vrej kyttn kuin mit
    niit on kytettviss ja thn kytetn paletin vrien "optimointia",
    joissa yhdistelln toisiaan lhell olevia vrej. Kvantisoinnin
    tehtv on siis lyhyesti etsi optimaalinen n vri sisltv paletti
    jolla voidaan nytt mahdollisimman alkuperist vastaavasti 
    m-vrinen kuva.

MOODI Yleisesti kytetty lyhenne nytttilasta, screen mode.

HEKSA 16-kantainen, eli HEKSAdesimaalinen luku, kytetn monesti muisti-
    osoitteissa ja porttien numeroissa. Lis tietoa tiedostosta LUVUT.TXT

FYYSINEN OSOITE Ks. SELEKTORI

LOOGINEN OSOITE Ks. SELEKTORI

SIIRROSOSOITE Ks. OFFSET

POINTTERI Hiiren kursori tai yleens ohjelmoinnissa tietoalkio, joka 
    sislt muistiosoitteen. Pointteri nyttmuistiin on siis alkio,
    jonka arvo on nyttmuistin osoite (ks. OFFSET, SEGMENTTI).
    Hydylliseksi pointterin tekee se, ett sit voidaan indeksoida,
    eli sit voidaan kytt kantaosoitteena johonkin muistialueeseen.
    Yhden indeksin osoittaman alkion pituus on pointterityypin pituus.
    Jos pointteri on char-tyyppinen niin sen yksi alkio on yht pitk
    kuin yksi char-alkio, eli 1 tavu. Indeksi 10 olisi siis 10 tavua
    pointterin osoittamasta muistista eteenpin.

    Tt kytetn hyvksi esimerkiksi kaksoispuskurissa 
    (ks. KAKSOISPUSKURI), jossa pointteri osoittaa sen alkuun (samoin
    kuin indeksi 0) ja sen 600. alkio kaksoispuskurin 600. tavuun,
    tss tapauksessa tulevan framen (ks. FRAME) 600. pikseliin (ks.
    PIKSELI), olettaen ett ollaan 256-vrisess tilassa.

    Lyhesti: Pointteri on muistiosoite, indeksointi on tapa saada indeksin
    mrm alkio pointterin osoittamalta muistialueelta. Esim. 12. tavu
    pointterin alusta saataisiin pointterin indeksill 11 (indeksi 0 on
    1. tavu). Muista, ett int-tyyppisen pointterin yhden indeksin 
    osoittaman alkion pituus on sizeof(int), eli 4 tavua, jolloin indeksiss
    0 on 1., 2., 3. ja 4. tavu ja indeksiss 1 vastaavasti 5., 6., 7. ja 8.

FRAME Tarkoittaa yht nyttruudullista, yht nyttruudun pivityskertaa.
    Jos kytt kaksoispuskuria on frame se, mink kopioit nyttmuistiin.
    Lyhyesti frame siis on valmis, nytettvksi tarkoitettu ruudullinen
    kuva-informaatiota. Katso selvennykseksi mys kohta FRAMERATE.

FRAMERATE On se montako framea (ks. FRAME) jokin ohjelma voi tuottaa
    tietyss ajassa, yleens sekunnissa (tt framea/sekunti kutsutaan
    mys nimell FPS). Jos matopelisi esimerkiksi pystyisi pivittmn
    madon sijainnin ruudulla, esteet ja muut objektit vaikka 10 kertaa
    sekunnissa niin sen "FPS" olisi ninollen 10.

FPS, Frames per second. Ks. FRAMERATE.

VIRKISTYSTAAJUUS Luku ilmoitetaan yleens hertsein (herzein, hertzein?)
    ja se kertoo montako kertaa sekunnissa (hertsi, hZ tarkoittaa 
    vrhtely/sekunti) monitori ja nytnohjain (heikoin lenkki ratkaisee)
    pystyvt pivittmn nyttruutua. Normaalissa VGA-tilassa luku on 70,
    mink takia yleens sanotaan, ett hyvn toimintapelin tulisi pyri
    70 fps (ks. FRAMERATE). Tm ei kuitenkaan est sit, ett fps ei voisi
    olla suurempi kuin virkistystaajuus, mutta jos ohjelmasi pyritt
    300 kuvaa ruudulle sekunnissa (sopiva mr 3D-enginelle jollain yksin-
    kertaisella varjostuksella, kuten phongilla) niin vain 70 nkyy.

ANTIALIAS Tm termi esiintyy yleens englanninkielisess materiaalissa
    muodossa antialising, joka tarkoittaa kuvioiden reunojen pehmentmist
    vreill. Esimerkiksi kun piirrt vinon viivan vihrell mustalle poh-
    jalle se nytt aika rujolta, mutta kun list jokaiseen kulmaan hie-
    man tummanvihret nytt viiva huomattavasti pehmemmlt. Kytnnss
    tm hoidetaan laskemalla paljonko viivan "arvio" (se mit piirretn
    ruudulle) poikkeaa oikean viivan sijainnista ja mit enemmn se poikkeaa
    sen enemmn reunoille laitetaan samaa vri (vri siis sekoitetaan siin
    suhteessa miss viiva sijaitsee minkkin pikselin pll.

CROSSFADE Suomeksi termi voisi olla ehk ristiliu'utus. Ideana on, ett
    toinen kuva ilmestyy toisen takaa pikkuhiljaa, ensin vain haaleana,
    mutta voimistuen hiljalleen toisen hipyess ja lopuksi ensimmisest
    kuvasta onkin tullut jlkimminen.

PALETTE ROTATION Eli kauniimmin paletinpyritys rullaa palettia ympri siten,
    ett aiemmin vrin 3 toiminut muuttuu vriksi 2, vri 2 muuttuu vriksi
    1, 1 muuttuu 0:ksi ja nolla menee vriksi 255, vri 255 vriksi 254 jne.
    Eli siirretn koko palettia asken taaksepin (tai eteenpin) ja se joka
    ei voi en menn edemms tai taaemmas laitetaan toiseen phn 
    vapautuneelle paikalle. Mys osia paletista voidaan pyritt. Tm
    aiheuttaa varsin kivan nkisi efektej, etenkin jos tausta sislt
    vriliu'utuksia. Tarkempaa tietoa palettia koskevasta kappaleesta ja
    esimerkkiohjelmasta pal3.c.

RLE Taas uusi jnnittv lyhenne kokoelmaamme. Run Length Encoding 
    tarkoittaa kytnnss, ett kun meill on 5 kappaletta N-kirjaimia,
    niin ilmoitamme ne tyyliin 5N. Nin sstmme 3 tavua tilaa jo 
    tuossakin. Idea on siis, ett useat toistuvat merkit ilmoitetaan
    numerona ja merkkin. Toisaalta vaihteleva data on ongelma ja thn
    on useita kiertotapoja, kuten PCX:n lhestymistapa, jossa tavu, jonka
    arvo on yli 192 tarkoittaa ett seuraavaa tavua ilmestyy 
    <luettutavu>-192 kertaa, tai LBM-tyyli, jossa on yksi tavu, joka kertoo
    joko kuinka monta pakkaamatonta pikseli edess on, tai kuinka monta
    pakattua (muistaakseni jos n on alle 128 niin tarkoitetaan montako
    pakkaamatonta edess ja jos se on yli, niin sitten jotain tyyliin 
    n-127).

HANDLERI Tm kummajainen on suomeksi sama kuin ksittelij. Ohjelmassa
    on usein monenlaisia handlereita, kuten nppishandleri, joka ksittelee
    nppinten painallukset tai esimerkiksi interrupt handleri, joka 
    ksittelee toiminnan painaessa CTRL-BREAK tai CTRL-C 
    -nppinyhdistelmi. Ks. mys KESKEYTYS.

KESKEYTYS PC-perusrakenteeseen kuuluvat keskeytykset, jotka osa ovat nk.
    software-keskeytyksi ja osa hardware-keskeytyksi. Se kummantyyppinen
    keskeytys on riippuu siit aiheuttaako sen ohjelma itse (esimerkiksi
    videokeskeytys 0x10 jolla voidaan vaihtaa vaikka nytttilaa) vai
    generoidaanko se laitteiston toimesta (kuten ajastinkeskeytys, joka
    generoidaan halutuin vlein). Keskeytyksen satuttua komento siirtyy
    keskeytysksittelijn (ks. HANDLERI), joka hoitaa tarvittavat 
    toimenpiteet keskeytyksen satuttua. Kyttj voi itse koukuttaa 
    handlereita (ks. KOUKUTUS) ja nin tarjota keskeytyspalveluja tai
    kutsua itse keskeytyksi ja pyyt nilt keskeytysksittelijit
    palveluksia, kuten edellmainittu videotilan vaihto.

    PC on aika keskeytyspohjainen tietokone ja esimerkiksi kovalevyn 
    lukeminen ja muu vastaava tehdn yleens keskeytysten kautta. Monet
    ajurit koukuttavat laitteen keskeytyksen ja kommunikoivat itse laitteen
    kanssa, jolloin keskeytyst kutsuvan ohjelman ei tarvitse tiet 
    tarkasti miten laite toimii. Esimerkiksi hiirikeskeytyksen koukuttaa
    hiiriajuri ja ajuri hoitaa suoran kommunikoinnin hiiren kanssa ohjelman
    tarvitessa vain kutsua keskeytysksittelij generoimalla 
    hiirikeskeytys.

KOUKUTUS (HOOKING) Keskeytysksittelijn muistiosoite sijaitsee taulukossa 
    aivan muistin alussa (luoja tiet onko se siell suojatussa tilassa, 
    min en ainakaan tied, mutta sill ei onneksi ole vli) ja koukutus
    tarkoittaa sit, ett otat talteen alkup. keskeytysksittelijn
    osoitteen ja sijoitat omasi sinne osoitteen tilalle, jolloin keskeytyst 
    kutsuttaessa ksky siirtyy omalle ksittelijllesi (ks. mys HANDLERI
    ja KESKEYTYS). Voit mys kutsua vanhaa ksittelij oman toimintasi
    jlkeen.

LFB Tapaa osoittaa suoraan koko nyttmuistiin. Kuten VGA-segmentti
    A000h, mutta sijaitsee kaukana 1 megan rajan ylpuolella, joten
    kokorajoitus ei en ole 64 kiloa.

BANKED-TILAT Toinen tapa pst ksiksi yli 64 kilon nyttmuistiin on
    tehd pieni ikkuna (yleens sama kuin VGA-segmentti ja koko 64
    kiloa) jota liikutellaan pitkin nyttmuistia. Aika tuskainen
    verrattuna LFB:hen (ks. LFB)

VESA eli Video Electronics Standards Assocation, jonka ksialaa ovat
    mm. nytnohjaimien ksittelyyn yleisesti kytetty
    VESA-standardi. Virallinen nimi standardille lienee kuitenkin VBE
    (ks. VBE)

VBE eli VESA BIOS Extension on normaalin grafiikkakeskeytyksen 10h
    rinnalle toteutettu joukko laajennuksia joka mahdollistaa
    SVGA-tilojen nytnohjainriippumattoman ksittelyn. 1.2, 2.0 ovat
    suosittuja ja 3.0 on ihan skettin saapunut.


9.3 Lhteet
-----------

Muutamia erityismaininnan ansaitsevia dokumentteja shkisess ja
paperimuodossa sekalaisessa jrjestyksess, joiden sisltm
informaatiota on kytetty tmn tutoriaalin tekoon.


Tiedostot:


PCGPE10.ZIP
    Jokaisen ohjelmoijan pakkoimurointi. Sekalainen kokoelma valittuja
    paloja. Sislt 10 ensimmist Aphyxian traineria!

FMODDOC2.ZIP
    Kaikille nikorteista ja MOD-playereist kiinnostuineille hieman
    vaikea (nikortin ohjelmointi ei nimittin aina ole helppoa)
    tutoriaali sislten kaiken tarvittavan tiedon. Lytyy jokaisen
    itsen kunnioittavan TosiKooderin kovalevylt.

HELPPC21.ZIP
    Mainio asioiden tarkistamiseen soveltuva lhdeteos.

HPC21_P5.ZIP
    Pivitys edelliseen sislten Pentium-kskyt.

TUT*.ZIP
    Asphyxian VGA-trainerit. Etsi hakusanalla Asphyxia.

3DICA*.ZIP
    3D ohjelmoija-wannaben sek kokeneemmankin raamattu. Suomen
    kielell kaiken lisksi!

DJTUT255.ZIP 
    Selitt DJGPP:n AT&T-syntaksin ja inline-asseblerin
    englanniksi. Korvaamaton jos haluaa kytt assembleria
    DJGPP-ohjelmissaan!

ASSYT.ZIP
    Assemblerin alkeet suomeksi.

NASM095B.ZIP 
    Tll voit tehd Intel-syntaksin assemblerilla DJGPP:n
    COFF-muotoisia objektitiedostoja. Tiivistettyn TASM joka osaa
    myskin DJGPP:n objektiformaatin. Huomaa, ett uusin versio voi
    olla muutakin kuin 0.95 (095-osa tiedostonimess). 

ABEDEMO?.ZIP
    Ruotsalainen demokoulu. Ei onneksi ruotsia, vaan
    englantia. Ensimmisi lukemiani tutoriaaleja, joka auttoi minut
    alkuun koodauksessa.

INTER*.ZIP
    Ralph Brownin keskeytyslista. Sislt hurjan mrn paketteja ja
    kyll tietoakin.


Kirjallisuus:


Opeta itsellesi C++ -ohjelmointi 21 pivss
    Jos et viel osaa C++:ssaa tai C:t, niin tm voi olla
    lainaamisen arvoinen teos. Kokeneemmalle ohjelmoijalle
    suositeltavampi voi olla jokin muu, mutta monet on tm kirja
    auttanut alkuun.

486-ohjelmointi
    Aina kun joku on kysynyt assembler-ohjelmointia ksittelev
    kirjaa, niin tll hnet on vaiennettu. Omasta mielestnikin kelpo
    kirja.

Assembler-ohjelmointi
    Vaan joku kuitenkin oli sit mielt, ett 486-ohjelmointi ei ollut
    paras, vaan ett tm kirja olisi selkempi. Itse en ole tt
    lukenut.

Computer Graphics: Principles and Practice
    Grafiikkaohjelmoijan raamattu. Sislt paljon erilaisista
    algoritmeist sun muusta. Tietkseni.

Zen of graphics programming: Second edition
    Grafiikkaohjelmoijan koraani. Tosin nykyn VESA:n ja
    huippumodernien 3d-engineiden aikana osa tiedosta on
    vanhentunutta. Sislt kuitenkin todella tehokkaita
    optimointikikkoja sun muuta mukavaa.
