加载多个 8 位表面后,SDL2 纹理有时为空

SDL2 Texture sometimes empty after loading multiple 8 bit surfaces

我会尽量让这个问题简洁明了,但请不要犹豫,要求澄清。

我正在处理遗留代码,我正在尝试从磁盘加载数千个 8 位图像,为每个图像创建一个纹理。

我已经尝试了多种方法,现在我正在尝试将我的 8 位图像加载到 32 位表面,然后从该表面创建纹理。

问题:当我尝试将 8 位图像加载到 32 位表面时正常工作,SDL_CreateTextureFromSurface,我最终得到很多完全空白的纹理(充满透明像素, 0x00000000).

并不是所有的纹理都是错误的,我想。每次我 运行 程序,我都会得到不同的 "bad" 纹理。有时多,有时少。当我跟踪程序时,我总是以正确的纹理结束(这是时间问题吗?)

我知道加载到 SDL_Surface 正在运行,因为我将所有表面保存到磁盘,它们都是正确的。但是我使用NVidia NSight Graphics检查了纹理,其中一半以上是空白的。

这是有问题的代码:

int __cdecl IMG_SavePNG(SDL_Surface*, const char*);

SDL_Texture* Resource8bitToTexture32(SDL_Renderer* renderer, SDL_Color* palette, int paletteSize, void* dataAddress, int Width, int Height)
{
  u32 uiCurrentOffset;
  u32 uiSourceLinearSize = (Width * Height);
  SDL_Color *currentColor;
  char strSurfacePath[500];

  // The texture we're creating
  SDL_Texture* newTexture = NULL;

  // Load image at specified address
  SDL_Surface* tempSurface = SDL_CreateRGBSurface(0x00, Width, Height, 32, 0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000);

  SDL_SetSurfaceBlendMode(tempSurface, SDL_BLENDMODE_NONE);

  if(SDL_MUSTLOCK(tempSurface)
    SDL_LockSurface(tempSurface);

  for(uiCurrentOffset = 0; uiCurrentOffset < uiSourceLinearSize; uiCurrentOffset++)
  {
    currentColor = &palette[pSourceData[uiCurrentOffset]];
    if(pSourceData[uiCurrentOffset] != PC_COLOR_TRANSPARENT)
    {
      ((u32*)tempSurface->pixels)[uiCurrentOffset] = (u32)((currentColor->a << 24) + (currentColor->r << 16) + (currentColor->g << 8) + (currentColor->b << 0));
    }
  }

  if(SDL_MUSTLOCK(tempSurface)
    SDL_UnlockSurface(tempSurface);

  // Create texture from surface pixels
  newTexture = SDL_CreateTextureFromSurface(renderer, tempSurface);

  // Save the surface to disk for verification only
  sprintf(strSurfacePath, "c:\tmp\surfaces\%s.png", GenerateUniqueName());
  IMG_SavePNG(tempSurface, strSurfacePath);

  // Get rid of old loaded surface
  SDL_FreeSurface(tempSurface);

  return newTexture;
}

请注意,在原始代码中,我正在检查边界,并在 SDL_Create* 之后检查 NULL。我也知道最好有一个 spritesheet 的纹理而不是单独加载每个纹理。

编辑: 这是我在捕获帧并使用资源视图时在 NSight 中观察到的示例。

前3186个纹理是正确的。然后我得到 43 个空纹理。然后我得到 228 个正确的纹理。然后是100个坏的。然后是539个正确的。然后是665个坏的。它像那样随机进行,每次我 运行 我的程序时它都会改变。

同样,每次 IMG_SavePNG 保存的表面都是正确的。这似乎表明当我调用 SDL_CreateTextureFromSurface 时发生了一些事情,但在那时,我不想排除任何可能性,因为这是一个非常奇怪的问题,而且到处都是未定义的行为。但是就是找不到问题。

在@mark-benningfield 的帮助下,我找到了问题。

TL;DR

使用 DX11 渲染器的 SDL 中存在错误(或至少是未记录的功能)。有一个解决方法;见文末

上下文

我正在尝试在我的程序启动时加载大约 12,000 个纹理。我知道这不是一个好主意,但我正计划将其用作另一个更健全的系统的垫脚石。

详情

我在调试该问题时意识到,DirectX 11 的 SDL 渲染器在创建纹理时会执行此操作:

result = ID3D11Device_CreateTexture2D(rendererData->d3dDevice,
         &textureDesc,
         NULL,
         &textureData->mainTexture
         );

微软的ID3D11Device::CreateTexture2D method页面表明:

If you don't pass anything to pInitialData, the initial content of the memory for the resource is undefined. In this case, you need to write the resource content some other way before the resource is read.

如果我们相信 that article :

Default Usage The most common type of usage is default usage. To fill a default texture (one created with D3D11_USAGE_DEFAULT) you can :

[...]

After calling ID3D11Device::CreateTexture2D, use ID3D11DeviceContext::UpdateSubresource to fill the default texture with data from a pointer provided by the application.

所以看起来 D3D11_CreateTexture 正在使用默认用法的第二种方法来初始化纹理及其内容。

但在那之后,在 SDL 中,我们调用 SDL_UpdateTexture(不检查 return 值;我稍后会谈到)。如果我们挖掘直到获得 D3D11 渲染器,我们会得到:

static int
D3D11_UpdateTextureInternal(D3D11_RenderData *rendererData, ID3D11Texture2D *texture, int bpp, int x, int y, int w, int h, const void *pixels, int pitch)
{
    ID3D11Texture2D *stagingTexture;
[...]
    /* Create a 'staging' texture, which will be used to write to a portion of the main texture. */
    ID3D11Texture2D_GetDesc(texture, &stagingTextureDesc);
[...]
    result = ID3D11Device_CreateTexture2D(rendererData->d3dDevice, &stagingTextureDesc, NULL, &stagingTexture);
[...]
    /* Get a write-only pointer to data in the staging texture: */
    result = ID3D11DeviceContext_Map(rendererData->d3dContext, (ID3D11Resource *)stagingTexture, 0, D3D11_MAP_WRITE, 0, &textureMemory);
[...]
    /* Commit the pixel buffer's changes back to the staging texture: */
    ID3D11DeviceContext_Unmap(rendererData->d3dContext, (ID3D11Resource *)stagingTexture, 0);

    /* Copy the staging texture's contents back to the texture: */
    ID3D11DeviceContext_CopySubresourceRegion(rendererData->d3dContext, (ID3D11Resource *)texture, 0, x, y, 0, (ID3D11Resource *)stagingTexture, 0, NULL);

    SAFE_RELEASE(stagingTexture);

    return 0;
}

注意:为了简洁起见,代码被删减了。

这似乎表明,基于that article I mentioned,SDL正在使用Default Usage的第二种方法在GPU上分配纹理内存,但使用Staging Usage上传实际像素。

我对 DX11 编程知之甚少,但这种技术的混合让我的程序员感到刺痛。

我联系了一个我认识的游戏程序员,向他解释了这个问题。他告诉我以下有趣的内容:

  • 驱动程序可以决定存储临时纹理的位置。它通常位于 CPU RAM 中。
  • 最好指定一个 pInitialData 指针,因为驱动程序可以决定异步上传纹理。
  • 如果加载过多暂存纹理而未将它们提交给 GPU,则可能会填满 RAM。

然后我想知道为什么 SDL 在我调用 SDL_CreateTextureFromSurface 时没有 return 给我一个 "out of memory" 错误,我发现了原因(再次,为简洁起见):

SDL_Texture *
SDL_CreateTextureFromSurface(SDL_Renderer * renderer, SDL_Surface * surface)
{
[...]

    SDL_Texture *texture;

[...]

    texture = SDL_CreateTexture(renderer, format, SDL_TEXTUREACCESS_STATIC,
                                surface->w, surface->h);
    if (!texture) {
        return NULL;
    }

[...]

        if (SDL_MUSTLOCK(surface)) {
            SDL_LockSurface(surface);
            SDL_UpdateTexture(texture, NULL, surface->pixels, surface->pitch);
            SDL_UnlockSurface(surface);
        } else {
            SDL_UpdateTexture(texture, NULL, surface->pixels, surface->pitch);
        }

[...]

    return texture;
}

如果纹理创建成功,则不关心更新纹理是否成功(不检查SDL_UpdateTexture的return值)。

解决方法

穷人解决这个问题的方法是每次调用 SDL_CreateTextureFromSurface.

时调用 SDL_RenderPresent

根据您的纹理大小,每一百个纹理执行一次可能没问题。但请注意,在不更新渲染器的情况下重复调用 SDL_CreateTextureFromSurface 实际上会填满系统 RAM,并且 SDL 不会 return 您检查任何错误情况。

具有讽刺意味的是,如果我在屏幕上实现了一个带有完成百分比的 "correct" 加载循环,我就永远不会遇到这个问题。但是命运让我以快速而肮脏的方式实现了这个,作为一个更大系统的概念证明,我陷入了这个问题。