DXT Texture compression and texture arrays [SOLVED]

You are an experienced programmer and have a problem with the engine, shaders, or advanced effects? Here you'll get answers.
No questions about C++ programming or topics which are answered in the tutorials!
Post Reply
Sinsemilla
Posts: 38
Joined: Mon Jan 09, 2012 5:07 pm

DXT Texture compression and texture arrays [SOLVED]

Post by Sinsemilla »

Heyas together.

I am trying to use compressed dds textures together with hendu's texture array patch in a glsl shader. I am aware that there is no support implemented for this, so i tried myself, but unfortunately i am failing to get this working.

First i backported the patent-free dds-loader from the trunk (Rev. 4448 - 4450) into stock irrlicht 1.8.2 because i wanted to avoid the patent problem of decompressing and gain the speed advantage without using an perhaps non stable Irrlicht version and because i don't want to change the shader handling in my project right now (yes i know this decision is questionable :D). The backport is working quite nicely, the single loaded textures are visible and i only needed to do a very minor change compared to nadro's commits into the trunk.

So, to get the compressed texture array i made a change in COpenGLTextureArray::uploadTexture by adding an if which is checking the IsCompressed variable and then calling glCompressedTexImage3D and glCompressedTexSubImage3D:

Code: Select all

 
void COpenGLTextureArray::uploadTexture(const core::array<ITexture*> &surfaces)
{
    const u32 nfiles = surfaces.size();
    const u32 w = surfaces[0]->getSize().Width;
    const u32 h = surfaces[0]->getSize().Height;
    u32 i;
 
    const COpenGLTexture * const tex = (COpenGLTexture *) surfaces[0];
    ECOLOR_FORMAT color = tex->getColorFormat();
    GLint filter;
    InternalFormat = getOpenGLFormatAndParametersFromColorFormat(
                color, filter, PixelFormat, PixelType);
 
    Driver->setActiveTexture(0, this);
    if (Driver->testGLError())
        os::Printer::log("Could not bind Texture", ELL_ERROR);
 
        char *tmp = new char[w*h*nfiles];
    
        if (IsCompressed) {
            glCompressedTexImage3D(GL_TEXTURE_2D_ARRAY, 0,  InternalFormat, w, h, nfiles, 0, w*h*nfiles, tmp);
        } else {
            glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, InternalFormat, w, h, nfiles, 0,
                PixelFormat, PixelType, NULL);
        }
    
    for (i = 0; i < nfiles; i++)
    {
        void *src = surfaces[i]->lock();
 
        Driver->setActiveTexture(0, this);
        if (Driver->testGLError())
            os::Printer::log("Could not bind Texture", ELL_ERROR);
 
               if (IsCompressed) {
                   glCompressedTexSubImage3D( GL_TEXTURE_2D_ARRAY, 0, 0, 0, i, w, h, 1, 
                   PixelFormat, PixelType, src);
               } else {
                   glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, i, w, h, 1,
                PixelFormat, PixelType, src);
              }
 
        if (Driver->testGLError())
            os::Printer::log("Could not glTexSubImage3D", ELL_ERROR);
 
        surfaces[i]->unlock();
    }
 
    Driver->extGlGenerateMipmap(GL_TEXTURE_2D_ARRAY);
}
 
Unfortunately everything is drawn black where there should be textures, so probably i missed a major thing, but i have no idea what to do further. I admit that i am an opengl noob, so i am asking kindly for help concerning opengl in irrlicht and also to not get a heart attack if did somthing terribly wrong :wink:. If i get this working thanks to one of you here i am of course willed to give back the full backport code (if somebody finds this useful) and prepare a patch for the dds texture arrays support. Btw. the tetxure array patch is very uselful, so thanks a lot for this one hendu :D.
Last edited by Sinsemilla on Sun Jan 31, 2016 5:06 pm, edited 3 times in total.
Sinsemilla
Posts: 38
Joined: Mon Jan 09, 2012 5:07 pm

Re: DXT Texture compression and texture arrays

Post by Sinsemilla »

Hmm, i think i see where the problem could be related to. I am passing a tmp array, which probably gets the image data, but nothing is done afterwards with it... I tried to pass NULL instead, but to no avail.

Code: Select all

if (IsCompressed) {
            glCompressedTexImage3D(GL_TEXTURE_2D_ARRAY, 0,  InternalFormat, w, h, nfiles, 0, w*h*nfiles, NULL);
        } else {
            glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, InternalFormat, w, h, nfiles, 0,
                PixelFormat, PixelType, NULL);
        }
hendu
Posts: 2600
Joined: Sat Dec 18, 2010 12:53 pm

