• Hur man skriver ett 4KB demo i C++ Del 2: Optimering

    I förra artikeln så gick jag igenom de grundläggande stegen för att få igång den extra optimering som behövs för att över huvud taget göra det möjligt att skriva program som tar mindre än 4KB exekverbart filminne.
    Nu tänkte jag gå igenom hur man skall tänka och göra för att faktiskt utnyttja minnesmängden fullt ut. Man måste anta själva filosofin bakom att jaga de där fåtal byten som kommer att göra skillnaden. Som exempel så satt jag en hel dag när jag skrev mitt 4KB spel maze_4k och kämpade för att hitta en metod och få ned spelet med sex bytes som var avgörande om det skulle få plats eller ej.
    Så om vi utgår ifrån koden i föregående artikel, och för enkelhets skull så klipper jag in den här nedan:
    Kod:
    #define WIN32_LEAN_AND_MEAN
    #include <windows.h>
    #include <GL/GL.H>
    
    // Size of window. Observe that this is not the size of the client area.
    #define WINDOW_WIDTH    800
    #define WINDOW_HEIGHT   600
    
    // Window name. We will also use it as window class name.
    WCHAR windowName[] = L"Minimal Win32 Hello Triangle";
    
    // WIndows message callback, we just use it to post the quit message
    LRESULT CALLBACK MsgProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
    {
        if((short)message == (short)WM_DESTROY)
        {
            PostQuitMessage(0);
        }
        else
        {
            return(DefWindowProc(hWnd, message, wParam, lParam));
        }
        return(0);
    }
    
    void ClearMem(void* dst, int size)
    {
        while (--size >= 0)
        {
            ((char*)dst)[size] = 0;
        }
    }
    
    // main function
    #ifdef _DEBUG
    int WINAPI WinMain(HINSTANCE hThisInst, HINSTANCE hPrevInst, LPSTR lpszArgs, int nWinMode)
    #else
    int WINAPI WinMainCRTStartup(void)
    #endif
    {
        // Local variables
        WNDCLASSEX wc;
        HDC hdc; 
        MSG msg;
        HWND hwnd;
        PIXELFORMATDESCRIPTOR pfd;
    
        // Init windows class
        ClearMem(&wc, sizeof(wc));
        wc.lpfnWndProc = MsgProc;
        wc.style = CS_CLASSDC;
        wc.cbSize = sizeof(WNDCLASSEX);
        wc.hInstance = GetModuleHandle(NULL);
        wc.lpszClassName = windowName;
    
        // Register windows class
        RegisterClassEx(&wc);
    
        // Create window
        hwnd = CreateWindow(
            windowName,
            windowName,
            WS_POPUPWINDOW | WS_CAPTION | WS_VISIBLE,
            CW_USEDEFAULT, CW_USEDEFAULT,
            WINDOW_WIDTH, WINDOW_HEIGHT,
            NULL,
            NULL,
            wc.hInstance,
            NULL
            );
    
        // Get device context
        hdc = GetDC(hwnd);
    
        // Init pixel format descriptor (describes what kind of frame buffer we want)
        ClearMem(&pfd, sizeof(pfd));
        pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR);
        pfd.nVersion = 1;
        pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
        pfd.iPixelType = PFD_TYPE_RGBA;
        pfd.cColorBits = 32;
        pfd.cDepthBits = 24;
        pfd.cStencilBits = 8;
        pfd.iLayerType = PFD_MAIN_PLANE;
    
        // Ask for a buffer of the format we described, and set it to the device context
        SetPixelFormat(hdc, ChoosePixelFormat(hdc, &pfd), &pfd);
    
        // Create OpenGL context and make it the current one.
        wglMakeCurrent(hdc, wglCreateContext(hdc));
    
        // Init the projection.
        glMatrixMode(GL_PROJECTION);
        glLoadIdentity();
    
        // Main loop
        while(true)
        {
            if(PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE))
            {
                if (msg.message == WM_QUIT)
                {
                    break;
                }
                DispatchMessage(&msg);
            }
            else
            {
                // OpenGL Hello triangle code
                glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
                glBegin(GL_TRIANGLES);
                glColor3f(1.0f, 0.0f, 0.0f);
                glVertex2f(0.0f, 0.5f);
                glColor3f(0.0f, 1.0f, 0.0f);
                glVertex2f(0.5f, -0.5f);
                glColor3f(0.0f, 0.0f, 1.0f);
                glVertex2f(-0.5f, -0.5f);
                glEnd();
                SwapBuffers(hdc);
            }
        }
    
        // Cleanup
        wglMakeCurrent(NULL, NULL);
        ReleaseDC(hwnd, hdc);
    
        return(0);
    }
    Koden ovan tar upp 924 bytes när vi kompilerar den med hjälp av Crinkler, och man kan ju tycka att den ser rätt minimal ut redan som den är, hur man man möjligen få ned den i minne...?
    Tja, vad sägs om raden:
    Kod:
    WCHAR windowName[] = L"Minimal Win32 Hello Triangle";
    "WCHAR" betyder att vi använder unicode tecken som Visual Studio kompilerar ned till två bytes per tecken. Och namnet är onödigt långt, eller hur?
    Så för att komma tillrätta med detta så går vi in i properties för projektet och ställr in:
    General -> Character Set = Use Multi-Byte character set.
    Samt ändrar koden till:
    Kod:
    char windowName[] = "iDmo";
    Efter en ny kompilering så ser vi att minnet nu är nere i 900 bytes, vi har just sparat 24 bytes - en fantastisk mängd minne i dessa sammanhangen.
    En poäng med det jag gjorde nu, är att testa sig fram. Notera hela tiden hur mycket minne som går åt, före och efter en förändring, så att man hela tiden kan backa tillbaka om det visade sig vara en dålig förändning. Eftersom Crinkler komprimerar den exekverbara filen, så kan slutresultatet ibland överraska, då man trodde att det skulle ta mindre minne, men blev mer... Detta kan man t.ex. testa genom att ställa om inställningarna för visual studio till att optimera kompileringen till att ta upp så lite minne som möjligt. Efter Crinkler har fått köra så kommer det nu att ta upp mer minne istället! Så testa er fram hela tiden, det är en av grundreglerna för att optimera 4KB produktioner.

    Så, vad mer kan vi göra för att få ned minnet?
    Tja, en funktion som jag retar mig på är "ClearMem" som vi var tvugna att lägga till för att "memset" föll bort ur runtime funktionerna... Men vi behöver ju rensa "WNDCLASSEX" och "PIXELFORMATDESCRIPTOR" strukturerna, hur löser vi det?
    Jo, det finns ett knep som är rätt dåligt dokumenterat i C/C++ världen... Knepet är att om man gör en variabel till en global statisk variabel, så kommer den att nollas vid inladdningen av den exekverbara filen!
    Så genom att i det globala namespacet defeniera:
    Kod:
    static WNDCLASSEX wc;
    static PIXELFORMATDESCRIPTOR pfd;
    Så kan vi ta bort de motsvarande lokala variablerna och hela "ClerMem" funktionen och de rader som kallar upp den.
    Programmet fungerer likadant och det kompilerar nu till 876 bytes, vi har sparat 24 bytes till!

    Nästa steg är att kolla på den helt korrekta initieringen av "PIXELFORMATDESCRIPTOR":
    Kod:
    pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR);
    pfd.nVersion = 1;
    pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
    pfd.iPixelType = PFD_TYPE_RGBA;
    pfd.cColorBits = 32;
    pfd.cDepthBits = 24;
    pfd.cStencilBits = 8;
    pfd.iLayerType = PFD_MAIN_PLANE;
    Ja, så där skall den enligt textböckerna se ut, men vad ingen bok säger, är att i Microsofts implementation så finns det fallbacks utifall vissa värden saknas... Vi kan med andra ord utnyttja odokumenterad kod till att initiera en del av värdena åt oss.
    Och hur har jag då lyckats ta reda på detta då? Det har jag inte, internet är fullt av folk som har tagit reda på allt du behöver veta, så lär dig googla och hela världen ligger för dina fötter!
    Vi moddar om koden till det minimala och fullt fungerande:
    Kod:
    pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
    pfd.cDepthBits = pfd.cColorBits = 32;
    Och programmet kompilerar nu till 865 bytes, ytterligare 11 bytes sparat (tänk på att jag vid ett tillfälle ägnade en dag till att bespara 6 bytes).

    Nästa sak jag hittar att spara minne på är att vi är snälla nog och avslutar programmet på ett windows korrekt vis... Vi har ett par spridda rader som ser till att fånga upp att man vill avsluta programmet och lämnar tillbaka resurser etc.
    Raderna jag menar är:
    Kod:
    PostQuitMessage(0);
    ...
    if (msg.message == WM_QUIT)
    {
        break;
    }
    ...
    wglMakeCurrent(NULL, NULL);
    ReleaseDC(hwnd, hdc);
    Men, vi är inte så snälla mot windows, vi tycker att windows gott kan få jobba lite självt... Typ: vi stänger hårt ned processen och låter windows fånga upp det och hantera alla resurser självt. Absulut inget sätt man skall göra i en riktig produktion, men det fungerar utan problem för oss i detta fallet.
    Så vi liksom bara tar bort de omnämnda spridda raderna och istället för:
    Kod:
    PostQuitMessage(0);
    Lägger vi in:
    Kod:
    ExitProcess(0);
    Programmet kompilerar nu till 830 bytes, vi sparade 35 bytes på det!

    Vid detta laget så har jag inte längre några mer knep för att optimera nuvarande kod mer... Storsläggan måste tas till...
    Programmet körs just nu i ett fönster, och faktum är, att om vi skulle välja att köra det hela i fullskärmsläge, så kan vi göra en del mirakel... Tyvärr så innebär detta så mycket kodförändringar, att det blir svårt att snyggt gå igenom alla. Vad jag har valt att göra, är att en bit nedan klippa in hela den förändrade koden, med fullskärm och alla ovanstående förändringar, så att ni lätt kan ta och klippa in den i det existerande projektet från förra artikeln och bara köra.
    Men, jag kan ändå beskriva vilka detaljer och "hack" som görs för att få igång fullskärm.

    Jag har lagt till en define som sätts om man kompilerar release:
    Kod:
    #ifndef _DEBUG
    #define FULLSCREEN
    #endif
    Detta för att det är enklare att debugga i fönsterläge än i fullskärm.
    Det gör det också enklare att se vad det är i koden som är fullskärmsförändringarna.

    Hela tanken med att gå över till fullskärm är att om vi tar upp hela skärmen, så behöver vi ju inget synligt fönster. I windows så måste man alltid ha ett fönster, men vi kan undvika att skapa en egen window class, så allting som har med "WNDCLASSEX" kan tas bort.
    Fast, säger någon, man kan ju inte skapa ett fönster utan en window class... Nä, däför använder vi en av systemet defenierad window class: "edit". Det är en class som används för edit boxar, men eftersom vi ändå inte skall visa fönstret, så kan vi använda den till att skapa fönstret med.
    En annan sak som påverkas av att vi inte har någon egen window class, är att vi inte längre kan ta emot några meddelanden från meddelande loopen... Så vi kan inte längre få ett meddelande om att användaren vill avsluta programmet... Något som kan irritera folk om man kör igång ett fullskärmsprogram som inte går att avsluta... Så istället så får vi ändra om loopen lite så att raderna:
    Kod:
    if (GetAsyncKeyState(VK_ESCAPE))
    {
        ExitProcess(0);
    }
    Kan fånga upp om vi trycker på escape tangenten och då avslutar programmet.

    Med alla förändringar, så kompilerar nu programmet i fullskärmsläge till 734 bytes. Vi har sparat 190 bytes från hur det var i tidigare artikel.
    Inte illa från att från början varit ett minimalistiskt program!

    Här är dessutom hela den nuvarande koden:

    Kod:
    #define WIN32_LEAN_AND_MEAN
    #include <windows.h>
    #include <GL/GL.H>
    
    #ifndef _DEBUG
    #define FULLSCREEN
    #endif
    
    // Size of window. Observe that this is not the size of the client area.
    #define WINDOW_WIDTH    800
    #define WINDOW_HEIGHT   600
    
    // Window name. We will also use it as window class name.
    #ifndef FULLSCREEN
    char windowName[] = "iDmo";
    #endif // FULLSCREEN
    
    #ifndef FULLSCREEN
    static WNDCLASSEX wc;
    #endif // FULLSCREEN
    
    static PIXELFORMATDESCRIPTOR pfd;
    
    #ifndef FULLSCREEN
    // WIndows message callback, we just use it to post the quit message
    LRESULT CALLBACK MsgProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
    {
        if((short)message == (short)WM_DESTROY)
        {
            ExitProcess(0);
        }
        else
        {
            return(DefWindowProc(hWnd, message, wParam, lParam));
        }
        return(0);
    }
    #endif // FULLSCREEN
    
    // main function
    #ifdef _DEBUG
    int WINAPI WinMain(HINSTANCE hThisInst, HINSTANCE hPrevInst, LPSTR lpszArgs, int nWinMode)
    #else
    int WINAPI WinMainCRTStartup(void)
    #endif
    {
        // Local variables
        HDC hdc; 
        MSG msg;
    
    #ifndef FULLSCREEN
        // Init windows class
        wc.lpfnWndProc = MsgProc;
        wc.style = CS_CLASSDC;
        wc.cbSize = sizeof(WNDCLASSEX);
        wc.hInstance = GetModuleHandle(NULL);
        wc.lpszClassName = windowName;
    
        // Register windows class
        RegisterClassEx(&wc);
    
        // Create window and get device context
        hdc = GetDC(CreateWindow(
            windowName,
            windowName,
            WS_POPUPWINDOW | WS_CAPTION | WS_VISIBLE,
            CW_USEDEFAULT, CW_USEDEFAULT,
            WINDOW_WIDTH, WINDOW_HEIGHT,
            NULL,
            NULL,
            wc.hInstance,
            NULL
            ));
    
        // Init pixel format descriptor (describes what kind of frame buffer we want)
        pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
        pfd.cDepthBits = pfd.cColorBits = 32;
    
    #else // FULLSCREEN
        hdc = GetDC(CreateWindow("edit", 0, WS_POPUP | WS_VISIBLE | WS_MAXIMIZE, 0, 0, 0, 0, NULL, NULL, NULL, NULL));
    
        // Init pixel format descriptor (describes what kind of frame buffer we want)
        pfd.dwFlags = PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
        pfd.cDepthBits = pfd.cColorBits = 32;
    
    #endif // FULLSCREEN
    
        // Ask for a buffer of the format we described, and set it to the device context
        SetPixelFormat(hdc, ChoosePixelFormat(hdc, &pfd), &pfd);
    
        // Create OpenGL context and make it the current one.
        wglMakeCurrent(hdc, wglCreateContext(hdc));
    
        // Init the projection.
        glMatrixMode(GL_PROJECTION);
        glLoadIdentity();
    
        // Main loop
        while(true)
        {
    #ifndef FULLSCREEN
            if(PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE))
            {
                DispatchMessage(&msg);
            }
    #else // FULLSCREEN
            if (GetAsyncKeyState(VK_ESCAPE))
            {
                ExitProcess(0);
            }
            else
    #endif // FULLSCREEN
            {
                // OpenGL Hello triangle code
                glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
                glBegin(GL_TRIANGLES);
                glColor3f(1.0f, 0.0f, 0.0f);
                glVertex2f(0.0f, 0.5f);
                glColor3f(0.0f, 1.0f, 0.0f);
                glVertex2f(0.5f, -0.5f);
                glColor3f(0.0f, 0.0f, 1.0f);
                glVertex2f(-0.5f, -0.5f);
                glEnd();
                SwapBuffers(hdc);
            }
        }
    
        return(0);
    }
    Därmed så avslutar jag denna delen. I nästa del så tänker jag att upp något som man alltid behöver i demos och spel: texturer. Hur sjutton får man in alla dessa minneskrävande texturer på så lite minne? Det är ju helt galet! Eller...