[Fix, Test] (DirectVulkan, GLState): apply a texture view level and layer window at every subresource boundary

This commit is contained in:
2026-08-22 11:17:09 -04:00
parent 6bb844b1c1
commit 473d9951b7
8 changed files with 363 additions and 35 deletions
@@ -9,6 +9,7 @@
#include "TextureObjectView.h"
#include <algorithm>
#include <cstring>
namespace MobileGL::MG_State::GLState {
namespace {
@@ -149,6 +150,53 @@ namespace MobileGL::MG_State::GLState {
return size;
}
SizeT TextureObjectView::LayerByteOffset(TextureUploadTarget viewTarget, Uint mipmapLevel) const {
if (m_viewMinLayer == 0 || m_ownerMipmap == nullptr) return 0;
const LayerAxis ownerAxis = LayerAxisOf(m_storageOwner->GetTarget());
if (ownerAxis == LayerAxis::None) {
// A cube-map owner keeps each face in its OWN blob, and ToOwnerUploadTarget already
// picked the right one; a 3D or plain 2D owner has no layers to skip.
return 0;
}
const TextureUploadTarget ownerTarget = ToOwnerUploadTarget(viewTarget);
const Uint ownerLevel = ToOwnerLevel(mipmapLevel);
const IntVec3 ownerSize = m_ownerMipmap->GetMipmapTexelSize(ownerTarget, ownerLevel);
const SizeT ownerBytes = m_ownerMipmap->GetMipmapByteSize(ownerTarget, ownerLevel);
const SizeT ownerTexels = static_cast<SizeT>(std::max(ownerSize.x(), 0)) *
static_cast<SizeT>(std::max(ownerSize.y(), 0)) *
static_cast<SizeT>(std::max(ownerSize.z(), 1));
if (ownerTexels == 0 || ownerBytes == 0) return 0;
const SizeT bytesPerTexel = ownerBytes / ownerTexels;
// One "layer" is a whole x*y slice for a 2D/cube array, and a single row of `width`
// texels for a 1D array (whose layer count lives in the state-side height).
const SizeT layerTexels = ownerAxis == LayerAxis::Y
? static_cast<SizeT>(std::max(ownerSize.x(), 0))
: static_cast<SizeT>(std::max(ownerSize.x(), 0)) *
static_cast<SizeT>(std::max(ownerSize.y(), 0));
const SizeT offset = static_cast<SizeT>(m_viewMinLayer) * layerTexels * bytesPerTexel;
return offset < ownerBytes ? offset : 0;
}
IntVec3 TextureObjectView::ToOwnerRegionOffset(const IntVec3& viewOffset) const {
if (m_viewMinLayer == 0) return viewOffset;
IntVec3 offset = viewOffset;
// The dirty region is recorded in the OWNER's blob coordinates - that is the space its
// upload path walks - so the view's layer origin has to be added here even though
// MapMipmapData hands back an already-shifted POINTER. The two are not double-counting:
// one moves the bytes, the other tells the owner which of its layers moved.
switch (LayerAxisOf(m_storageOwner->GetTarget())) {
case LayerAxis::Y:
offset.y() += static_cast<Int>(m_viewMinLayer);
break;
case LayerAxis::Z:
offset.z() += static_cast<Int>(m_viewMinLayer);
break;
case LayerAxis::None:
break;
}
return offset;
}
Uint TextureObjectView::GetMipmapLevelCount() const {
if (m_ownerMipmap == nullptr) return 0;
const Uint ownerLevels = m_ownerMipmap->GetMipmapLevelCount();
@@ -180,7 +228,14 @@ namespace MobileGL::MG_State::GLState {
const SizeT viewTexels = static_cast<SizeT>(std::max(viewSize.x(), 0)) *
static_cast<SizeT>(std::max(viewSize.y(), 0)) *
static_cast<SizeT>(std::max(viewSize.z(), 1));
return (ownerBytes / ownerTexels) * viewTexels;
const SizeT viewBytes = (ownerBytes / ownerTexels) * viewTexels;
// Clamped against what remains of the owner's blob past this view's layer origin. A view
// whose layer window the shadow cannot lay out contiguously - several faces of a cube-map
// owner, which are separate blobs - would otherwise advertise more bytes than
// MapMipmapData can hand back, and a caller sizing a copy off this would overrun.
const SizeT layerOffset = LayerByteOffset(target, mipmapLevel);
const SizeT available = layerOffset < ownerBytes ? ownerBytes - layerOffset : 0;
return std::min(viewBytes, available);
}
void TextureObjectView::AllocateStorage(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapInput input) {
@@ -199,12 +254,36 @@ namespace MobileGL::MG_State::GLState {
void TextureObjectView::UpdateMipmapSubData(TextureUploadTarget uploadTarget, Uint mipmapLevel, DataPtr input) {
if (m_ownerMipmap == nullptr) return;
m_ownerMipmap->UpdateMipmapSubData(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel), input);
const TextureUploadTarget ownerTarget = ToOwnerUploadTarget(uploadTarget);
const Uint ownerLevel = ToOwnerLevel(mipmapLevel);
const SizeT layerOffset = LayerByteOffset(uploadTarget, mipmapLevel);
if (layerOffset == 0) {
m_ownerMipmap->UpdateMipmapSubData(ownerTarget, ownerLevel, input);
return;
}
// The owner's whole-level write starts at ITS level origin, which for a layer-sliced view
// is the wrong place: writing there would silently overwrite the parent's layers 0..n
// instead of the window this view opened. Write through the shifted pointer instead, and
// mark exactly the layers that moved.
auto* destination = static_cast<Uint8*>(m_ownerMipmap->MapMipmapData(ownerTarget, ownerLevel));
if (destination == nullptr || input.data == nullptr || input.size == 0) return;
const SizeT capacity = GetMipmapByteSize(uploadTarget, mipmapLevel);
std::memcpy(destination + layerOffset, input.data, std::min(input.size, capacity));
const IntVec3 viewSize = GetMipmapTexelSize(uploadTarget, mipmapLevel);
MarkStorageDirtyRegion(uploadTarget, mipmapLevel, IntVec3{0, 0, 0},
IntVec3{viewSize.x(), viewSize.y(), std::max(viewSize.z(), 1)});
}
void* TextureObjectView::MapMipmapData(TextureUploadTarget uploadTarget, Uint mipmapLevel) {
if (m_ownerMipmap == nullptr) return nullptr;
return m_ownerMipmap->MapMipmapData(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel));
auto* data = static_cast<Uint8*>(
m_ownerMipmap->MapMipmapData(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel)));
if (data == nullptr) return nullptr;
// Shifted to the view's first LAYER, so that a caller which maps this pointer and then
// offsets into it using the extents GetMipmapTexelSize reports - which is what every
// glTexSubImage*/glGetTexImage path does - lands on the layers this view addresses rather
// than on the parent's first ones.
return data + LayerByteOffset(uploadTarget, mipmapLevel);
}
void TextureObjectView::MarkStorageDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty) {
@@ -220,8 +299,8 @@ namespace MobileGL::MG_State::GLState {
void TextureObjectView::MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel, IntVec3 offset,
IntVec3 size) {
if (m_ownerMipmap == nullptr) return;
m_ownerMipmap->MarkStorageDirtyRegion(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel), offset,
size);
m_ownerMipmap->MarkStorageDirtyRegion(ToOwnerUploadTarget(uploadTarget), ToOwnerLevel(mipmapLevel),
ToOwnerRegionOffset(offset), size);
}
MipmapDirtyRegion TextureObjectView::GetStorageDirtyRegion(TextureUploadTarget uploadTarget,
@@ -30,13 +30,17 @@ namespace MobileGL::MG_State::GLState {
// "<minlevel> plus the value of TEXTURE_VIEW_MIN_LEVEL from the original texture" rule
// means; one hop therefore always reaches real storage and no recursion is possible.
//
// LAYER offsets are deliberately NOT applied here. The TextureObjectMipmap interface
// addresses storage as (upload target, level) and a layer lives INSIDE a level's blob, so a
// layer offset is not expressible at this boundary. The entry points that move texels for a
// view (glTexSubImage*, glGetTexImage) therefore redirect to the owner themselves and add
// GetViewMinLayer() to the z coordinate there, where it can be said. What this class does
// apply is the view's layer COUNT, because the level extents it reports are what both
// backends size their images and views from.
// LEVEL offsets are applied by shifting the level index; LAYER offsets cannot be, because the
// TextureObjectMipmap interface addresses storage as (upload target, level) and a layer lives
// INSIDE a level's blob. They are applied two other ways instead, and the pair is what keeps
// a layer-sliced view from corrupting its parent:
// * MapMipmapData returns a pointer already advanced to the view's first layer, so a caller
// that maps it and then offsets using the extents GetMipmapTexelSize reports - which is
// what every glTexSubImage*/glGetTexImage path does - writes the layers it meant to; and
// * MarkStorageDirtyRegion moves the region's origin into the OWNER's layer space, which is
// the space its upload path walks.
// Those two are not double-counting: one moves the bytes, the other names which of the
// owner's layers moved.
class TextureObjectView : public TextureObjectMipmap {
public:
TextureObjectView(Uint externalIndex, TextureTarget target, SharedPtr<ITextureObject> storageOwner,
@@ -45,6 +49,13 @@ namespace MobileGL::MG_State::GLState {
const SharedPtr<ITextureObject>& GetViewStorageOwner() const override { return m_storageOwner; }
const Vector<TextureUploadTarget>& GetUploadTargets() const override { return m_uploadTargets; }
// A view is immutable from birth (GL 4.6 core 8.18 sets its TEXTURE_IMMUTABLE_FORMAT), and
// unconditionally so: the base class infers immutability from a non-zero level count, and
// a degenerate view - one the spec's min() composition narrowed to zero levels - would
// otherwise report GL_FALSE, walk straight past ValidateTextureMutable and let
// glTexImage2D respecify the PARENT's immutable storage through AllocateStorage.
Bool IsImmutable() const override { return true; }
// GL 4.6 core 8.18: "TEXTURE_IMMUTABLE_LEVELS is set to the value of
// TEXTURE_IMMUTABLE_LEVELS from the original texture" - NOT to <numlevels>. Kept as a
// forward rather than in m_immutableLevels so that the base class's level-range clamp
@@ -98,6 +109,13 @@ namespace MobileGL::MG_State::GLState {
// axis. A GL 1D array carries its layer count in the state-side HEIGHT while every other
// layered target carries it in z, so the axis is target-dependent.
IntVec3 ToViewLevelSize(const IntVec3& ownerLevelSize) const;
// Where this view's first LAYER starts inside the owner's level blob. The layer axis a
// level's bytes are laid out along is the OWNER's, so this is a slice for a 2D/cube array
// and a single row for a 1D array; a cube-map owner returns 0 because its faces are
// separate blobs that ToOwnerUploadTarget already selects between.
SizeT LayerByteOffset(TextureUploadTarget viewTarget, Uint mipmapLevel) const;
// A dirty-region origin moved from the view's layer space into the owner's.
IntVec3 ToOwnerRegionOffset(const IntVec3& viewOffset) const;
SharedPtr<ITextureObject> m_storageOwner;
// Non-owning; m_storageOwner keeps it alive and is never a view, so this is set once in