Re: DXT Texture compression and texture arrays

Post by hendu »

You should pass 0, NULL as the last two params of glCompressedTexImage3D - you are not uploading anything in that call, only setting up the format, like in the uncompressed case. The uploading happens per-image with the sub call.
Vectrotek
Competition winner
Posts: 1087
Joined: Sat May 02, 2015 5:05 pm

Re: DXT Texture compression and texture arrays

Post by Vectrotek »

I suspect the image compression has to do with things like 256 color mode images. NVIDIA under CG has lots on that. I've discovered that information is usually available on a matter even if it is not specifically GLSL, CG, CGFX, FX or HLSL. What Im trying to say is that if you want information on things when you're using, for example, GLSL, then you'd probably find helpful stuff under other text that deals with, say CG. The underlying workings of GPU shaders no matter what the flavor, is the same, I hope you find what you're looking for. By the way, I found that a "black" render may have something to do with a more fundamental problem like the Video Driver on your machine. Cheers!
Sinsemilla
Posts: 38
Joined: Mon Jan 09, 2012 5:07 pm

Re: DXT Texture compression and texture arrays

Post by Sinsemilla »

Thanks for the answers.
@Vectrotek: I will keep this in mind when i am searching for information. Regarding my black textures issue i am not persuaded that it's a fundamental driver problem. As far as i know it is possible that this sort of problems are related to texture binding which is done in the lock function as i believe.

@hendu: I tried your suggestion, (which helped to understand this a little bit more) but i still get everything in black. I had a closer look at the rest in COpenGLTexture.cpp though. I think the problem is related to locking/unlocking, since i can see that nothing is done in case of compressed textures and that that lock/unlock is called in uploadTexture before and after glCompressedTexSubImage3D is called:

