Que signifie "émuler" ?

"En informatique, l'émulation est l'ensemble des techniques utilisées pour qu'une machine obtienne les même résultats qu'une autre"

Quelques émulateurs

  • PSX: EPSX, PCSX
  • PS2: PCSX2
  • GBA: VBA, No$GBA
  • Wii: Dolphin
  • Misc: QEMU
  • ...

Cas d'étude: la Gameboy

Fiche technique

  • Démarrage le 21 Avril 1989
  • Arrêt le 23 mars 2003
     
  • 245 instructions
  • 8k RAM
  • 8k Video RAM
  • 256k - 8Mo ROM

L'intérieur d'un émulateur

  • CPU
  • MMU
  • GPU
  • Timer / Input / Sounds

CPU

Fait correspondre un entier à une action

Ex: 0x80 signifie "A = A + B"

CPU : Implémentation

  • Un énorme switch / case
  • Tableau de fonctions
  • Code auto-généré
  • Facilement testable

    'LDHL_r16_i8' : ( address, nextAddress, parameters, h ) => `

        var hlBefore = ${h.readR16(parameters[0])};
        var hlAfter = ${h.add16('hlBefore', parameters[1])};

        ${h.writeR16('hl', 'hlAfter')};

        ${h.bcd(false)};
        ${h.zero(false)};
        ${h.half('hlAfter', '<', 'hlBefore')};
        ${h.carry('hlAfter', '<', 'hlBefore')};

        ${h.applyClockCycles(3)};

    `,

    'LDI_(r16)_r8' : ( address, nextAddress, parameters, h ) => `

        var position = ${h.readR16(parameters[0])};
        ${h.writeMem8('position', h.readR8(parameters[1]))};

        ${h.writeR16(parameters[0], h.add16('position', 1))}

        ${h.applyClockCycles(2)};

    `,

    'LDI_r8_(r16)' : ( address, nextAddress, parameters, h ) => `

        var position = ${h.readR16(parameters[1])};
        ${h.writeR8(parameters[0], h.readMem8('position'))};

        ${h.writeR16(parameters[1], h.add16('position', 1))};

        ${h.applyClockCycles(2)};

    `,

    'LDD_(r16)_r8' : ( address, nextAddress, parameters, h ) => `

        var position = ${h.readR16(parameters[0])};
        ${h.writeMem8('position', h.readR8(parameters[1]))};

        ${h.writeR16(parameters[0], h.sub16('position', 1))}

        ${h.applyClockCycles(2)};

    `,

MMU

Route les accès mémoire vers le "hardware"

L'équivalent des getters / setters en Javascript

