mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 21:58:31 +09:00
Compare commits
5
Commits
990e518e33
...
d7976326fa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7976326fa | ||
|
|
72ee7c439c | ||
|
|
cdea275227 | ||
|
|
6a02c5fea0 | ||
|
|
7db5b35a3e |
@@ -2440,10 +2440,45 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
static_cast<SizeT>(dirtyRegion.lo.z()) * levelSliceBytes +
|
||||
static_cast<SizeT>(dirtyRegion.lo.y()) * levelRowBytes +
|
||||
static_cast<SizeT>(dirtyRegion.lo.x()) * bpp;
|
||||
// Scatter refinement behind the union box: ~100 sprite
|
||||
// writes into an atlas leave a box that spans nearly the
|
||||
// whole level while the touched texels are a few percent of
|
||||
// it. The storage's bounded rect list recovers the true
|
||||
// footprint; each rect is uploaded with the same
|
||||
// ROW_LENGTH striding into the level shadow as the box
|
||||
// path. The storage only hands the list out when its
|
||||
// summed area is materially smaller than the box (0
|
||||
// otherwise), so the extra calls always move fewer bytes.
|
||||
MG_State::GLState::MipmapDirtyRegion
|
||||
dirtyRects[MG_State::GLState::MipmapStorage::kMaxDirtyRects];
|
||||
SizeT dirtyRectCount = 0;
|
||||
if (subRectEligible) {
|
||||
dirtyRectCount = textureMipmapObject->GetStorageDirtyRects(
|
||||
uploadTarget, level, dirtyRects,
|
||||
MG_State::GLState::MipmapStorage::kMaxDirtyRects);
|
||||
}
|
||||
const auto rectShadowPtr = [&](const MG_State::GLState::MipmapDirtyRegion& rect) {
|
||||
return static_cast<const Uint8*>(uploadData) +
|
||||
static_cast<SizeT>(rect.lo.z()) * levelSliceBytes +
|
||||
static_cast<SizeT>(rect.lo.y()) * levelRowBytes +
|
||||
static_cast<SizeT>(rect.lo.x()) * bpp;
|
||||
};
|
||||
switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) {
|
||||
case TextureTarget::Texture2D:
|
||||
case TextureTarget::TextureCubeMap:
|
||||
if (subRectEligible) {
|
||||
if (subRectEligible && dirtyRectCount >= 2) {
|
||||
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x());
|
||||
for (SizeT r = 0; r < dirtyRectCount; ++r) {
|
||||
const auto& rect = dirtyRects[r];
|
||||
g_GLESFuncs.glTexSubImage2D(
|
||||
glUploadTarget, static_cast<GLint>(level), rect.lo.x(),
|
||||
rect.lo.y(), static_cast<GLsizei>(rect.hi.x() - rect.lo.x()),
|
||||
static_cast<GLsizei>(rect.hi.y() - rect.lo.y()), glFormat,
|
||||
glType, rectShadowPtr(rect));
|
||||
}
|
||||
// The surrounding ScopedDefaultUnpackState shadow says 0.
|
||||
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
} else if (subRectEligible) {
|
||||
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x());
|
||||
g_GLESFuncs.glTexSubImage2D(
|
||||
glUploadTarget, static_cast<GLint>(level), dirtyRegion.lo.x(),
|
||||
@@ -2463,7 +2498,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// ES 3.2 has GL_TEXTURE_CUBE_MAP_ARRAY natively and it stores exactly
|
||||
// like a 2D array whose depth is 6 * the cube count.
|
||||
case TextureTarget::TextureCubeMapArray:
|
||||
if (subRectEligible) {
|
||||
if (subRectEligible && dirtyRectCount >= 2) {
|
||||
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x());
|
||||
g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, texelSize.y());
|
||||
for (SizeT r = 0; r < dirtyRectCount; ++r) {
|
||||
const auto& rect = dirtyRects[r];
|
||||
g_GLESFuncs.glTexSubImage3D(
|
||||
glUploadTarget, static_cast<GLint>(level), rect.lo.x(),
|
||||
rect.lo.y(), rect.lo.z(),
|
||||
static_cast<GLsizei>(rect.hi.x() - rect.lo.x()),
|
||||
static_cast<GLsizei>(rect.hi.y() - rect.lo.y()),
|
||||
static_cast<GLsizei>(rect.hi.z() - rect.lo.z()), glFormat,
|
||||
glType, rectShadowPtr(rect));
|
||||
}
|
||||
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0);
|
||||
} else if (subRectEligible) {
|
||||
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, texelSize.x());
|
||||
g_GLESFuncs.glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, texelSize.y());
|
||||
g_GLESFuncs.glTexSubImage3D(
|
||||
|
||||
@@ -2305,6 +2305,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
IntVec3 regionLo = {0, 0, 0};
|
||||
IntVec3 regionSize = {0, 0, 0};
|
||||
SizeT texelBytes = 0;
|
||||
// Scatter refinement of the single dirty box: when the storage's rect
|
||||
// list reports the writes' true footprint (~100 sprites whose union box
|
||||
// spans the whole atlas), each rect is staged tightly and copied with
|
||||
// its own VkBufferImageCopy in ONE vkCmdCopyBufferToImage. Empty means
|
||||
// "stage the one box above". Only set while subRegion.
|
||||
Vector<MG_State::GLState::MipmapDirtyRegion> rects;
|
||||
};
|
||||
|
||||
Vector<UploadItem> uploadItems;
|
||||
@@ -2368,6 +2374,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static_cast<SizeT>(uploadItem.regionSize.y()) *
|
||||
static_cast<SizeT>(uploadItem.regionSize.z()) *
|
||||
uploadItem.texelBytes;
|
||||
// Scatter refinement: the storage only hands out its rect list
|
||||
// when the rects' summed area is materially smaller than the
|
||||
// union box (0 otherwise), so taking it always stages fewer
|
||||
// bytes than the box - the very amplification this path exists
|
||||
// to avoid paying twice.
|
||||
MG_State::GLState::MipmapDirtyRegion
|
||||
dirtyRects[MG_State::GLState::MipmapStorage::kMaxDirtyRects];
|
||||
const SizeT dirtyRectCount = mipmapTexture.GetStorageDirtyRects(
|
||||
target, level, dirtyRects, MG_State::GLState::MipmapStorage::kMaxDirtyRects);
|
||||
if (dirtyRectCount >= 2) {
|
||||
uploadItem.rects.assign(dirtyRects, dirtyRects + dirtyRectCount);
|
||||
SizeT rectTexels = 0;
|
||||
for (const auto& rect : uploadItem.rects) {
|
||||
rectTexels += rect.TexelCount();
|
||||
}
|
||||
uploadItem.uploadByteSize = rectTexels * uploadItem.texelBytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (formatInfo.expandRgbToRgba) {
|
||||
@@ -2534,22 +2557,37 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
std::memcpy(dst, item.source, item.uploadByteSize);
|
||||
continue;
|
||||
}
|
||||
// Tight-pack the dirty box: the shadow keeps whole-level rows, the
|
||||
// staging slice holds only the region (bufferRowLength stays 0).
|
||||
// Tight-pack the dirty box(es): the shadow keeps whole-level rows, the
|
||||
// staging slice holds only the region (bufferRowLength stays 0). Multi-
|
||||
// rect items pack their rects back to back in list order; the copy loop
|
||||
// below recomputes the same running offsets.
|
||||
const SizeT levelRowBytes = static_cast<SizeT>(item.texelSize.x()) * item.texelBytes;
|
||||
const SizeT levelSliceBytes = static_cast<SizeT>(item.texelSize.y()) * levelRowBytes;
|
||||
const SizeT regionRowBytes = static_cast<SizeT>(item.regionSize.x()) * item.texelBytes;
|
||||
const Uint8* src = static_cast<const Uint8*>(item.source);
|
||||
for (Int z = 0; z < item.regionSize.z(); ++z) {
|
||||
for (Int y = 0; y < item.regionSize.y(); ++y) {
|
||||
const Uint8* srcRow = src +
|
||||
static_cast<SizeT>(item.regionLo.z() + z) * levelSliceBytes +
|
||||
static_cast<SizeT>(item.regionLo.y() + y) * levelRowBytes +
|
||||
static_cast<SizeT>(item.regionLo.x()) * item.texelBytes;
|
||||
std::memcpy(dst + (static_cast<SizeT>(z) * item.regionSize.y() + y) * regionRowBytes,
|
||||
srcRow, regionRowBytes);
|
||||
const auto packBox = [&](Uint8* out, const IntVec3& lo, const IntVec3& boxSize) {
|
||||
const SizeT boxRowBytes = static_cast<SizeT>(boxSize.x()) * item.texelBytes;
|
||||
for (Int z = 0; z < boxSize.z(); ++z) {
|
||||
for (Int y = 0; y < boxSize.y(); ++y) {
|
||||
const Uint8* srcRow = src + static_cast<SizeT>(lo.z() + z) * levelSliceBytes +
|
||||
static_cast<SizeT>(lo.y() + y) * levelRowBytes +
|
||||
static_cast<SizeT>(lo.x()) * item.texelBytes;
|
||||
std::memcpy(out + (static_cast<SizeT>(z) * static_cast<SizeT>(boxSize.y()) + y) *
|
||||
boxRowBytes,
|
||||
srcRow, boxRowBytes);
|
||||
}
|
||||
}
|
||||
return static_cast<SizeT>(boxSize.x()) * static_cast<SizeT>(boxSize.y()) *
|
||||
static_cast<SizeT>(boxSize.z()) * item.texelBytes;
|
||||
};
|
||||
if (!item.rects.empty()) {
|
||||
for (const auto& rect : item.rects) {
|
||||
dst += packBox(dst, rect.lo,
|
||||
IntVec3{rect.hi.x() - rect.lo.x(), rect.hi.y() - rect.lo.y(),
|
||||
rect.hi.z() - rect.lo.z()});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
packBox(dst, item.regionLo, item.regionSize);
|
||||
}
|
||||
|
||||
const VkImageAspectFlags aspectMask = GetAspectMaskForFormat(outResource.format);
|
||||
@@ -2573,6 +2611,46 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
outResource.viewType == VK_IMAGE_VIEW_TYPE_2D_ARRAY ||
|
||||
outResource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY;
|
||||
for (const auto& item : uploadItems) {
|
||||
if (!item.rects.empty()) {
|
||||
// Multi-rect item: one VkBufferImageCopy per rect, all submitted in a
|
||||
// single vkCmdCopyBufferToImage. The rect list is pairwise disjoint by
|
||||
// construction, so no two copies write the same texels. Multi-rect
|
||||
// implies subRegion, which implies a plain color aspect - the combined
|
||||
// depth-stencil split below can never see one of these.
|
||||
VkBufferImageCopy rectCopies[MG_State::GLState::MipmapStorage::kMaxDirtyRects];
|
||||
Uint32 rectCopyCount = 0;
|
||||
VkDeviceSize runningOffset = item.offset;
|
||||
for (const auto& rect : item.rects) {
|
||||
const IntVec3 rectSize = {rect.hi.x() - rect.lo.x(), rect.hi.y() - rect.lo.y(),
|
||||
rect.hi.z() - rect.lo.z()};
|
||||
const Uint32 rectDepth = static_cast<Uint32>(std::max(rectSize.z(), 1));
|
||||
VkBufferImageCopy rectCopy{};
|
||||
rectCopy.bufferOffset = stagingBase + runningOffset;
|
||||
rectCopy.bufferRowLength = 0;
|
||||
rectCopy.bufferImageHeight = 0;
|
||||
rectCopy.imageSubresource.aspectMask = aspectMask;
|
||||
rectCopy.imageSubresource.mipLevel = item.level;
|
||||
rectCopy.imageSubresource.baseArrayLayer = item.baseArrayLayer;
|
||||
rectCopy.imageSubresource.layerCount = 1;
|
||||
rectCopy.imageOffset = {rect.lo.x(), rect.lo.y(),
|
||||
depthSelectsArrayLayer ? 0 : rect.lo.z()};
|
||||
rectCopy.imageExtent = {static_cast<Uint32>(rectSize.x()),
|
||||
static_cast<Uint32>(rectSize.y()),
|
||||
depthSelectsArrayLayer ? 1u : rectDepth};
|
||||
if (depthSelectsArrayLayer) {
|
||||
// The GL "depth" axis addresses array layers here, so a partial
|
||||
// z-range narrows the layer span rather than the extent.
|
||||
rectCopy.imageSubresource.baseArrayLayer =
|
||||
item.baseArrayLayer + static_cast<Uint32>(rect.lo.z());
|
||||
rectCopy.imageSubresource.layerCount = rectDepth;
|
||||
}
|
||||
rectCopies[rectCopyCount++] = rectCopy;
|
||||
runningOffset += static_cast<VkDeviceSize>(rect.TexelCount() * item.texelBytes);
|
||||
}
|
||||
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, outResource.image,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, rectCopyCount, rectCopies);
|
||||
continue;
|
||||
}
|
||||
const Uint32 depthOrLayers = item.texelSize.z() > 0 ? static_cast<Uint32>(item.texelSize.z()) : 1u;
|
||||
VkBufferImageCopy copy{};
|
||||
copy.bufferOffset = stagingBase + item.offset;
|
||||
|
||||
@@ -3114,47 +3114,16 @@ void main() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Cross-frame revalidation. Only all-resident, unmapped layouts qualify:
|
||||
// - a streamed binding's slice moves to a new arena block every frame BY DESIGN,
|
||||
// but every such move funnels through BumpSliceEpoch, so the per-binding epoch
|
||||
// compares below catch it (as they do a respecify, a sub-data update, a
|
||||
// resident<->streamed promotion and a buffer becoming persistently mapped);
|
||||
// - a mapped buffer mutates its shadow with no API call and must re-run the
|
||||
// Sync/acquire path every frame, so it is excluded outright;
|
||||
// - epochs are minted from a process-lifetime counter, so a deleted buffer (or
|
||||
// resource) recycled at the same address can never reproduce a recorded epoch.
|
||||
if (entry.anyBufferMapped) {
|
||||
return false;
|
||||
}
|
||||
const auto& attributes = vao.GetAllAttributes();
|
||||
VkBufferResource* resources[ResolvedVertexBindings::kMaxBindings] = {};
|
||||
for (Uint32 binding = 0; binding < entry.bindingCount; ++binding) {
|
||||
// Compare against the LIVE pointer before any dereference: entry.buffers may
|
||||
// dangle if the app deleted a buffer since (the VAO unbind that deletion
|
||||
// performs bumps the config version, so the hash compare above already
|
||||
// declined - this is defense in depth for the aliased-address case).
|
||||
auto* bufferObject = attributes[entry.attributeLocations[binding]].Buffer.get();
|
||||
if (bufferObject != entry.buffers[binding]) {
|
||||
return false;
|
||||
}
|
||||
auto* resource = static_cast<VkBufferResource*>(bufferObject->GetBackendResource().get());
|
||||
if (resource == nullptr || resource->sliceEpoch != entry.sliceEpochs[binding]) {
|
||||
return false;
|
||||
}
|
||||
resources[binding] = resource;
|
||||
}
|
||||
// Every binding still resolves to the recorded slice. The per-frame resolve this
|
||||
// replaces had one side effect the busy tracking depends on (glBufferSubData's
|
||||
// host-write-vs-staged-copy choice): stamping each resource's GPU-use serial.
|
||||
// Do exactly that, then re-arm the entry so the rest of the frame's draws take
|
||||
// the one-compare path above.
|
||||
for (Uint32 binding = 0; binding < entry.bindingCount; ++binding) {
|
||||
resources[binding]->lastUseSerial = frameSerial;
|
||||
}
|
||||
entry.frameSerial = frameSerial;
|
||||
entry.sliceEpochCounter = m_bufferManager.GetSliceEpochCounter();
|
||||
ShadowedBindVertexBuffers(commandBuffer, entry.vkBuffers, entry.vkOffsets, entry.bindingCount);
|
||||
return true;
|
||||
// NO cross-frame trust: a memo recorded in an earlier frame declines here and
|
||||
// the draw re-resolves through the full acquire path. The epoch-compare
|
||||
// revalidation that used to sit here shipped visible corruption (journeymap /
|
||||
// common-mods retraces, vertex anomalies on Adreno): the acquire path is the
|
||||
// frame's content-sync point, and skipping it across frames trusted the
|
||||
// BumpSliceEpoch inventory to cover every way a buffer's GPU copy can go stale.
|
||||
// At least one path escapes it. Until that inventory is proven complete the
|
||||
// hot layout memo above (same-frame) keeps the factory-chase win, and the
|
||||
// first draw of each (VAO, frame) pays one full resolve.
|
||||
return false;
|
||||
}
|
||||
|
||||
VulkanRenderer::VaoDrawMemo* VulkanRenderer::LookupVaoDrawMemo(
|
||||
@@ -3709,18 +3678,10 @@ void main() {
|
||||
if (indexMemo->indexFrameSerial == frameSerial &&
|
||||
indexMemo->indexSliceEpochCounter == m_bufferManager.GetSliceEpochCounter()) {
|
||||
sliceStillValid = true;
|
||||
} else {
|
||||
auto* resource = static_cast<VkBufferResource*>(
|
||||
indexBufferShared->GetBackendResource().get());
|
||||
if (resource != nullptr && resource->sliceEpoch == indexMemo->indexSliceEpoch) {
|
||||
sliceStillValid = true;
|
||||
// Same busy-tracking stamp the skipped acquire would have made,
|
||||
// then re-arm the one-compare path for the rest of the frame.
|
||||
resource->lastUseSerial = frameSerial;
|
||||
indexMemo->indexFrameSerial = frameSerial;
|
||||
indexMemo->indexSliceEpochCounter = m_bufferManager.GetSliceEpochCounter();
|
||||
}
|
||||
}
|
||||
// NO cross-frame trust for the EBO either (same corruption class as the
|
||||
// vertex half, see TryBindResolvedVertexBindings): a memo from an earlier
|
||||
// frame declines and the draw re-runs the acquire, which is the sync point.
|
||||
if (sliceStillValid) {
|
||||
const VkDeviceSize memoBindOffset = indexMemo->indexSliceOffset +
|
||||
static_cast<VkDeviceSize>(pIndexBufferView->indexByteOffset);
|
||||
@@ -5113,13 +5074,21 @@ void main() {
|
||||
}
|
||||
|
||||
Uint32 VulkanRenderer::GetBaseTransformFlagsRaw() {
|
||||
// GetShaderTransformFlags is a pure function of the pre-transform, which only
|
||||
// changes on surface rotation - memoised so the per-draw path pays one field
|
||||
// compare instead of the call + switch.
|
||||
// GetShaderTransformFlags is a function of the pre-transform AND of whether
|
||||
// the bound draw framebuffer is the default one (the Y-flip/rotation bits
|
||||
// apply only when presenting). Memo keyed on both; keying on the
|
||||
// pre-transform alone served an FBO pass's unflipped flags to the following
|
||||
// default-framebuffer pass and flipped the whole frame.
|
||||
const VkSurfaceTransformFlagBitsKHR preTransform = m_swapchainObject.GetPreTransform();
|
||||
if (preTransform != m_baseTransformFlagsPreTransform) {
|
||||
const auto& currentDrawFBO =
|
||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
const Bool isDefaultFbo = currentDrawFBO != nullptr && currentDrawFBO->IsDefaultFramebuffer();
|
||||
if (!m_baseTransformFlagsKeyValid || preTransform != m_baseTransformFlagsPreTransform ||
|
||||
isDefaultFbo != m_baseTransformFlagsIsDefaultFbo) {
|
||||
m_baseTransformFlagsCache = GetShaderTransformFlags(preTransform).GetRaw();
|
||||
m_baseTransformFlagsPreTransform = preTransform;
|
||||
m_baseTransformFlagsIsDefaultFbo = isDefaultFbo;
|
||||
m_baseTransformFlagsKeyValid = true;
|
||||
}
|
||||
return m_baseTransformFlagsCache;
|
||||
}
|
||||
@@ -5127,10 +5096,34 @@ void main() {
|
||||
Bool VulkanRenderer::TrySetupDrawFastPath(FrameContext::FrameData& frame, GLenum mode,
|
||||
Flags<DrawSetupAspect> aspects, const DrawCmdParam& drawParams,
|
||||
const IndexBufferView* pIndexBufferView) {
|
||||
SetupDrawSnapshot& snap = m_setupDrawSnapshot;
|
||||
if (!snap.valid || !frame.isCommandRecording) {
|
||||
if (!frame.isCommandRecording) {
|
||||
return false;
|
||||
}
|
||||
// Entry select: by the draw program's lifetime id, MRU first (the id pins
|
||||
// the entry; every other fact is re-guarded below, so probing a stale
|
||||
// entry can only decline, never serve stale state).
|
||||
const auto& program = *MG_State::pGLContext->GetProgramForDraw();
|
||||
const Uint64 programLifetimeId = program.GetLifetimeId();
|
||||
SetupDrawSnapshot* snapPtr = nullptr;
|
||||
{
|
||||
SetupDrawSnapshot& mru = m_setupDrawSnapshots[m_setupDrawSnapshotMru];
|
||||
if (mru.valid && mru.programLifetimeId == programLifetimeId) {
|
||||
snapPtr = &mru;
|
||||
} else {
|
||||
for (Uint32 i = 0; i < kSetupDrawSnapshotCount; ++i) {
|
||||
SetupDrawSnapshot& candidate = m_setupDrawSnapshots[i];
|
||||
if (candidate.valid && candidate.programLifetimeId == programLifetimeId) {
|
||||
snapPtr = &candidate;
|
||||
m_setupDrawSnapshotMru = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (snapPtr == nullptr) {
|
||||
return false;
|
||||
}
|
||||
SetupDrawSnapshot& snap = *snapPtr;
|
||||
if (snap.aspects != aspects.GetRaw() || snap.mode != mode) {
|
||||
return false;
|
||||
}
|
||||
@@ -5142,9 +5135,7 @@ void main() {
|
||||
snap.imageIndex != m_imageIndexAcquired) {
|
||||
return false;
|
||||
}
|
||||
const auto& program = *MG_State::pGLContext->GetProgramForDraw();
|
||||
if (program.GetLifetimeId() != snap.programLifetimeId ||
|
||||
program.GetBackendStateVersion() != snap.programVersion) {
|
||||
if (program.GetBackendStateVersion() != snap.programVersion) {
|
||||
return false;
|
||||
}
|
||||
// A changed VAO does NOT decline: the VAO only feeds the pipeline's vertex
|
||||
@@ -5282,7 +5273,7 @@ void main() {
|
||||
}
|
||||
}
|
||||
if (bindsMoved &&
|
||||
!m_uniformManager->SampledBindingsUnchanged(program, programObj, m_sampledBindingRecordsScratch)) {
|
||||
!m_uniformManager->SampledBindingsUnchanged(program, programObj, snap.sampledBindingRecords)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -5291,8 +5282,11 @@ void main() {
|
||||
// sampleable, then stamp recording use exactly as the full path would.
|
||||
// A feedback case (sampled texture written by the active pass) fails the
|
||||
// layout check and falls back to the full path's end-pass handling.
|
||||
const auto& sampledTextures = m_sampledTexturesScratch;
|
||||
const auto& sampledResources = m_sampledResourcesScratch;
|
||||
// The ENTRY's copies, not the scratch vectors: with more than one entry
|
||||
// the scratch holds only the last full-path draw's set, which may belong
|
||||
// to a different program.
|
||||
const auto& sampledTextures = snap.sampledTextures;
|
||||
const auto& sampledResources = snap.sampledResources;
|
||||
if (sampledResources.size() != sampledTextures.size()) {
|
||||
return false;
|
||||
}
|
||||
@@ -5306,7 +5300,7 @@ void main() {
|
||||
// generation means SampledBindingsUnchanged proved the per-binding (texture,
|
||||
// sampler) pairs identical, and the sums/generation checks below cover every
|
||||
// remaining descriptor input.
|
||||
const Bool layoutSnapshotUsable = m_sampledLayoutSnapshots.size() == sampledTextures.size();
|
||||
const Bool layoutSnapshotUsable = snap.sampledLayouts.size() == sampledTextures.size();
|
||||
Bool samplerDescriptorsUnchanged = layoutSnapshotUsable;
|
||||
for (SizeT i = 0; i < sampledTextures.size(); ++i) {
|
||||
const auto* sampledTexture = sampledTextures[i];
|
||||
@@ -5317,8 +5311,8 @@ void main() {
|
||||
if (resource == nullptr || !IsValidSampledImageLayout(resource->layout)) {
|
||||
return false;
|
||||
}
|
||||
if (layoutSnapshotUsable && m_sampledLayoutSnapshots[i] != resource->layout) {
|
||||
m_sampledLayoutSnapshots[i] = resource->layout;
|
||||
if (layoutSnapshotUsable && snap.sampledLayouts[i] != resource->layout) {
|
||||
snap.sampledLayouts[i] = resource->layout;
|
||||
samplerDescriptorsUnchanged = false;
|
||||
}
|
||||
contentSum += sampledTexture->GetContentVersion();
|
||||
@@ -5437,17 +5431,51 @@ void main() {
|
||||
if (TrySetupDrawFastPath(frame, mode, aspects, drawParams, pIndexBufferView)) {
|
||||
return true;
|
||||
}
|
||||
// The fast path declined: whatever it saw may be stale. The full path
|
||||
// below re-resolves everything and refreshes the snapshot on success.
|
||||
m_setupDrawSnapshot.valid = false;
|
||||
const auto& drawFbo =
|
||||
MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
|
||||
if (drawFbo != nullptr && IsUnsupportedFramebufferForDirectVulkan(*drawFbo)) {
|
||||
// Nothing was mutated: other entries' per-probe guards (FBO identity +
|
||||
// version among them) stay authoritative, so none need invalidating.
|
||||
RecordUnsupportedFramebufferError(__func__);
|
||||
return false;
|
||||
}
|
||||
const auto& vao = *MG_State::pGLContext->GetBoundVertexArray();
|
||||
const auto& program = *MG_State::pGLContext->GetProgramForDraw();
|
||||
// The fast path declined (or had no entry for this program): whatever THIS
|
||||
// program's entry saw may be stale, and the full path below mutates state as
|
||||
// it goes, so the entry must not stay matchable if that path fails mid-way.
|
||||
// Select it now - the program's own entry when one exists, else an invalid
|
||||
// slot, else a round-robin victim - and invalidate it until the successful
|
||||
// refill at the end. Other programs' entries keep their validity: every fact
|
||||
// they carry is re-guarded per probe (live pass hash, epochs, versions,
|
||||
// sums), so a full path run in between can only make them decline.
|
||||
SetupDrawSnapshot* fillSnap = nullptr;
|
||||
{
|
||||
Uint32 fillIndex = kSetupDrawSnapshotCount;
|
||||
const Uint64 fillProgramLifetimeId = program.GetLifetimeId();
|
||||
for (Uint32 i = 0; i < kSetupDrawSnapshotCount; ++i) {
|
||||
if (m_setupDrawSnapshots[i].valid &&
|
||||
m_setupDrawSnapshots[i].programLifetimeId == fillProgramLifetimeId) {
|
||||
fillIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (fillIndex == kSetupDrawSnapshotCount) {
|
||||
for (Uint32 i = 0; i < kSetupDrawSnapshotCount; ++i) {
|
||||
if (!m_setupDrawSnapshots[i].valid) {
|
||||
fillIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fillIndex == kSetupDrawSnapshotCount) {
|
||||
fillIndex = m_setupDrawSnapshotVictim;
|
||||
m_setupDrawSnapshotVictim = (m_setupDrawSnapshotVictim + 1) % kSetupDrawSnapshotCount;
|
||||
}
|
||||
fillSnap = &m_setupDrawSnapshots[fillIndex];
|
||||
fillSnap->valid = false;
|
||||
m_setupDrawSnapshotMru = fillIndex;
|
||||
}
|
||||
ProgramFactory::CompileOptionFlags transformFlags =
|
||||
ProgramFactory::CompileOptionFlags(GetBaseTransformFlagsRaw());
|
||||
// Captured draws take the xfb-decorated program variant.
|
||||
@@ -5773,7 +5801,7 @@ void main() {
|
||||
// Snapshot the fully resolved configuration for the consecutive-draw
|
||||
// fast path (see TrySetupDrawFastPath).
|
||||
{
|
||||
auto& snap = m_setupDrawSnapshot;
|
||||
auto& snap = *fillSnap;
|
||||
const auto* nowActiveRenderPass = VkRenderPassManager::GetActiveRenderPass();
|
||||
if (nowActiveRenderPass != nullptr && !programObj.hasStorageImages) {
|
||||
snap.valid = true;
|
||||
@@ -5815,10 +5843,15 @@ void main() {
|
||||
snap.samplingResolutionGeneration = MG_State::pGLContext->GetSamplingResolutionGeneration();
|
||||
Uint64 snapContentSum = 0;
|
||||
Uint64 snapParamsSum = 0;
|
||||
// Per-entry copies of this draw's sampled set (the scratch vectors
|
||||
// will be overwritten by the next full-path draw of ANY program).
|
||||
// Record each resource's layout VALUE for the descriptor-reuse hint;
|
||||
// transitions above updated the resources in place, so this reads the
|
||||
// layouts the descriptors just resolved against.
|
||||
m_sampledLayoutSnapshots.assign(sampledTextures.size(), VK_IMAGE_LAYOUT_UNDEFINED);
|
||||
snap.sampledTextures = sampledTextures;
|
||||
snap.sampledResources = sampledResources;
|
||||
snap.sampledBindingRecords = m_sampledBindingRecordsScratch;
|
||||
snap.sampledLayouts.assign(sampledTextures.size(), VK_IMAGE_LAYOUT_UNDEFINED);
|
||||
for (SizeT i = 0; i < sampledTextures.size(); ++i) {
|
||||
const auto* sampledTexture = sampledTextures[i];
|
||||
if (sampledTexture == nullptr) {
|
||||
@@ -5827,7 +5860,7 @@ void main() {
|
||||
snapContentSum += sampledTexture->GetContentVersion();
|
||||
snapParamsSum += sampledTexture->GetTextureParamsVersion();
|
||||
if (sampledResources[i] != nullptr) {
|
||||
m_sampledLayoutSnapshots[i] = sampledResources[i]->layout;
|
||||
snap.sampledLayouts[i] = sampledResources[i]->layout;
|
||||
}
|
||||
}
|
||||
snap.sampledContentSum = snapContentSum;
|
||||
@@ -9439,13 +9472,75 @@ void main() {
|
||||
|
||||
VkCommandBuffer& commandBuffer = frame.commandBuffer;
|
||||
|
||||
for (Uint32 idraw = 0; idraw < payload.drawCount; ++idraw) {
|
||||
vkCmdDrawIndexed(commandBuffer,
|
||||
payload.pParams[idraw].indexCount,
|
||||
payload.pParams[idraw].instanceCount,
|
||||
payload.pParams[idraw].firstIndex,
|
||||
payload.pParams[idraw].vertexOffset,
|
||||
payload.pParams[idraw].firstInstance);
|
||||
// Collapse contiguous sub-draw runs into one vkCmdDrawIndexed. Per-sub-draw
|
||||
// command emission in the driver dominates a Sodium-shaped multi-draw
|
||||
// (steady-state profile: >60% of the case inside the Vulkan driver's
|
||||
// vkCmdDrawIndexed encoding for 132x32 sub-draws/frame), and a chunk
|
||||
// renderer's sub-draws are runs of adjacent index ranges over one buffer.
|
||||
// Two draws are one iff they concatenate to an identical index stream:
|
||||
// - a LIST topology (points/lines/triangles). Strips/fans/loops would
|
||||
// weld primitives across the seam.
|
||||
// - the accumulated count ends on a primitive boundary, otherwise GL
|
||||
// discards the dangling indices at the sub-draw's end but the merged
|
||||
// stream would assemble them with the next sub-draw's indices.
|
||||
// - primitive restart is off: with restart on, a sentinel mid-stream
|
||||
// resets assembly, so a partial primitive before the seam would
|
||||
// otherwise be discarded per sub-draw (same dangling-index argument).
|
||||
// - identical baseVertex/instancing and firstIndex adjacency, so the
|
||||
// merged range fetches exactly the two sub-draws' indices in order.
|
||||
Uint32 mergeGranularity = 0;
|
||||
switch (payload.mode) {
|
||||
case GL_POINTS: mergeGranularity = 1; break;
|
||||
case GL_LINES: mergeGranularity = 2; break;
|
||||
case GL_TRIANGLES: mergeGranularity = 3; break;
|
||||
default: break;
|
||||
}
|
||||
if (mergeGranularity != 0) {
|
||||
const RenderStateParameters& rsp = MG_State::pGLContext->GetRenderStateParameters();
|
||||
if (rsp.PrimitiveRestartEnabled || rsp.PrimitiveRestartFixedIndexEnabled) {
|
||||
mergeGranularity = 0;
|
||||
}
|
||||
}
|
||||
if (mergeGranularity == 0) {
|
||||
for (Uint32 idraw = 0; idraw < payload.drawCount; ++idraw) {
|
||||
vkCmdDrawIndexed(commandBuffer,
|
||||
payload.pParams[idraw].indexCount,
|
||||
payload.pParams[idraw].instanceCount,
|
||||
payload.pParams[idraw].firstIndex,
|
||||
payload.pParams[idraw].vertexOffset,
|
||||
payload.pParams[idraw].firstInstance);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Uint32 idraw = 0;
|
||||
while (idraw < payload.drawCount) {
|
||||
const DrawIndexedCmdParam& head = payload.pParams[idraw];
|
||||
++idraw;
|
||||
if (head.indexCount == 0) {
|
||||
continue; // draws nothing, contributes nothing to a run
|
||||
}
|
||||
Uint32 mergedIndexCount = head.indexCount;
|
||||
if (head.instanceCount == 1) {
|
||||
while (idraw < payload.drawCount) {
|
||||
const DrawIndexedCmdParam& next = payload.pParams[idraw];
|
||||
if (next.indexCount == 0) {
|
||||
++idraw;
|
||||
continue;
|
||||
}
|
||||
if (mergedIndexCount % mergeGranularity != 0 ||
|
||||
next.instanceCount != 1 ||
|
||||
next.vertexOffset != head.vertexOffset ||
|
||||
next.firstInstance != head.firstInstance ||
|
||||
next.firstIndex != head.firstIndex + mergedIndexCount ||
|
||||
mergedIndexCount + next.indexCount < mergedIndexCount) {
|
||||
break;
|
||||
}
|
||||
mergedIndexCount += next.indexCount;
|
||||
++idraw;
|
||||
}
|
||||
}
|
||||
vkCmdDrawIndexed(commandBuffer, mergedIndexCount, head.instanceCount, head.firstIndex,
|
||||
head.vertexOffset, head.firstInstance);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10105,7 +10200,7 @@ void main() {
|
||||
void VulkanRenderer::OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) {
|
||||
// Dynamic state does not survive a command-buffer boundary.
|
||||
ResetDynamicStateShadow();
|
||||
m_setupDrawSnapshot.valid = false;
|
||||
InvalidateSetupDrawSnapshots();
|
||||
if (m_uniformManager) {
|
||||
m_uniformManager->OnCommandBufferBoundary();
|
||||
}
|
||||
@@ -10271,7 +10366,7 @@ void main() {
|
||||
// A recreated pipeline could reuse a freed handle value and alias
|
||||
// the bind-dedup shadow; force the next draw to re-bind.
|
||||
g_dynamicStateShadow.graphicsPipelineValid = false;
|
||||
m_setupDrawSnapshot.valid = false;
|
||||
InvalidateSetupDrawSnapshots();
|
||||
}
|
||||
m_vertexInputStateFactory->OnFrameBoundary();
|
||||
m_samplerManager->OnFrameBoundary();
|
||||
@@ -11653,7 +11748,7 @@ void main() {
|
||||
}
|
||||
InvalidatePipelineMemo(); // pipelines freed -> the memoized handle would dangle
|
||||
g_dynamicStateShadow.graphicsPipelineValid = false;
|
||||
m_setupDrawSnapshot.valid = false;
|
||||
InvalidateSetupDrawSnapshots();
|
||||
DestroyComputePipelines();
|
||||
if (m_frameContext.GetFrameCount() > 0) {
|
||||
m_frameContext.GetCurrent().isCommandRecording = false;
|
||||
|
||||
@@ -648,11 +648,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 m_pipelineStateHashColorCount = 0;
|
||||
Uint64 m_pipelineStateHash = 0;
|
||||
Bool m_pipelineStateHashValid = false;
|
||||
// GetShaderTransformFlags(preTransform) memo: a pure function of the swapchain
|
||||
// pre-transform, re-evaluated only when that value changes (surface rotation).
|
||||
// No other invalidation input exists.
|
||||
// GetShaderTransformFlags memo. NOT pure in the pre-transform alone: the
|
||||
// function also reads whether the bound DRAW framebuffer is the default one
|
||||
// (only the default framebuffer gets the Y-flip and rotation bits - an FBO
|
||||
// pass renders unflipped). Keyed on BOTH inputs; missing the FBO bit shipped
|
||||
// an upside-down default-framebuffer pass after any render-to-texture
|
||||
// (minecraft-1.17-main-menu retrace, whole frame flipped).
|
||||
VkSurfaceTransformFlagBitsKHR m_baseTransformFlagsPreTransform =
|
||||
VK_SURFACE_TRANSFORM_FLAG_BITS_MAX_ENUM_KHR;
|
||||
Bool m_baseTransformFlagsIsDefaultFbo = false;
|
||||
Bool m_baseTransformFlagsKeyValid = false;
|
||||
Uint32 m_baseTransformFlagsCache = 0;
|
||||
Uint32 GetBaseTransformFlagsRaw();
|
||||
// Drops every memoized pipeline handle. Required at command-buffer
|
||||
@@ -765,8 +770,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// it bypasses GetOrCreateProgram, or the idle sweep could evict a live entry.
|
||||
const ProgramFactory::VkProgramObject* programObj = nullptr;
|
||||
Uint64 programFactoryEpoch = 0;
|
||||
// Per-entry copies of the snapshotting draw's sampled set (the scratch
|
||||
// vectors below hold only the LAST full-path draw's set, which with more
|
||||
// than one snapshot entry is not necessarily this entry's program).
|
||||
// sampledTextures/sampledResources carry the same epoch-guarded pointer
|
||||
// lifetime rules as the scratch originals: textureEraseEpoch (checked
|
||||
// every probe) declines the entry before any erased resource pointer
|
||||
// could be dereferenced. sampledLayouts is the layout VALUE each
|
||||
// resource held when this entry's descriptors were built (the
|
||||
// descriptor-reuse hint needs the SAME layout, not just a sampleable
|
||||
// one), and sampledBindingRecords feeds SampledBindingsUnchanged when
|
||||
// the bind generation moved.
|
||||
Vector<MG_State::GLState::ITextureObject*> sampledTextures;
|
||||
Vector<VkTextureManager::TextureResource*> sampledResources;
|
||||
Vector<VkImageLayout> sampledLayouts;
|
||||
Vector<UniformManager::SampledBindingRecord> sampledBindingRecords;
|
||||
};
|
||||
SetupDrawSnapshot m_setupDrawSnapshot;
|
||||
// Program-keyed snapshot entries: program ping-pong (Sodium switches programs
|
||||
// mid-frame every few draws) would otherwise evict the single snapshot on
|
||||
// every switch and send every draw through the full path. Entries are found
|
||||
// by programLifetimeId (MRU-first probe); every other guard stays per-probe,
|
||||
// so a stale entry declines itself exactly like the old single snapshot did.
|
||||
static constexpr Uint32 kSetupDrawSnapshotCount = 4;
|
||||
SetupDrawSnapshot m_setupDrawSnapshots[kSetupDrawSnapshotCount];
|
||||
Uint32 m_setupDrawSnapshotMru = 0; // last entry that hit or was filled
|
||||
Uint32 m_setupDrawSnapshotVictim = 0; // round-robin fill cursor when all entries are live
|
||||
void InvalidateSetupDrawSnapshots() {
|
||||
for (auto& snapshot : m_setupDrawSnapshots) {
|
||||
snapshot.valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Per-draw scratch buffers (clear keeps capacity) — these paths run for every
|
||||
// draw call and must not allocate.
|
||||
@@ -782,13 +815,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// post-transition loop can skip re-resolving textures whose layout is
|
||||
// already sampleable.
|
||||
Vector<VkTextureManager::TextureResource*> m_sampledResourcesScratch;
|
||||
// Layout VALUE of each sampled resource when the snapshot (and so the cached
|
||||
// sampler descriptors) was built, parallel to m_sampledResourcesScratch. The
|
||||
// fast path's validity check only proves the layout is still sampleable; the
|
||||
// descriptor-reuse hint additionally needs it to be the SAME sampleable
|
||||
// layout (a mid-frame compute dispatch can move a sampled texture from
|
||||
// READ_ONLY_OPTIMAL to GENERAL, both valid, different descriptor).
|
||||
Vector<VkImageLayout> m_sampledLayoutSnapshots;
|
||||
Vector<MG_State::GLState::ITextureObject*> m_storageImageTexturesScratch;
|
||||
Vector<VkBuffer> m_vertexBuffersScratch;
|
||||
Vector<VkDeviceSize> m_vertexOffsetsScratch;
|
||||
|
||||
@@ -11,6 +11,23 @@
|
||||
namespace MobileGL {
|
||||
namespace MG_State {
|
||||
namespace GLState {
|
||||
namespace {
|
||||
// Overlapping OR abutting ([lo, hi) intervals meeting edge-to-edge) in
|
||||
// every axis: merging abutting boxes keeps scanline/tile write patterns
|
||||
// as one rect instead of a picket fence.
|
||||
Bool RegionsTouch(const MipmapDirtyRegion& a, const MipmapDirtyRegion& b) {
|
||||
return a.lo.x() <= b.hi.x() && b.lo.x() <= a.hi.x() && a.lo.y() <= b.hi.y() &&
|
||||
b.lo.y() <= a.hi.y() && a.lo.z() <= b.hi.z() && b.lo.z() <= a.hi.z();
|
||||
}
|
||||
|
||||
MipmapDirtyRegion RegionUnion(const MipmapDirtyRegion& a, const MipmapDirtyRegion& b) {
|
||||
return {IntVec3{std::min(a.lo.x(), b.lo.x()), std::min(a.lo.y(), b.lo.y()),
|
||||
std::min(a.lo.z(), b.lo.z())},
|
||||
IntVec3{std::max(a.hi.x(), b.hi.x()), std::max(a.hi.y(), b.hi.y()),
|
||||
std::max(a.hi.z(), b.hi.z())}};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
SizeT MipmapStorage::GetLevelCount() const {
|
||||
return m_data.size();
|
||||
}
|
||||
@@ -28,6 +45,7 @@ namespace MobileGL {
|
||||
m_texelSizes.resize(requiredLevelCount);
|
||||
m_isDirty.resize(requiredLevelCount, false);
|
||||
m_dirtyRegions.resize(requiredLevelCount);
|
||||
m_dirtyRects.resize(requiredLevelCount);
|
||||
m_compressedData.resize(requiredLevelCount);
|
||||
m_compressedFormats.resize(requiredLevelCount, GL_NONE);
|
||||
}
|
||||
@@ -44,6 +62,11 @@ namespace MobileGL {
|
||||
std::max(input.texelSize.z(), 1)}}
|
||||
: MipmapDirtyRegion{};
|
||||
}
|
||||
// The rect list mirrors the union box's reset: whatever rects were
|
||||
// pending measured the OLD extents. Empty list = union box tells all.
|
||||
if (level < m_dirtyRects.size()) {
|
||||
m_dirtyRects[level].clear();
|
||||
}
|
||||
auto& data = m_data[level];
|
||||
data.resize(input.byteSize, 0);
|
||||
|
||||
@@ -94,6 +117,7 @@ namespace MobileGL {
|
||||
m_texelSizes.resize(levelCount);
|
||||
m_isDirty.resize(levelCount);
|
||||
m_dirtyRegions.resize(levelCount);
|
||||
m_dirtyRects.resize(levelCount);
|
||||
m_compressedData.resize(levelCount);
|
||||
m_compressedFormats.resize(levelCount);
|
||||
}
|
||||
@@ -142,6 +166,13 @@ namespace MobileGL {
|
||||
m_dirtyRegions[level] = {};
|
||||
}
|
||||
}
|
||||
// Both directions collapse the rect list to "just the union box": a
|
||||
// whole-level dirty IS the union box, a clean level has nothing to say.
|
||||
// clear() keeps the vector's capacity, so per-frame streaming levels
|
||||
// allocate their slots once and reuse them.
|
||||
if (level < m_dirtyRects.size()) {
|
||||
m_dirtyRects[level].clear();
|
||||
}
|
||||
}
|
||||
|
||||
bool MipmapStorage::IsDirty(Uint level) const {
|
||||
@@ -158,6 +189,20 @@ namespace MobileGL {
|
||||
std::min(offset.y() + size.y(), levelSize.y()),
|
||||
std::min(offset.z() + std::max(size.z(), 1), std::max(levelSize.z(), 1))};
|
||||
if (incoming.Empty()) return;
|
||||
// Rect list first, while the union box still holds only the PREVIOUS
|
||||
// writes: a level that is already dirty with an empty list is in the
|
||||
// "union box tells all" resting state, so that box seeds the list
|
||||
// before the incoming rect refines it.
|
||||
if (level < m_dirtyRects.size()) {
|
||||
auto& rects = m_dirtyRects[level];
|
||||
if (!m_isDirty[level]) {
|
||||
rects.clear(); // stale-safety; MarkDirty(false) already cleared it
|
||||
} else if (rects.empty() && level < m_dirtyRegions.size() &&
|
||||
!m_dirtyRegions[level].Empty()) {
|
||||
rects.push_back(m_dirtyRegions[level]);
|
||||
}
|
||||
InsertDirtyRect(level, incoming);
|
||||
}
|
||||
if (level < m_dirtyRegions.size()) {
|
||||
MipmapDirtyRegion& region = m_dirtyRegions[level];
|
||||
if (m_isDirty[level] && !region.Empty()) {
|
||||
@@ -174,10 +219,82 @@ namespace MobileGL {
|
||||
m_isDirty[level] = true;
|
||||
}
|
||||
|
||||
void MipmapStorage::InsertDirtyRect(Uint level, MipmapDirtyRegion incoming) {
|
||||
auto& rects = m_dirtyRects[level];
|
||||
if (rects.capacity() < kMaxDirtyRects) {
|
||||
rects.reserve(kMaxDirtyRects);
|
||||
}
|
||||
// Cascade-merge: absorb every rect the incoming touches. The absorbed
|
||||
// union can reach rects a smaller box did not, so rescan until stable;
|
||||
// every merge shrinks the list, so this terminates. Swap-with-back keeps
|
||||
// removal O(1) - the list is unordered by design.
|
||||
Bool merged = true;
|
||||
while (merged) {
|
||||
merged = false;
|
||||
for (SizeT i = 0; i < rects.size(); ++i) {
|
||||
if (RegionsTouch(rects[i], incoming)) {
|
||||
incoming = RegionUnion(rects[i], incoming);
|
||||
rects[i] = rects.back();
|
||||
rects.pop_back();
|
||||
merged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rects.size() < kMaxDirtyRects) {
|
||||
rects.push_back(incoming);
|
||||
return;
|
||||
}
|
||||
// Full: fold the incoming rect into the neighbour whose box grows least
|
||||
// (least new area dragged into the upload), then re-insert the grown
|
||||
// box - it may now touch others. The removal above guarantees the
|
||||
// recursion appends on the second pass at the latest.
|
||||
SizeT best = 0;
|
||||
SizeT bestGrowth = ~static_cast<SizeT>(0);
|
||||
for (SizeT i = 0; i < rects.size(); ++i) {
|
||||
const SizeT growth = RegionUnion(rects[i], incoming).TexelCount() - rects[i].TexelCount();
|
||||
if (growth < bestGrowth) {
|
||||
bestGrowth = growth;
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
incoming = RegionUnion(rects[best], incoming);
|
||||
rects[best] = rects.back();
|
||||
rects.pop_back();
|
||||
InsertDirtyRect(level, incoming);
|
||||
}
|
||||
|
||||
MipmapDirtyRegion MipmapStorage::GetDirtyRegion(Uint level) const {
|
||||
if (level >= m_dirtyRegions.size()) return {};
|
||||
return m_dirtyRegions[level];
|
||||
}
|
||||
|
||||
SizeT MipmapStorage::GetDirtyRects(Uint level, MipmapDirtyRegion* outRects, SizeT maxRects) const {
|
||||
if (outRects == nullptr || level >= m_dirtyRects.size() || level >= m_dirtyRegions.size()) {
|
||||
return 0;
|
||||
}
|
||||
const auto& rects = m_dirtyRects[level];
|
||||
// 0 or 1 rects: the union box already says exactly this. More than the
|
||||
// caller can take: never truncate - a dropped rect is a dropped write.
|
||||
if (rects.size() < 2 || rects.size() > maxRects) {
|
||||
return 0;
|
||||
}
|
||||
// Total-bytes accounting: when the scattered rects add up to most of
|
||||
// the union box anyway (>= 3/4), one driver call on the box beats many
|
||||
// calls moving nearly the same bytes.
|
||||
SizeT summedArea = 0;
|
||||
for (const auto& rect : rects) {
|
||||
summedArea += rect.TexelCount();
|
||||
}
|
||||
const SizeT unionArea = m_dirtyRegions[level].TexelCount();
|
||||
if (summedArea * 4 >= unionArea * 3) {
|
||||
return 0;
|
||||
}
|
||||
for (SizeT i = 0; i < rects.size(); ++i) {
|
||||
outRects[i] = rects[i];
|
||||
}
|
||||
return rects.size();
|
||||
}
|
||||
} // namespace GLState
|
||||
} // namespace MG_State
|
||||
} // namespace MobileGL
|
||||
|
||||
@@ -31,6 +31,11 @@ namespace MobileGL {
|
||||
return lo.x() <= 0 && lo.y() <= 0 && lo.z() <= 0 && hi.x() >= levelSize.x() &&
|
||||
hi.y() >= levelSize.y() && hi.z() >= std::max(levelSize.z(), 1);
|
||||
}
|
||||
SizeT TexelCount() const {
|
||||
if (Empty()) return 0;
|
||||
return static_cast<SizeT>(hi.x() - lo.x()) * static_cast<SizeT>(hi.y() - lo.y()) *
|
||||
static_cast<SizeT>(hi.z() - lo.z());
|
||||
}
|
||||
};
|
||||
|
||||
class MipmapStorage {
|
||||
@@ -54,6 +59,28 @@ namespace MobileGL {
|
||||
// Meaningful only while IsDirty(level).
|
||||
MipmapDirtyRegion GetDirtyRegion(Uint level) const;
|
||||
|
||||
// Behind the union box, the level keeps up to kMaxDirtyRects pairwise
|
||||
// disjoint rects recording WHERE the writes actually landed. A frame of
|
||||
// ~100 scattered sprite updates in a big atlas has a union box that
|
||||
// covers nearly the whole level while the touched texels are ~5% of it;
|
||||
// the union box stays the source of truth (every write funnels through
|
||||
// MarkDirty/MarkDirtyRegion into BOTH representations), backends OPT IN
|
||||
// to the list purely as an upload-size refinement. 96 slots because the
|
||||
// pattern this exists for is Minecraft's ~100 sprites/frame: a 16-slot
|
||||
// list forced into far-apart merges was measured at >90% of the union
|
||||
// box's area on exactly that pattern, i.e. worthless. Inserts merge any
|
||||
// touching/overlapping rect (cascading, so the list stays disjoint);
|
||||
// when full, the incoming rect folds into the neighbour whose box grows
|
||||
// least and the list degrades gracefully toward the union box.
|
||||
static constexpr SizeT kMaxDirtyRects = 96;
|
||||
// Copies the level's dirty rects into outRects and returns how many were
|
||||
// written. 0 means "upload the union box instead" and covers every
|
||||
// reason at once: tracking unavailable, a single rect (identical to the
|
||||
// union box by construction), more rects than maxRects, or a summed
|
||||
// area so close to the union box's that one big upload beats many small
|
||||
// ones (fewer driver calls wins when the bytes are nearly equal).
|
||||
SizeT GetDirtyRects(Uint level, MipmapDirtyRegion* outRects, SizeT maxRects) const;
|
||||
|
||||
// The bytes an application handed to glCompressedTexImage*, kept verbatim beside the
|
||||
// (uncompressed) texel shadow rather than in place of it. GL 4.6 core 8.11 requires
|
||||
// glGetCompressedTexImage to return the image *as stored*, and no backend here has a
|
||||
@@ -70,10 +97,22 @@ namespace MobileGL {
|
||||
const void* MapCompressedData(Uint level) const;
|
||||
|
||||
protected:
|
||||
// Insert one clamped, non-empty write box, keeping the list disjoint
|
||||
// and bounded (see kMaxDirtyRects).
|
||||
void InsertDirtyRect(Uint level, MipmapDirtyRegion incoming);
|
||||
|
||||
Vector<IntVec3> m_texelSizes;
|
||||
Vector<Vector<Uint8>> m_data;
|
||||
Vector<bool> m_isDirty;
|
||||
Vector<MipmapDirtyRegion> m_dirtyRegions;
|
||||
// Per level, the disjoint rect list behind m_dirtyRegions' union box.
|
||||
// An EMPTY list is the common resting state and always means "the union
|
||||
// box is the whole story" - clean levels, whole-level dirties and
|
||||
// respecifies all just clear it, so plain full-level uploads never pay
|
||||
// a heap allocation; the first scattered MarkDirtyRegion on an
|
||||
// already-dirty level seeds the list from the union box accumulated so
|
||||
// far and refines from there.
|
||||
Vector<Vector<MipmapDirtyRegion>> m_dirtyRects;
|
||||
Vector<Vector<Uint8>> m_compressedData;
|
||||
Vector<GLenum> m_compressedFormats;
|
||||
};
|
||||
|
||||
@@ -84,6 +84,12 @@ namespace MobileGL {
|
||||
return m_storage[targetIndex].GetDirtyRegion(level);
|
||||
}
|
||||
|
||||
SizeT GetDirtyRects(Uint targetIndex, Uint level, MipmapDirtyRegion* outRects,
|
||||
SizeT maxRects) const {
|
||||
MOBILEGL_ASSERT(targetIndex < TargetCount, "GetDirtyRects: target invalid");
|
||||
return m_storage[targetIndex].GetDirtyRects(level, outRects, maxRects);
|
||||
}
|
||||
|
||||
void SetCompressedImage(Uint targetIndex, Uint level, GLenum internalFormat, const void* data,
|
||||
SizeT size) {
|
||||
MOBILEGL_ASSERT(targetIndex < TargetCount, "SetCompressedImage: target invalid");
|
||||
|
||||
@@ -345,6 +345,13 @@ namespace MobileGL {
|
||||
return m_textureStorage.GetDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
|
||||
}
|
||||
|
||||
SizeT TextureObjectWithOneMipmap::GetStorageDirtyRects(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
MipmapDirtyRegion* outRects,
|
||||
SizeT maxRects) const {
|
||||
return m_textureStorage.GetDirtyRects(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
|
||||
outRects, maxRects);
|
||||
}
|
||||
|
||||
void TextureObjectWithOneMipmap::SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
GLenum internalFormat, const void* data, SizeT size) {
|
||||
m_textureStorage.SetCompressedImage(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
|
||||
|
||||
@@ -186,6 +186,20 @@ namespace MobileGL::MG_State::GLState {
|
||||
const IntVec3 size = GetMipmapTexelSize(uploadTarget, mipmapLevel);
|
||||
return {IntVec3{0, 0, 0}, IntVec3{size.x(), size.y(), std::max(size.z(), 1)}};
|
||||
}
|
||||
// Scatter detail behind GetStorageDirtyRegion: up to maxRects disjoint rects
|
||||
// that together cover every dirty texel, so ~100 sprite writes in a big atlas
|
||||
// need not be uploaded as one atlas-sized box. Returns how many rects were
|
||||
// written to outRects; 0 means "no list, upload the union box" and is always a
|
||||
// safe answer - this base fallback keeps whole-level semantics for storage
|
||||
// classes that do not track rects, and backends OPT IN by calling this.
|
||||
virtual SizeT GetStorageDirtyRects(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
MipmapDirtyRegion* outRects, SizeT maxRects) const {
|
||||
(void)uploadTarget;
|
||||
(void)mipmapLevel;
|
||||
(void)outRects;
|
||||
(void)maxRects;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The compressed image a glCompressedTexImage* call shadowed for this level, kept verbatim
|
||||
// next to the texel data rather than instead of it - see MipmapStorage. The texel shadow
|
||||
@@ -257,6 +271,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
void MarkStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel, IntVec3 offset,
|
||||
IntVec3 size) override;
|
||||
MipmapDirtyRegion GetStorageDirtyRegion(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
SizeT GetStorageDirtyRects(TextureUploadTarget uploadTarget, Uint mipmapLevel, MipmapDirtyRegion* outRects,
|
||||
SizeT maxRects) const override;
|
||||
void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel, GLenum internalFormat,
|
||||
const void* data, SizeT size) override;
|
||||
GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
|
||||
@@ -69,6 +69,12 @@ namespace MobileGL {
|
||||
return m_textureStorage.GetDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel);
|
||||
}
|
||||
|
||||
SizeT TextureObject2DCube::GetStorageDirtyRects(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
MipmapDirtyRegion* outRects, SizeT maxRects) const {
|
||||
return m_textureStorage.GetDirtyRects(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
|
||||
outRects, maxRects);
|
||||
}
|
||||
|
||||
void TextureObject2DCube::SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
GLenum internalFormat, const void* data, SizeT size) {
|
||||
m_textureStorage.SetCompressedImage(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel,
|
||||
|
||||
@@ -31,6 +31,8 @@ namespace MobileGL {
|
||||
IntVec3 size) override;
|
||||
MipmapDirtyRegion GetStorageDirtyRegion(TextureUploadTarget uploadTarget,
|
||||
Uint mipmapLevel) const override;
|
||||
SizeT GetStorageDirtyRects(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
MipmapDirtyRegion* outRects, SizeT maxRects) const override;
|
||||
void SetMipmapCompressedImage(TextureUploadTarget uploadTarget, Uint mipmapLevel,
|
||||
GLenum internalFormat, const void* data, SizeT size) override;
|
||||
GLenum GetMipmapCompressedFormat(TextureUploadTarget uploadTarget, Uint mipmapLevel) const override;
|
||||
|
||||
Reference in New Issue
Block a user