Code: Select all

 
void* COpenGLTexture::lock(E_TEXTURE_LOCK_MODE mode, u32 mipmapLevel)
{
    if (IsCompressed) // TO-DO
        return 0;
 
...
 
void COpenGLTexture::unlock()
{
    if (IsCompressed) // TO-DO
        return;
 
As the comments in the code suggests there are TODO's in lock/unlock in the COpenGLTexture class, but that's a point where my knowledge stops at the moment. Also in CImage the compressed textures seem to be spared out in the copy functions, setPixel/getPixel and fill...

According to what i have written above i have some new questions: What would need to be done in COpenGLTexture::lock and COpenGLTexture::unlock? And would something need to be changed in CImage for compressed textures as well? I don't see any TODO's there...
hendu
Posts: 2600
Joined: Sat Dec 18, 2010 12:53 pm

Re: DXT Texture compression and texture arrays

Post by hendu »

You want to upload the raw data, so in case of a compressed texture, you need to return Image->lock(). That's what the 2d compressed upload path uses.
Sinsemilla
Posts: 38
Joined: Mon Jan 09, 2012 5:07 pm

Re: DXT Texture compression and texture arrays

Post by Sinsemilla »

Thx again hendu. I tried to do this the following way, but now it's crashing because the image pointer is NULL:

Code: Select all

void* COpenGLTexture::lock(E_TEXTURE_LOCK_MODE mode, u32 mipmapLevel)
{
        IImage* image = (mipmapLevel==0)?Image:MipImage;
    if (IsCompressed)
        return image->lock();
    ....
    
 
void COpenGLTexture::unlock()
{
    IImage* image = MipImage?MipImage:Image;
    if (IsCompressed) // TO-DO
                image->unlock();
        return;
 
Sinsemilla
Posts: 38
Joined: Mon Jan 09, 2012 5:07 pm

Re: DXT Texture compression and texture arrays

Post by Sinsemilla »

Ok, i think i am one little step further in understanding things now but it is still not working at the moment... I came up with the following changes after reading about glCompressedTexImage3D and glCompressedTexSubImage3D. I just put the functions lock and unlock back to their original state, e.g. removing the if (IsCompressed) clause. In COpenGLTextureArray::uploadTexture i call lock with the ETLM_WRITE_ONLY parameter, since it shall only upload the data to the GPU.

Code: Select all

 
    for (i = 0; i < nfiles; i++)
    {
        void *src = surfaces[i]->lock(ETLM_WRITE_ONLY);
 
When debugging i am now receiving a GL error message from the second testGLError call in the loop, stating GL_INVALID_OPERATION. Btw, the first testGLError call in the loop was also returning an error: GL_INVALID_VALUE, but i could get rid of that one by calling glCompressedTexImage3D with w * h * nfiles as the second last parameter, which according to the documentaion on opengl.org is the byte size of the array.

I also changed the second last parameter in glCompressedTexSubImage3D from PixelType to w*h, since the documentation on opengl.org says it's the number of bytes of the passed image data.
The IsCompressed stuff is commented out as it was not working there, and i am using only compressed textures anyway....

Code: Select all

 
    //if (IsCompressed) {
        glCompressedTexImage3D(GL_TEXTURE_2D_ARRAY, 0, InternalFormat, w, h, nfiles, 0, w * h * nfiles, NULL);
    //} else {
       // glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, InternalFormat, w, h, nfiles, 0,
                //PixelFormat, PixelType, NULL);
    //}
    
    for (i = 0; i < nfiles; i++)
    {
        void *src = surfaces[i]->lock(ETLM_WRITE_ONLY);
        
        // seems ok now with w * h * nfiles in glCompressedTexImage3D
        Driver->setActiveTexture(0, this);
        if (Driver->testGLError())
            os::Printer::log("Could not bind Texture", ELL_ERROR);
 
        //if (IsCompressed) {
            glCompressedTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, i, w, h, 1, 
                PixelFormat, w*h, src);
        //} else {
           // glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, i, w, h, 1,
                //PixelFormat, PixelType, src);
       // }
        // error raised here: GL_INVALID_OPERATION
        if (Driver->testGLError())
            os::Printer::log("Could not glTexSubImage3D", ELL_ERROR);
 
        surfaces[i]->unlock();
    }
 
I wonder now why i do get the GL_INVALID_OPERATION error. According to opengl.org it should be the parameters i pass to glTexSubImage3D, or the GL_PIXEL_UNPACK_BUFFER stuff...
The documentation i found is here: https://www.opengl.org/wiki/GLAPI/glCom ... SubImage3D
Sinsemilla
Posts: 38
Joined: Mon Jan 09, 2012 5:07 pm

Re: DXT Texture compression and texture arrays

Post by Sinsemilla »

I finally made it work :D

I'll explain the changes here, and make a patch for it later.

COpenGLTexture::uploadTexture

Code: Select all

 
        if (newTexture)
        {
        ...
        }
        else
        {
            if (IsCompressed)
            {
                if(ColorFormat == ECF_DXT1)
                    compressedDataSize = ((image->getDimension().Width + 3) / 4) * ((image->getDimension().Height + 3) / 4) * 8;
                else if (ColorFormat == ECF_DXT2 || ColorFormat == ECF_DXT3 || ColorFormat == ECF_DXT4 || ColorFormat == ECF_DXT5)
                    compressedDataSize = ((image->getDimension().Width + 3) / 4) * ((image->getDimension().Height + 3) / 4) * 16;
 
                Driver->extGlCompressedTexSubImage2D(GL_TEXTURE_2D, level, 0, 0, image->getDimension().Width,
                    image->getDimension().Height, InternalFormat, compressedDataSize, source);
            }
        }
 
I changed the parameter PixelFormat in the extGlCompressedTexSubImage2D call to InternalFormat. For DXT compressed textures, the format must be either GL_COMPRESSED_RGB_S3TC_DXT1_EXT, GL_COMPRESSED_RGBA_S3TC_DXT3_EXT or GL_COMPRESSED_RGBA_S3TC_DXT5_EXT.

COpenGLTexture::lock

Code: Select all

 
void* COpenGLTexture::lock(E_TEXTURE_LOCK_MODE mode, u32 mipmapLevel)
{
    // store info about which image is locked
    IImage* image = (mipmapLevel==0)?Image:MipImage;
    ReadOnlyLock |= (mode==ETLM_READ_ONLY);
    MipLevelStored = mipmapLevel;
    if (!ReadOnlyLock && mipmapLevel)
    {
#ifdef GL_SGIS_generate_mipmap
        if (Driver->queryFeature(EVDF_MIP_MAP_AUTO_UPDATE))
        {
            // do not automatically generate and update mipmaps
            glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_FALSE);
        }
#endif
        AutomaticMipmapUpdate=false;
    }
 
    // if data not available or might have changed on GPU download it
    if (!image || IsRenderTarget)
    {
        // prepare the data storage if necessary
        if (!image)
        {
            if (mipmapLevel)
            {
                u32 i=0;
                u32 width = TextureSize.Width;
                u32 height = TextureSize.Height;
                do
                {
                    if (width>1)
                        width>>=1;
                    if (height>1)
                        height>>=1;
                    ++i;
                }
                while (i != mipmapLevel);
                MipImage = image = Driver->createImage(ColorFormat, core::dimension2du(width,height));
            } else {
                Image = image = Driver->createImage(ColorFormat, ImageSize);
            }
        }
        if (!image)
            return 0;
 
        if (mode != ETLM_WRITE_ONLY)
        {
            u8* pixels = static_cast<u8*>(image->lock());
            if (!pixels)
                return 0;
 
            // we need to keep the correct texture bound later on
            GLint tmpTexture;
            glGetIntegerv(GL_TEXTURE_BINDING_2D, &tmpTexture);
            glBindTexture(GL_TEXTURE_2D, TextureName);
            
            if (Driver->testGLError())
                os::Printer::log("Could not bind TextureName", ELL_ERROR);
 
            // we need to flip textures vertical
            // however, it seems that this does not hold for mipmap
            // textures, for unknown reasons.
 
            // allows to read pixels in top-to-bottom order
#ifdef GL_MESA_pack_invert
            if (!mipmapLevel && Driver->queryOpenGLFeature(COpenGLExtensionHandler::IRR_MESA_pack_invert))
                glPixelStorei(GL_PACK_INVERT_MESA, GL_TRUE);
#endif
 
            // download GPU data as ARGB8 to pixels;
            if (IsCompressed) {
                glGetCompressedTexImage(GL_TEXTURE_2D, mipmapLevel, pixels);
                if (Driver->testGLError())
                    os::Printer::log("Could not download compressed GPU data", ELL_ERROR);
            } else {
                glGetTexImage(GL_TEXTURE_2D, mipmapLevel, GL_BGRA_EXT, GL_UNSIGNED_BYTE, pixels);
                if (Driver->testGLError())
                    os::Printer::log("Could not download GPU data", ELL_ERROR);
            }
 
            if (!mipmapLevel)
            {
#ifdef GL_MESA_pack_invert
                if (Driver->queryOpenGLFeature(COpenGLExtensionHandler::IRR_MESA_pack_invert))
                    glPixelStorei(GL_PACK_INVERT_MESA, GL_FALSE);
                else
#endif
                {
                    if (!IsCompressed) {
                        // opengl images are horizontally flipped, so we have to fix that here.
                        const s32 pitch=image->getPitch();
                        u8* p2 = pixels + (image->getDimension().Height - 1) * pitch;
                        u8* tmpBuffer = new u8[pitch];
                        for (u32 i=0; i < image->getDimension().Height; i += 2)
                        {
                            memcpy(tmpBuffer, pixels, pitch);
                            memcpy(pixels, p2, pitch);
                            memcpy(p2, tmpBuffer, pitch);
                            pixels += pitch;
                            p2 -= pitch;
                        }
                        delete [] tmpBuffer;
                    }
                }
            }
            image->unlock();
 
            //reset old bound texture
            glBindTexture(GL_TEXTURE_2D, tmpTexture);
        }
    }
    return image->lock();
}
 
In order to be able to use all compressed formats, the createImage call now takes the ColorFormat dynamically. I am not 100% sure if this is correct, i simply made the assumption that the ColorFormat Variable is always already set with the correct value. The part of the donwnload from the GPU is handled by glGetCompressedTexImage in case of a compressed texture. The fix for the flipped texture must not be done for compressed textures, i assume because they probably are inverted again.

CImage::initData()

Code: Select all

 
void CImage::initData()
{
#ifdef _DEBUG
    setDebugName("CImage");
#endif
    
    u32 dataSize = 0;
    
    if (Format == ECF_DXT1 || Format == ECF_DXT2 || Format == ECF_DXT3 || Format == ECF_DXT4 || Format == ECF_DXT5) {
        u32 compressedDataSize = 0;
        
        if (Format == ECF_DXT1) {
            compressedDataSize = ((Size.Width + 3) / 4) * ((Size.Height + 3) / 4) * 8;
        } else {
            compressedDataSize = ((Size.Width + 3) / 4) * ((Size.Height + 3) / 4) * 16;
        }
        dataSize = compressedDataSize;
    } else {
        BytesPerPixel = getBitsPerPixelFromFormat(Format) / 8;
 
        // Pitch should be aligned...
        Pitch = BytesPerPixel * Size.Width;
        dataSize = Size.Height * Pitch;
    }
    if (!Data)
    {
        DeleteMemory=true;
        Data = new u8[dataSize];
    }
}
 
I took the calculation of the data size of the Image directly from the uploadTexture method. The DXT sizes which were returned from getBitsPerPixelFromFormat were wrong in my opinion. For ECF_DXT2 to ECF_DXT5 it should be 8 bits per Pixel. For ECF_DXT1 it's 4 bits per Pixel. The code as it was would break DXT1 and cause a crash because it would result in 0.5 bytes per pixel which is not handled well by a u32 and then lead to an alocation of 0 bytes. The image size has to match the size of the texture which has been uploaded as raw data to the GPU.

COpenGLTexture::unlock

Code: Select all

 
//! unlock function
void COpenGLTexture::unlock()
{
        // test if miplevel or main texture was locked
    IImage* image = MipImage?MipImage:Image;
    if (!image)
        return;
    // unlock image to see changes
    image->unlock();
    // copy texture data to GPU
    if (!ReadOnlyLock)
        uploadTexture(false, 0, MipLevelStored);
    ReadOnlyLock = false;
    // cleanup local image
    if (MipImage)
    {
        MipImage->drop();
        MipImage=0;
    }
    else if (!KeepImage)
    {
        Image->drop();
        Image=0;
    }
    // update information
    if (Image)
        ColorFormat=Image->getColorFormat();
    else
        ColorFormat=ECF_A8R8G8B8;
}
 
There was not much to do here, i just removed the IsCompressed "barrier", so that the raw texture is uploaded into the texture array on the GPU.

COpenGLTextureArray::uploadTexture

Code: Select all

 
void COpenGLTextureArray::uploadTexture(const core::array<ITexture*> &surfaces)
{
    const u32 nfiles = surfaces.size();
    const u32 w = surfaces[0]->getSize().Width;
    const u32 h = surfaces[0]->getSize().Height;
    u32 i;
 
    const COpenGLTexture * const tex = (COpenGLTexture *) surfaces[0];
    ECOLOR_FORMAT color = tex->getColorFormat();
    GLint filter;
    InternalFormat = getOpenGLFormatAndParametersFromColorFormat(
                color, filter, PixelFormat, PixelType);
 
    if (color == ECF_DXT1 || color == ECF_DXT2 || color == ECF_DXT3 || color == ECF_DXT4 || color == ECF_DXT5) {
        ColorFormat = color;
        IsCompressed = true;
    }
    Driver->setActiveTexture(0, this);
    if (Driver->testGLError())
        os::Printer::log("Could not bind Texture", ELL_ERROR);
 
    GLint compressedSize = 0;
    
    // Define it
    if (IsCompressed) {
        glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &compressedSize);
        glCompressedTexImage3D(GL_TEXTURE_2D_ARRAY, 0, InternalFormat, w, h, nfiles, 0, compressedSize * nfiles, NULL);
    } else {
        glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, InternalFormat, w, h, nfiles, 0, PixelFormat, PixelType, NULL);
    }
    if (Driver->testGLError())
        os::Printer::log("Could not define Texture array", ELL_ERROR);
    
    for (i = 0; i < nfiles; i++)
    {
        void *src = surfaces[i]->lock();
 
        Driver->setActiveTexture(0, this);
        if (Driver->testGLError())
            os::Printer::log("Could not bind Texture", ELL_ERROR);
        
        if (IsCompressed) {
            glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &compressedSize);
            glCompressedTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, i, w, h, 1, InternalFormat, compressedSize, src);
        } else {
            glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, i, w, h, 1, PixelFormat, PixelType, src);
        }
 
        if (Driver->testGLError())
            os::Printer::log("Could not glTexSubImage3D", ELL_ERROR);
 
        surfaces[i]->unlock();
    }
 
    Driver->extGlGenerateMipmap(GL_TEXTURE_2D_ARRAY);
}
 
