0
\$\begingroup\$

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<ID3D11Texture2D> 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->GetDxDevice()->CreateTexture2D(&tex_desc, (mustUseInitialData ? &subresource_data : nullptr), &dx_tex);
    bool succeeded = SUCCEEDED(hr);
    if(succeeded) {
        return std::make_unique<Texture2D>(*_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()->GetDxContext();
    auto* dx_resource = _tile_index_texture->GetDxResource();
    if(HRESULT hr = dx_context->Map(dx_resource, 0, D3D11_MAP_WRITE_DISCARD, 0U, &resource); SUCCEEDED(hr)) {
        std::memcpy(resource.pData, data.data(), data.size() * sizeof(Rgba));
        dx_context->Unmap(dx_resource, 0);
    }
}
```
\$\endgroup\$
2
\$\begingroup\$

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.

\$\endgroup\$
7
  • \$\begingroup\$ To copy a row at a time, is the terminating condition for(auto i = 0; i < resource.DepthPitch; i += resource.RowPitch) { std::memcpy(resource.pData + i, data.data() + i, resource.RowPitch); }? \$\endgroup\$
    – Casey
    Dec 13 '20 at 14:23
  • \$\begingroup\$ No, depthpitch is for 3d textures. Use width and height of the texture. I'm on mobile at the moment so it's not easy to provide code, but I can do so later if required. \$\endgroup\$ Dec 13 '20 at 15:21
  • \$\begingroup\$ Could you provide an example? \$\endgroup\$
    – Casey
    Dec 13 '20 at 19:28
  • 1
    \$\begingroup\$ This works great. I tend to avoid low-level C functions like memcpy unless I have to and this was one of those cases. Makes getting it right the first time more difficult. \$\endgroup\$
    – Casey
    Dec 13 '20 at 22:21
  • 2
    \$\begingroup\$ See the DirectXTex project for numerous examples of 'row-by-row' copies. \$\endgroup\$ Dec 14 '20 at 20:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.