• Hur man skriver ett 4KB demo i C++

    En gång i tiden på en annan plats på internet, så skrev jag ett spel som uppfyllde följande kriterier: spelet fick endast bestå av en fil, och filen fick inte vara större än 4KB. Spelet kunde få använda sig av vilket content som helst så länge som det fanns tillgängligt i densamme fil eller fanns på en standard windows installation.
    Resultatet blev spelet som finns på följande Pouet.net länk: maze_4k
    Tyvärr så fungerar det inte på Windows 7 på grund av vissa förändringar i hur header formatet för exekverbara filer tolkas utav.
    Det här spelet går under samma kategori som så kallade 4K demos gör, och ibland hör jag hur folk pratar om dessa demos, som om det vore magiska, vilket är långt ifrån fallet. Så, jag tänkte ta och skriva en serie med inlägg som bit för bit går igenom trixen med att göra produktioner på endast 4K. Tanken är att börja med det grundläggande och sätta upp en basmiljö för att kunna utveckla 4K produktioner, för att sedan steg för steg utveckla en riktig produkt. Förhoppningsvis så lyckas jag med att ta det lugnt och sansat så att varje steg blir uppenbart. För sakens skull så kommer jag hålla mig till ren C++ kod, och inte optimera med hjälp av inline assembler som dramatiskt försämrar möjligheten till att förstå koden. Jag kommer med andra ord att offra en del minne på att göra det lättare att förstå.

    Så, låt oss starta med att förstå vad en exekverbar fil gör och vad vi kan göra för att minska storleken på den.
    Häromdagen så postade jag en minimal c++ kod för att starta upp OpenGl i ett fönster: Minimal OpenGL
    När man kompilerar den koden i release mode i Visual Studio 2010, så får man en exekverbar fil på 8192 bytes. Detta utan att ta till några speciella tricks eller så. Man kan tänka sig att det går att få ned en bit till med hjälp av olika kompileringsflaggor etc. men vi kommer inte ens komma i närheten av de 4K vi måste hålla oss under. Så blir det verkligen sådär mycket exekverbar kod av den där lilla programsnutten? Nä. Majoriteten av den exekverbara koden är standard runtime kod för C/C++ program. I den koden finns en del standard funktioner samt en del startupkod som kompilatorn och linkern tycker är bra o ha.
    En del exekverbar kod kan man via vissa tricks få bort, men det finns ett mycket bra generellt trick att ta till som gör en väldans massa saker så otroligt mycket enklare för oss. Tricket kallas för "Crinkler" och finns på: Crinkler
    Crinkler är en Schweizisk armekniv för de som vill skriva en 4K produktion. Det är en filkomprimerare som är specialgjord för exekverbara filer. Den bakar dessutom in en mycket liten dekomprimerare i den exekverbara koden, så att programmet den komprimerar blir självuppackande. Men det är inte allt. Crinkler går att koppla till Visual Studio och använda som en linker. Detta gör att Crinkler kan gå in och ta bort allt det som kompilatorn lägger in som bra o ha grejjer, men som bara ökar minnet för oss. Crinkler gör en massa andra saker också, men om ni vill veta exakt vad så är nog deras dokumentation bättre att studera.
    Så, första steget för oss att göra, är att se till att få igång Crinkler på den minimalistiska OpenGL koden jag nämnde tidigare.

    Jag går här igenom steg för steg vad som måste göras för att få igång Crinkler på Visual Studio 2010, men för de utav er som är lata, så finns en förberedd solution här: minimal.zip
    Väljer ni att gå på den solution filen, så hoppa då över alla stegen fram till 4.

    För att komma igång med projektet:
    1. Starta Visual Studio 2010, och skapa ett nytt tomt Win32 projekt.
    2. Skapa en fil i projektet och kalla den för "main.cpp". Klipp in main.cpp koden nedan som är en modifierad version av den minimalistiska OpenGL koden. Jag går även igenom och förklarar modifieringarna nedan.
    3. Högerklicka på projektnamnet i Solution explorer, och välj "Properties" och välj Configuration "Release". Justera sedan följande:
      1. VC++ Directories -> Executable Directories = $(SolutionDir)
        Berättar för Visual Studio att vi vill att vårt solution directory skall sökas igenom när någon exekverbar fil skall kallas upp. Mer om det i steg 6.
      2. C/C++ -> Optimization -> Whole Program Optimization = No
        Måste stängas av så att det inte stör Crinkler.
      3. C/C++ -> Code Generation -> Buffer Security Check = No
        Jag fick problem om jag inte stängde av denna...
      4. Linker -> Input -> Additional Dependencies = OpenGL32.lib
        Se beskrivningen om modifieringarna i main.cpp. (Sätt denna för debug också om du vill kunna debugga)
      5. Linker -> Command Line -> Additional Options = /CRINKLER /RANGEpengl32
        Detta är det som berättar för Crinkler att vi vill använda den som linker och vi säger även att vi vill ha med en koppling till OpenGL32.dll i vårt program.
    4. Ladda ned Crinkler, men notera att du kan behöva tänka på vilken version du plockar hem. Om du kompilerar till Windows 7, så ladda hem: Crinkler 1.3 annars laddar du hem Crinkler 1.2.
    5. Packa upp Crinkler på något lämpligt ställe, och kopiera sedan in crinkler.exe in i den folder där ditt solution ligger.
    6. Döp om crinkler.exe till link.exe, och det är nu det häftiga kommer in... Eftersom vi ovan har talat om för Visual Studio att söka igenom vårt solution directory när det skall köra en exekverbar fil, så kommer crinkler som nu är omdöpt till link.exe att köras istället för visual studio linkern. Detta gäller för just detta projektet.
    7. Bygg och kör programmet. Det skall funka nu.


    Med all sannolikhet så borde storleken på den exekverbara filen vara omkring 924 bytes nu, vilket gör att vi tappat bort över 7KB och lätt hamnar under 4KB gränsen!

    Så, som utlovat så är main.cpp koden här:
    Kod: [Visa]
    #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);
    }
    Så vad är det då för förändringar, och varför behövs dom?

    Första förändringen:
    Kod: [Visa]
    #pragma comment(lib, "OpenGL32")
    Denna kod fick tas bort, då Crinkler inte förstod den. Därför fick vi istället använda den korrekta metoden att beskriva detta på, vilket gjordes i steg 3.4.

    Andra förändringen:
    Kod: [Visa]
    void ClearMem(void* dst, int size)
    {
        while (--size >= 0)
        {
            ((char*)dst)[size] = 0;
        }
    }
    På några ställen i koden hade jag använt memset(..), vilket tyvärr är en av de där bra o ha funktionerna som finns med i C/C++ runtime funktionerna som Crinkler optimerat bort... Så därför har jag bytt ut memset till ClearMem och laggt till denna koden som nollar en minnesarea.

    Tredje förändringen:
    Kod: [Visa]
    #ifdef _DEBUG
    int WINAPI WinMain(HINSTANCE hThisInst, HINSTANCE hPrevInst, LPSTR lpszArgs, int nWinMode)
    #else
    int WINAPI WinMainCRTStartup(void)
    #endif
    Återigen så har Crinkler tagit bort något, och i detta fallet så var det självaste startupkoden. Startup koden startar normalt i en funktion som heter "WinMainCRTStartup" där lite svart magi sker innan "WinMain" kallas upp. Eftersom Crinkler inte gillar svart magi, så kommer man aldrig till "WinMain". Så därför ser koden ovan till att programmet startar i "WinMainCRTStartup" om man kompilerar för release. För debug så har jag låtit Crinkler vara avstängt, så det är därför jag ifdef'ar koden. Det är gott att kunna debugga ibland, och då betyder ju inte heller filstorleken något.

    Så, med lite förhoppning så har jag inte varit alldeles vilseledande, och med lite tur så kan jag snart få tid med att skriva uppföljningen där vi kommer att ta bort en massa onödig kod. Det går att få ned filstorleken en bit bara genom att optimera existerande kod. Eller hur? Den gör ju jättemycket onödiga saker!
    This article was originally published in blog: Hur man skriver ett 4KB demo i C++ started by Hildenborg