First the format is checked in order to know if we have compressed textures. Then, in the compressed case, the size of the last raw image uploaded to the GPU is retrieved. This was the simplest way, because it is difficult to calculate the byte sizes which do not match the pixel amount of the data. Then the array is defined by using glCompressedTexImage3D.

For the array elements, the byte size is also retrieved from the uploaded GPU data. Also note that the glCompressedTexSubImage3D call uses InternalFormat and not PixelFormat, since again for compressed textures the formats GL_COMPRESSED_RGBA_S3TC* (GL_COMPRESSED_RGB_S3TC* in case of DXT1) must be used.

isRenderTargetOnlyFormat (IImage.h)

Code: Select all

 
static bool isRenderTargetOnlyFormat(const ECOLOR_FORMAT format)
{
    switch(format)
    {
        case ECF_A1R5G5B5:
        case ECF_R5G6B5:
        case ECF_R8G8B8:
        case ECF_A8R8G8B8:
        case ECF_DXT1:
        case ECF_DXT2:
        case ECF_DXT3:
        case ECF_DXT4:
        case ECF_DXT5:
            return false;
        default:
             return true;
    }
}
 
To be able to pass all the compressed color fomats dynamically to the CImage contructor, i added ECF_DXT1 - ECF_DXT5 to isRenderTargetOnlyFormat.
Post Reply