hedgewars/uAtlas.pas
changeset 7292 18430abfbcd2
child 7295 e70b81854fb9
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hedgewars/uAtlas.pas	Mon Jun 25 12:01:19 2012 +0200
@@ -0,0 +1,701 @@
+{$INCLUDE "options.inc"}
+{$IF GLunit = GL}{$DEFINE GLunit:=GL,GLext}{$ENDIF}
+
+unit uAtlas;
+
+interface
+
+uses SDLh, uTypes;
+
+procedure initModule;
+
+function Surface2Tex_(surf: PSDL_Surface; enableClamp: boolean): PTexture;
+procedure FreeTexture_(sprite: PTexture);
+
+implementation
+
+uses GLunit, uBinPacker, uDebug, png, sysutils;
+
+const
+    MaxAtlases = 1;    // Maximum number of atlases (textures) to allocate
+    MaxTexSize = 4096; // Maximum atlas size in pixels
+    MinTexSize = 128;  // Minimum atlas size in pixels
+    CompressionThreshold = 0.4; // Try to compact (half the size of) an atlas, when occupancy is less than this
+
+type
+    AtlasInfo = record
+        PackerInfo: Atlas;     // Rectangle packer context
+        TextureInfo: TAtlas;   // OpenGL texture information
+        Allocated: boolean;    // indicates if this atlas is in use
+    end;
+
+var
+    Info: array[0..MaxAtlases-1] of AtlasInfo;
+
+
+////////////////////////////////////////////////////////////////////////////////
+// Debug routines
+
+var
+    DumpID: Integer;
+    DumpFile: File of byte;
+
+const
+    PNG_COLOR_TYPE_RGBA = 6;
+    PNG_COLOR_TYPE_RGB = 2;
+    PNG_INTERLACE_NONE = 0;
+    PNG_COMPRESSION_TYPE_DEFAULT = 0;
+    PNG_FILTER_TYPE_DEFAULT = 0;
+    
+
+
+procedure writefunc(png: png_structp; buffer: png_bytep; size: QWord); cdecl;
+var
+    p: Pbyte;
+    i: Integer;
+begin
+  //TStream(png_get_io_ptr(png)).Write(buffer^, size);
+    BlockWrite(DumpFile, buffer^, size);
+{    p:= PByte(buffer^);
+    for i:=0 to pred(size) do
+    begin
+        Write(DumpFile, p^);
+        inc(p);
+    end;}
+end;
+
+function IntToStrPad(i: Integer): string;
+var
+  s: string;
+begin
+   s:= IntToStr(i);
+   if (i < 10) then s:='0' + s;
+   if (i < 100) then s:='0' + s;
+
+   IntToStrPad:=s;
+end;
+
+procedure DumpAtlas(var info: AtlasInfo);
+var
+    png: png_structp;
+    png_info: png_infop;
+    w, h, sz: Integer;
+    filename: string;
+    rows: array of png_bytep;
+    size: Integer;
+    i, j: Integer;
+    mem, p, pp: PByte;
+begin
+    filename:= '/home/wolfgangst/hedgewars/dump/atlas_' + IntToStrPad(DumpID) + '.png';
+    Assign(DumpFile, filename);
+    inc(DumpID);
+    Rewrite(DumpFile);
+
+    w:= info.TextureInfo.w;
+    h:= info.TextureInfo.h;
+    size:= w * h * 4;
+    SetLength(rows, h);
+    GetMem(mem, size);
+
+    glBindTexture(GL_TEXTURE_2D, info.TextureInfo.id);
+
+    glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, mem);
+
+    p:= mem;
+    for i:= 0 to pred(h) do
+    begin
+        rows[i]:= p;
+        pp:= p;
+        inc(pp, 3);
+        {for j:= 0 to pred(w) do
+        begin
+            pp^:=255;
+            inc(pp, 4);
+        end;}
+        inc(p, w * 4);
+    end;
+
+    png := png_create_write_struct(PNG_LIBPNG_VER_STRING, nil, nil, nil);
+    png_info := png_create_info_struct(png);
+
+    png_set_write_fn(png, nil, @writefunc, nil);
+    png_set_IHDR(png, png_info, w, h, 8, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
+    png_write_info(png, png_info);
+    png_write_image(png, @rows[0]);
+    png_write_end(png, png_info);
+    png_destroy_write_struct(@png, @png_info);
+
+    FreeMem(mem);
+    
+    SetLength(rows, 0);
+    Close(DumpFile);
+
+    //if (DumpID >= 30) then
+    //    halt(0);
+end;
+
+////////////////////////////////////////////////////////////////////////////////
+// Upload routines
+
+function createTexture(width, height: Integer): TAtlas;
+var
+  nullTex: Pointer;
+begin
+    createTexture.w:= width;
+    createTexture.h:= height;
+    createTexture.priority:= 0;
+    glGenTextures(1, @createTexture.id);
+    glBindTexture(GL_TEXTURE_2D, createTexture.id);
+
+    //glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nil);
+    
+    GetMem(NullTex, width * height * 4);
+    FillChar(NullTex^, width * height * 4, 0);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NullTex);
+    FreeMem(NullTex);
+
+    glBindTexture(GL_TEXTURE_2D, 0);
+end;
+
+function Min(x, y: Single): Single;
+begin
+  if x < y then
+    Min:=x
+  else Min:=y;
+end;
+
+function Max(x, y: Single): Single;
+begin
+  if x > y then
+    Max:=x
+  else Max:=y;
+end;
+
+
+procedure HSVToRGB(const H, S, V: Single; out R, G, B: Single); 
+const 
+    SectionSize = 60/360; 
+var 
+    Section: Single; 
+    SectionIndex: Integer; 
+    f: single; 
+    p, q, t: Single; 
+begin
+    if H < 0 then 
+    begin 
+        R:= V; 
+        G:= R; 
+        B:= R; 
+    end 
+    else 
+    begin 
+        Section:= H/SectionSize; 
+        SectionIndex:= Trunc(Section); 
+        f:= Section - SectionIndex; 
+        p:= V * ( 1 - S ); 
+        q:= V * ( 1 - S * f ); 
+        t:= V * ( 1 - S * ( 1 - f ) ); 
+        case SectionIndex of 
+            0: 
+            begin 
+                R:= V; 
+                G:= t; 
+                B:= p; 
+            end; 
+            1: 
+            begin 
+                R:= q; 
+                G:= V; 
+                B:= p; 
+            end; 
+            2: 
+            begin 
+                R:= p; 
+                G:= V; 
+                B:= t; 
+            end; 
+            3: 
+            begin 
+                R:= p; 
+                G:= q; 
+                B:= V; 
+            end; 
+            4: 
+            begin 
+                R:= t; 
+                G:= p; 
+                B:= V; 
+            end; 
+            else 
+                R:= V; 
+                G:= p; 
+                B:= q; 
+        end; 
+    end; 
+end; 
+
+procedure DebugColorize(surf: PSDL_Surface);
+var
+    sz: Integer;
+    p: PByte;
+    i: Integer;
+    r, g, b, a, inva: Integer;
+    randr, randg, randb: Single;
+    randh: Single;
+begin
+    sz:= surf^.w * surf^.h;
+    p:= surf^.pixels;
+    //randr:=Random;
+    //randg:=Random;
+    //randb:=1 - min(randr, randg);
+    randh:=Random;
+    HSVToRGB(randh, 1.0, 1.0, randr, randg, randb);
+    for i:=0 to pred(sz) do
+    begin
+        a:= p[3];
+        inva:= 255 - a;
+
+        r:=Trunc(inva*randr + p[0]*a/255);
+        g:=Trunc(inva*randg + p[1]*a/255);
+        b:=Trunc(inva*randb + p[2]*a/255);
+        if r > 255 then r:= 255;
+        if g > 255 then g:= 255;
+        if b > 255 then b:= 255;
+
+        p[0]:=r;
+        p[1]:=g;
+        p[2]:=b;
+        p[3]:=255;
+        inc(p, 4);
+    end;
+end;
+
+procedure Upload(var info: AtlasInfo; sprite: Rectangle; surf: PSDL_Surface);
+var
+    sp: PTexture;
+    i, j, stride: Integer;
+    scanline: PByte;
+begin
+    writeln('Uploading sprite to ', sprite.x, ',', sprite.y, ',', sprite.width, ',', sprite.height);
+    sp:= PTexture(sprite.UserData);
+    sp^.x:= sprite.x;
+    sp^.y:= sprite.y;
+    sp^.isRotated:= sp^.w <> sprite.width;
+    sp^.atlas:= @info.TextureInfo;
+
+    if SDL_MustLock(surf) then
+        SDLTry(SDL_LockSurface(surf) >= 0, true);
+
+    //if GrayScale then
+    //    Surface2GrayScale(surf);
+    DebugColorize(surf);
+
+    glBindTexture(GL_TEXTURE_2D, info.TextureInfo.id);
+    if (sp^.isRotated) then
+    begin
+        scanline:= surf^.pixels;
+        for i:= 0 to pred(sprite.width) do
+        begin
+            glTexSubImage2D(GL_TEXTURE_2D, 0, sprite.x + i, sprite.y, 1, sprite.height, GL_RGBA, GL_UNSIGNED_BYTE, scanline);
+            inc(scanline, sprite.height * 4);
+        end;
+    end
+    else
+        glTexSubImage2D(GL_TEXTURE_2D, 0, sprite.x, sprite.y, sprite.width, sprite.height, GL_RGBA, GL_UNSIGNED_BYTE, surf^.pixels);
+    glBindTexture(GL_TEXTURE_2D, 0);
+
+    if SDL_MustLock(surf) then
+        SDL_UnlockSurface(surf);
+end;
+
+{$DEFINE HAS_PBO}
+procedure Repack(var info: AtlasInfo; newAtlas: Atlas; newSprite: PTexture; surf: PSDL_Surface);
+var
+{$IFDEF HAS_PBO}
+    pbo: GLuint;
+{$ENDIF}
+    base: PByte;
+    oldSize: Integer;
+    oldWidth: Integer;
+    offset: Integer;
+    i,j : Integer;
+    r: Rectangle;
+    sp: PTexture;
+    newIsRotated: boolean;
+    newSpriteRect: Rectangle;
+begin
+    writeln('Repacking atlas (', info.PackerInfo.width, 'x', info.PackerInfo.height, ')', ' -> (', newAtlas.width, 'x', newAtlas.height, ')');
+
+{$IFDEF RETAIN_SURFACES}
+    // we can simply re-upload from RAM
+
+    // delete the old atlas
+    glDeleteTextures(1, @info.TextureInfo.id);
+
+    // create a new atlas with different size
+    info.TextureInfo:= createTexture(newAtlas.width, newAtlas.height);
+    glBindTexture(GL_TEXTURE_2D, info.TextureInfo.id);
+
+    atlasDelete(info.PackerInfo);
+    info.PackerInfo:= newAtlas;
+
+    // and process all sprites of the new atlas
+    for i:=0 to pred(newAtlas.usedRectangles.count) do
+    begin
+        r:= newAtlas.usedRectangles.data[i];
+        sp:= PTexture(r.UserData);
+        Upload(info, r, sp^.surface);
+    end;
+
+{$ELSE}
+    // as we dont have access to the original sprites in ram anymore,
+    // we need to copy from the existing atlas to an PBO, delete the original texture
+    // and finally copy from the PBO back to the new texture object
+
+    // allocate a PBO and copy from old atlas to it
+    oldSize:= info.TextureInfo.w * info.TextureInfo.h * 4;
+    oldWidth:= info.TextureInfo.w;
+
+    glBindTexture(GL_TEXTURE_2D, info.TextureInfo.id);
+
+{$IFDEF HAS_PBO}
+    base:= nil;
+    glGenBuffers(1, @pbo);
+    glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo);
+    glBufferData(GL_PIXEL_PACK_BUFFER, oldSize, nil, GL_COPY);
+    //glGetTexImage( GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, nil);
+    
+    glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
+    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
+{$ELSE}
+    GetMem(base, oldSize);
+    glGetTexImage( GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, base);
+{$ENDIF}
+
+    // delete the old atlas
+    glDeleteTextures(1, @info.TextureInfo.id);
+
+    // create a new atlas with different size
+    info.TextureInfo:= createTexture(newAtlas.width, newAtlas.height);
+    glBindTexture(GL_TEXTURE_2D, info.TextureInfo.id);
+    
+    
+    // and process all sprites of the new atlas
+    for i:=0 to pred(newAtlas.usedRectangles.count) do
+    begin
+        r:= newAtlas.usedRectangles.data[i];
+        sp:= PTexture(r.UserData);
+        if sp = newSprite then // this is the to be added sprite
+        begin
+            // we need to do defer the upload till after this loop, 
+            // as we currently upload from the PBO to texture
+            newSpriteRect:= r;
+            continue;
+        end;
+
+        newIsRotated:= sp^.w <> r.width;
+        if newIsRotated <> sp^.isRotated then
+        begin
+            glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
+            glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
+            glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);
+            offset:= sp^.x + sp^.y * oldWidth;
+            for j:= 0 to pred(r.width) do
+            begin
+                glTexSubImage2D(GL_TEXTURE_2D, 0, r.x + j, r.y, 1, r.height, GL_RGBA, GL_UNSIGNED_BYTE, base + offset * 4);
+                inc(offset, oldWidth);
+            end;
+        end 
+        else
+        begin
+            glPixelStorei(GL_UNPACK_ROW_LENGTH, oldWidth);
+            glPixelStorei(GL_UNPACK_SKIP_PIXELS, sp^.x);
+            glPixelStorei(GL_UNPACK_SKIP_ROWS, sp^.y);
+            glTexSubImage2D(GL_TEXTURE_2D, 0, r.x, r.y, r.width, r.height, GL_RGBA, GL_UNSIGNED_BYTE, base);
+        end;
+
+        sp^.x:= r.x;
+        sp^.y:= r.y;
+        sp^.isRotated:= newIsRotated;
+        sp^.atlas:= @info.TextureInfo;
+    end;
+    glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
+    glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
+    glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);
+
+    atlasDelete(info.PackerInfo);
+    info.PackerInfo:= newAtlas;
+
+{$IFDEF HAS_PBO}
+    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
+    glDeleteBuffers(1, @pbo);
+{$ELSE}
+    FreeMem(base, oldSize);
+{$ENDIF}
+
+    // finally upload the new sprite (if any)
+    if newSprite <> nil then
+        Upload(info, newSpriteRect, surf);
+
+    glBindTexture(GL_TEXTURE_2D, 0);
+{$ENDIF}
+end;
+
+
+////////////////////////////////////////////////////////////////////////////////
+// Utility functions
+
+function SizeForSprite(sprite: PTexture): Size;
+begin
+    SizeForSprite.width:= sprite^.w;
+    SizeForSprite.height:= sprite^.h;
+    SizeForSprite.UserData:= sprite;
+end;
+
+procedure EnlargeSize(var x: Integer; var y: Integer);
+begin
+    if (y < x) then
+        y:= y + y
+    else
+        x:= x + x;
+end;
+
+procedure CompactSize(var x: Integer; var y: Integer);
+begin
+    if (x > y) then
+        x:= x div 2
+    else
+        y:= y div 2;
+end;
+
+////////////////////////////////////////////////////////////////////////////////
+// Sprite allocation logic
+
+function TryRepack(var info: AtlasInfo; w, h: Integer; hasNewSprite: boolean; 
+                   newSprite: Size; surf: PSDL_Surface): boolean;
+var
+    sizes: SizeList;
+    repackedAtlas: Atlas;
+    sprite: PTexture;
+    i: Integer;
+    rects: RectangleList; // we wont really need this as we do a full repack using the atlas later on
+begin
+    TryRepack:= false;
+
+    // STEP 1: collect sizes of all existing sprites
+    sizeListInit(sizes);
+    for i:= 0 to pred(info.PackerInfo.usedRectangles.count) do
+    begin
+        sprite:= PTexture(info.PackerInfo.usedRectangles.data[i].UserData);
+        sizeListAdd(sizes, SizeForSprite(sprite));
+    end;
+
+    // STEP 2: add the new sprite to the list
+    if hasNewSprite then
+        sizeListAdd(sizes, newSprite);
+
+    // STEP 3: try to create a non adaptive re-packing using the whole list
+    repackedAtlas:= atlasNew(w, h);
+    rectangleListInit(rects);
+    if atlasInsertSet(repackedAtlas, sizes, rects) then
+    begin
+        TryRepack:= true;
+        if hasNewSprite then
+            sprite:= PTexture(newSprite.UserData)
+        else
+            sprite:= nil;
+        Repack(info, repackedAtlas, sprite, surf);
+        // repack assigns repackedAtlas to the current info and deletes the old one
+        // thus we wont do atlasDelete(repackedAtlas); here 
+        rectangleListClear(rects);
+        sizeListClear(sizes);
+        DumpAtlas(info);
+        exit;
+    end;
+
+    rectangleListClear(rects);
+    sizeListClear(sizes);
+    atlasDelete(repackedAtlas);
+end;
+
+function TryInsert(var info: AtlasInfo; newSprite: Size; surf: PSDL_Surface): boolean;
+var
+    rect: Rectangle;
+    sprite: PTexture;
+begin
+    TryInsert:= false;
+
+    if atlasInsertAdaptive(info.PackerInfo, newSprite, rect) then
+    begin
+        // we succeeded adaptivley allocating the sprite to the i'th atlas.
+        Upload(info, rect, surf);
+        DumpAtlas(info);
+        TryInsert:= true;
+    end;
+end;
+
+function Surface2Tex_(surf: PSDL_Surface; enableClamp: boolean): PTexture;
+var
+    sz: Size;
+    sprite: PTexture;
+    currentWidth, currentHeight: Integer;
+    i: Integer;
+begin
+    if (surf^.w > MaxTexSize) or (surf^.h > MaxTexSize) then
+    begin
+        // we could at best downscale the sprite, abort for now
+        writeln('Sprite size larger than maximum texture size');
+        halt(-1);        
+    end;
+
+    // allocate the sprite
+    new(sprite);
+    Surface2Tex_:= sprite;
+
+    sprite^.w:= surf^.w;
+    sprite^.h:= surf^.h;
+    sprite^.x:= 0;
+    sprite^.y:= 0;
+    sprite^.isRotated:= false;
+    sprite^.surface:= surf;
+
+    sz:= SizeForSprite(sprite);
+
+    // STEP 1
+    // try to allocate the new sprite in one of the existing atlases
+    for i:= 0 to pred(MaxAtlases) do
+    begin
+        if not Info[i].Allocated then
+            continue;
+        if TryInsert(Info[i], sz, surf) then
+            exit; 
+    end;
+
+
+    // STEP 2
+    // none of the atlases has space left for the allocation, try a garbage collection
+    for i:= 0 to pred(MaxAtlases) do
+    begin
+        if not Info[i].Allocated then
+            continue;
+
+        if TryRepack(Info[i], Info[i].PackerInfo.width, Info[i].PackerInfo.height, true, sz, surf) then
+            exit;
+    end;
+
+    // STEP 3
+    // none of the atlases could be repacked in a way to fit the new sprite, try enlarging
+    for i:= 0 to pred(MaxAtlases) do
+    begin
+        if not Info[i].Allocated then
+            continue;
+
+        currentWidth:= Info[i].PackerInfo.width;
+        currentHeight:= Info[i].PackerInfo.height;
+
+        EnlargeSize(currentWidth, currentHeight);
+        while (currentWidth <= MaxTexSize) and (currentHeight <= MaxTexSize) do
+        begin
+            if TryRepack(Info[i], currentWidth, currentHeight, true, sz, surf) then
+                exit;
+            EnlargeSize(currentWidth, currentHeight);
+        end;
+    end;
+
+    // STEP 4
+    // none of the existing atlases could be resized, try to allocate a new atlas
+    for i:= 0 to pred(MaxAtlases) do
+    begin
+        if Info[i].Allocated then
+            continue;
+
+        currentWidth:= MinTexSize;
+        currentHeight:= MinTexSize;
+        while (sz.width > currentWidth) do
+            currentWidth:= currentWidth + currentWidth;
+        while (sz.height > currentHeight) do
+            currentHeight:= currentHeight + currentHeight;
+
+        with Info[i] do
+        begin
+            PackerInfo:= atlasNew(currentWidth, currentHeight);
+            TextureInfo:= createTexture(currentWidth, currentHeight);
+            Allocated:= true;
+        end;
+
+        if TryInsert(Info[i], sz, surf) then
+            exit;
+
+        // this shouldnt have happened, the rectpacker should be able to fit the sprite
+        // into an unused rectangle that is the same size or larger than the requested sprite.
+        writeln('Internal error: atlas allocation failed');
+        halt(-1);
+    end;
+
+    // we reached the upperbound of resources we are willing to allocate
+    writeln('Exhausted maximum sprite allocation size');
+    halt(-1);
+end;
+
+////////////////////////////////////////////////////////////////////////////////
+// Sprite deallocation logic
+
+
+procedure FreeTexture_(sprite: PTexture);
+var
+    i, j, deleteAt: Integer;
+    usedArea: Integer;
+    totalArea: Integer;
+    r: Rectangle;
+    atlasW, atlasH: Integer;
+    unused: Size;
+begin
+    if sprite = nil then
+        exit;
+
+    for i:= 0 to pred(MaxAtlases) do
+    begin
+        if sprite^.atlas <> @Info[i].TextureInfo then
+            continue;
+
+        usedArea:= 0;
+        for j:=0 to pred(Info[i].PackerInfo.usedRectangles.count) do
+        begin
+            r:= Info[i].PackerInfo.usedRectangles.data[j];
+            if r.UserData = sprite then
+                deleteAt:= j
+            else
+                inc(usedArea, r.width * r.height);
+        end;
+
+        rectangleListRemoveAt(Info[i].PackerInfo.usedRectangles, j);
+        dispose(sprite);
+
+        while true do
+        begin
+            atlasW:= Info[i].PackerInfo.width;
+            atlasH:= Info[i].PackerInfo.height;
+            totalArea:=  atlasW * atlasH;
+            if usedArea >= totalArea * CompressionThreshold then
+                exit;
+
+            if (atlasW = MinTexSize) and (atlasH = MinTexSize) then
+                exit; // we could try to move everything from this to another atlas here
+
+            CompactSize(atlasW, atlasH);
+            unused:= unused;
+            TryRepack(Info[i], atlasW, atlasH, false, unused, nil);
+        end;
+    end;
+end;
+
+procedure initModule;
+var
+    i: Integer;
+begin
+    DumpID:=0;
+    for i:= 0 to pred(MaxAtlases) do
+        Info[i].Allocated:= false;
+end;
+
+end.