0

Background

I'm generating a memory texture at load time to store the offsets into a texture atlas for a tilemap as described here Any way to combine instantiated sprite renderers into one texture so I can apply into a plane at runtime? (but in DirectX, not Unity.)

I then read that texture at runtime in the geometry shader to generate a tile mesh from each tile. This works for static tiles. Animated tiles require that the tile indicies be updated in the texture every frame.

Problem

This is where I'm stuck. The data loaded into the texture is not the same when it is retrieved via a call to ID3D11DeviceContext::Map. The stride, pitch, or even the format is different such that the texture does not get fully updated or the wrong pixels are generated.

Question

Given the implementation of UpdateTileIndexTexture below, what am I doing wrong in order to fully overwrite the initial texture data with the correct pixel data?

Implementation

Initial generation of the dynamic texture. This step works.

void Map::GenerateTileIndexTexture() noexcept {
    const auto dims = CalcMaxDimensions();
    const auto width = static_cast<std::size_t>(dims.x);
    const auto height = static_cast<std::size_t>(dims.y);
    std::vector<Rgba> data(width * height, Rgba::Black);
    auto data_iter = std::begin(data);
    for(auto& layer : _layers) {
        for(auto& tile : *layer) {
            const auto* def = tile.GetDefinition();
            const auto coords = def->GetSprite()->GetCurrentSpriteCoords();
            data_iter->r = static_cast<unsigned char>(coords.x);
            data_iter->g = static_cast<unsigned char>(coords.y);
            ++data_iter;
        }
    }
    _tile_index_texture = _renderer.Create2DTextureFromMemory(data, static_cast<unsigned int>(width), static_cast<unsigned int>(height), BufferUsage::Dynamic);
    auto* mat = GetTileMaterial();
    mat->SetTextureSlot(Material::TextureID::Normal, _tile_index_texture.get());
}

Implementation of Create2DTextureFromMemory for reference:

std::unique_ptr<Texture> Renderer::Create2DTextureFromMemory(const std::vector<Rgba>& data, unsigned int width /*= 1*/, unsigned int height /*= 1*/, const BufferUsage& bufferUsage /*= BufferUsage::STATIC*/, const BufferBindUsage& bindUsage /*= BufferBindUsage::SHADER_RESOURCE*/, const ImageFormat& imageFormat /*= ImageFormat::R8G8B8A8_UNORM*/) const noexcept {
    D3D11_TEXTURE2D_DESC tex_desc{};
//...Snip

// Setup Initial Data
D3D11_SUBRESOURCE_DATA subresource_data = {};

subresource_data.pSysMem = data.data();
subresource_data.SysMemPitch = width * sizeof(Rgba);
subresource_data.SysMemSlicePitch = width * height * sizeof(Rgba);

Microsoft::WRL::ComPtr&lt;ID3D11Texture2D&gt; dx_tex{};

//If IMMUTABLE or not multi-sampled, must use initial data.
bool isMultiSampled = tex_desc.SampleDesc.Count != 1 || tex_desc.SampleDesc.Quality != 0;
bool isImmutable = bufferUsage == BufferUsage::Static;
bool mustUseInitialData = isImmutable || !isMultiSampled;

HRESULT hr = _rhi_device-&gt;GetDxDevice()-&gt;CreateTexture2D(&amp;tex_desc, (mustUseInitialData ? &amp;subresource_data : nullptr), &amp;dx_tex);
bool succeeded = SUCCEEDED(hr);
if(succeeded) {
    return std::make_unique&lt;Texture2D&gt;(*_rhi_device, dx_tex);
} else {
    return nullptr;
}

}

Attempt at updating the dynamic texture at runtime:

void Map::UpdateTileIndexTexture() noexcept {
    const auto dims = CalcMaxDimensions();
    const auto width = static_cast<std::size_t>(dims.x);
    const auto height = static_cast<std::size_t>(dims.y);
    std::vector<Rgba> data(width * height, Rgba::Black);
    auto data_iter = std::begin(data);
    for(auto& layer : _layers) {
        for(auto& tile : *layer) {
            if(const auto* def = tile.GetDefinition(); def != nullptr) {
                const auto spriteCoords = Vector2{def->GetSprite()->GetCurrentSpriteCoords()};
                data_iter->r = static_cast<unsigned char>(spriteCoords.x);
                data_iter->g = static_cast<unsigned char>(spriteCoords.y);
                ++data_iter;
            }
        }
    }
D3D11_MAPPED_SUBRESOURCE resource{};
auto* dx_context = _renderer.GetDeviceContext()-&gt;GetDxContext();
auto* dx_resource = _tile_index_texture-&gt;GetDxResource();
if(HRESULT hr = dx_context-&gt;Map(dx_resource, 0, D3D11_MAP_WRITE_DISCARD, 0U, &amp;resource); SUCCEEDED(hr)) {
    std::memcpy(resource.pData, data.data(), data.size() * sizeof(Rgba));
    dx_context-&gt;Unmap(dx_resource, 0);
}

} ```

Casey
  • 2,025
  • 2
  • 17
  • 26

1 Answers1

2

Check the returned pitch from your map call - you're assuming it's width * 4 (for 32-bit RGBA) but it may not be (particularly if your texture is not a power of 2 or it's width is not a multiple of 4).

You can only memcpy the entire block in one operation if pitch is equal to width * number of bytes in the format. Otherwise you must memcpy one row at a time.

Sample code, excuse C-isms:

// assumes that src and dst are 32-bit RGBA data
unsigned *src; // this comes from whatever your input is
unsigned *dst = (unsigned *) msr.pData; // msr is a D3D11_MAPPED_SUBRESOURCE derived from ID3D11DeviceContext::Map

// width and height come from ID3D11Texture2D::GetDesc for (int i = 0; i < height; i++) { memcpy (dst, src, width * 4); // copy one row at a time because msr.RowPitch may be != (width * 4) dst += msr.RowPitch >> 2; // msr.RowPitch is in bytes so for 32-bit data we divide by 4 (or downshift by 2, same thing) src += width; // assumes pitch of source data is equal to width * 4 }

You can, of course, also include a test for if (msr.RowPitch == width * 4) and do a single memcpy of the entire thing if it's true.

Maximus Minimus
  • 20,144
  • 2
  • 38
  • 66