MMU : Implémentation

  • Une table de fonctions
    • Trop lent pour être considéré
  • Plein de if pour gérer les ranges
    • Fallback sur un switch / case pour le reste☺
  • Relativement facilement debuggable

    _fastWriteUint8( address, value ) {

        if ( address >= 0x0000 && address < 0x8000 )
            this.mbc.writeRomUint8( address, value );

        else if ( address >= 0x8000 && address < 0xA000 ) {
            if ( ! this._environment.gpuLcdFeature || this._environment.gpuMode !== 0x03 ) {
                this._vramBankNN[ address & 0x1FFF ] = value;
                if ( address < 0x9800 ) {
                    this._gpu.updateTile( this._environment.cgbVramBank, address & 0x1FFF );
                } else if ( this._environment.cgbVramBank === 0x01 ) {
                    this._gpu.updateMetadata( address - 0x9800 );
                }
            }
        }

        else if ( address >= 0xA000 && address < 0xC000 )
            this.mbc.writeRamUint8( address - 0xA000, value );

        else if ( address >= 0xC000 && address < 0xD000 )
            this._wramBank00[ address - 0xC000 ] = value;

        else if ( address >= 0xD000 && address < 0xE000 )
            this._wramBankNN[ address - 0xD000 ] = value;

        else if ( address >= 0xE000 && address < 0xF000 )
            this._wramBank00[ address - 0xE000 ] = value;

        else if ( address >= 0xF000 && address < 0xFE00 )
            this._wramBankNN[ address - 0xF000 ] = value;

        else if ( address >= 0xFE00 && address < 0xFEA0 ) {
            if ( ! this._environment.gpuLcdFeature || this._environment.gpuMode <= 0x01 ) {
                this._oam[ address - 0xFE00 ] = value;
                this._gpu.updateSprite( address - 0xFE00 );
            }
        }

        else if ( address >= 0xFF80 && address < 0xFFFF )
            this._hram[ address - 0xFF80 ] = value;

        else switch ( address ) {

            case 0xFF00:
                this._environment.ioKeyColumn = value & 0x30;
            break ;

            case 0xFF04:
                this._environment.timerDivider = 0;
            break ;

            case 0xFF05:
                this._environment.timerCounter = value;
            break ;

            case 0xFF06:
                this._environment.timerCounterModulo = value;
            break ;

            case 0xFF07:
                this._environment.timerCounterFeature = ( value & 0b100 ) >>> 2;
                this._environment.timerCounterFrequency = timerFrequencies[ ( value & 0b011 ) >>> 0 ];
                this._environment.timerCounterControl = ( value & 0b111 ) >>> 0;
            break ;

            case 0xFF0F:
                this._environment.pendingInterrupts = value;
            break ;

GPU

Transforme la Video RAM en tableau de pixels

Bien plus complexe qu'un CPU ! Nombreuses règles

GPU : Implémentation

  • Machine à états (4 différents)
  • Beaucoup de règles différentes
  • Beaucoup d'exceptions aux règles à implémenter
  • Difficile à tester
  • Très difficile à débugger

    for ( var x = 0; x < ROW_PIXELS; ++ x ) {
    
        // Same computations than before, but for X coordinates
    
        var actualX = ( scrollX + offsetX + x ) & 0xFF;
        var mapOffsetX = ( actualX >>> 3 ) & 31;
        var tileX = actualX & 0x7;
    
        // Knowing the X and Y map offset, we can now fetch the tile index from the VRAM
    
        var mapOffset = baseAddress + mapOffsetY + mapOffsetX;
        var tileIndex = this._vramBank00[ mapOffset ];
    
        // When using the second tileset, the index is actually a signed number so that whatever the tileset, the same index greater than 0x7F (such as 0xFF) will always point toward the same tile (that's actually pretty clever!)
    
        if ( ! this._environment.gpuTilesetBase )
            if ( tileIndex > 0x7f )
                tileIndex -= 0x100;
    
        // In CGB, each mixed tile has also metadata associed
    
        if ( this._environment.cgbUnlocked ) {
    
            var metadata = this._metadata[ mapOffset - 0x1800 ];
    
            if ( metadata.xflip )
                tileX = 8 - ( tileX + 1 );
    
            if ( metadata.yflip )
                tileY = 8 - ( tileY + 1 );
    
            vramBank = metadata.bank;
            palette = this._environment.cgbBackgroundRgbPalettes[ metadata.paletteCgb ];
    
        }
    
        // We just have to get the palette index color stored in the tileset cell, then the color from the palette
    
        var paletteIndex = this._tilesets[ vramBank ][ tilesOffset + tileIndex ][ tileY ][ tileX ];
        var trueColor = palette[ paletteIndex ];
    
        // We store the palette index inside the 'color' (internally only, it will disappear when sent to the screen device), because the sprite may be behind the background. In such case, we have to know if they are behind a transparent pixel or not.
    
        this._scanline[ x ] = ( paletteIndex << 24 ) | trueColor;

Les outils développés

Virtjs

Implémente des devices interchangeables

Permet de se concentrer sur le développement du core

PProcjs / Audiojs

Implémentent des devices additionnels pour Virtjs

Permettent d'appliquer facilement du postprocessing !

Archjs

Compile les coeurs de la Libretro via Emscripten

Utilise les interfaces Virtjs pour tourner partout


    var Engine = Archjs.byName.vbanext;

    var canvas = document.querySelector('#screen');

    // Start a WebGL renderer on the specified canvas
    var screen = new WebGLScreen({ canvas });
    screen.setOutputSize(canvas.width, canvas.height);

    // Listen for keyboard actions and bind them to the engine keycodes
    var input = new KeyboardInput({ codeMap: Engine.codeMap });

    // Use requestAnimationFrame for the internal timer
    var timer = new AnimationFrameTimer();

    // Use Audiojs to provide an audio output using browsers APIs
    var audio = new AudiojsAudio();

    // Finally create the engine, linked to our devices
    var engine =  new Engine({ devices: { screen, timer, input, audio } });

    // Load the game ROM (you can use window#fetch to get an ArrayBuffer from an HTTP stream)
    engine.loadArrayBuffer(arrayBuffer, { fileName });

    // You can easily get a save state at any time
    var state = engine.getState();

    // And use this save state to restore your game
    engine.setState(state);

    // That's it!

Start9.io

Une plate-forme construite au dessus d'Archjs

Archivez vos jeux, jouez, sauvegardez votre progression

Emulation en Javascript

By arcanis

Emulation en Javascript

  • 1,716