mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 10:03:23 +09:00
[Fix] (Magma): an upload into an image the open recording already wrote, or that still has a queued clear, is submitted after that work - a glTexSubImage after a clear or a draw into the level no longer ends up under it
This commit is contained in:
@@ -1293,6 +1293,42 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
VkTextureManager::TextureResource* VkTextureManager::FindTextureResource(
|
||||
MG_State::GLState::ITextureObject& textureOrView) {
|
||||
auto it = m_textureResources.find(MakeTextureIdentity(&StorageTextureOf(textureOrView)));
|
||||
return it != m_textureResources.end() ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
Bool VkTextureManager::HasLiveImage(MG_State::GLState::ITextureObject& textureOrView) const {
|
||||
auto it = m_textureResources.find(MakeTextureIdentity(&StorageTextureOf(textureOrView)));
|
||||
return it != m_textureResources.end() && it->second.image != VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
Bool VkTextureManager::HasPendingTexelUpload(MG_State::GLState::ITextureObject& textureOrView) const {
|
||||
auto& storage = StorageTextureOf(textureOrView);
|
||||
auto it = m_textureResources.find(MakeTextureIdentity(&storage));
|
||||
if (it == m_textureResources.end() || it->second.image == VK_NULL_HANDLE) {
|
||||
return false;
|
||||
}
|
||||
return it->second.syncedShapeVersion == storage.GetShapeVersion() &&
|
||||
it->second.syncedContentVersion != storage.GetContentVersion();
|
||||
}
|
||||
|
||||
Bool VkTextureManager::WasTextureWrittenThisRecording(MG_State::GLState::ITextureObject& textureOrView) const {
|
||||
auto it = m_textureResources.find(MakeTextureIdentity(&StorageTextureOf(textureOrView)));
|
||||
return it != m_textureResources.end() && it->second.lastRecordingWriteGeneration == m_recordingGeneration;
|
||||
}
|
||||
|
||||
void VkTextureManager::StampTextureRecordingWrite(MG_State::GLState::ITextureObject* texture) {
|
||||
if (texture == nullptr) {
|
||||
return;
|
||||
}
|
||||
auto it = m_textureResources.find(MakeTextureIdentity(&StorageTextureOf(*texture)));
|
||||
if (it != m_textureResources.end()) {
|
||||
StampResourceRecordingWrite(it->second);
|
||||
}
|
||||
}
|
||||
|
||||
void VkTextureManager::UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout) {
|
||||
MOBILEGL_ASSERT(texture != nullptr, "UpdateTrackedImageLayout: texture is null");
|
||||
auto it = m_textureResources.find(MakeTextureIdentity(texture));
|
||||
@@ -1321,7 +1357,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
"UpdateTrackedImageLayoutAfterAttachmentWrite: textureId=%d mipLevel=%u out of range %u",
|
||||
texture->GetExternalIndex(), writtenMipLevel, resource.mipLevels);
|
||||
// Pre-pass stream bookkeeping: the render pass that just ended wrote this image.
|
||||
StampResourceRecordingUse(resource);
|
||||
StampResourceRecordingWrite(resource);
|
||||
|
||||
if (resource.layout != newLayout && resource.mipLevels > 1) {
|
||||
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
@@ -1435,8 +1471,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource->aspect, 0, resource->mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "TransitionTextureForStorageImage: transition failed for textureId=%d",
|
||||
texture.GetExternalIndex());
|
||||
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
||||
StampResourceRecordingUse(*resource);
|
||||
// Pre-pass stream bookkeeping: a command referencing the image was recorded, and an
|
||||
// image unit may store to it.
|
||||
StampResourceRecordingWrite(*resource);
|
||||
return ok;
|
||||
}
|
||||
|
||||
|
||||
@@ -320,6 +320,11 @@ public:
|
||||
// into the frame's PRE command buffer - which executes strictly before the
|
||||
// frame's commands - instead of splitting the active render pass.
|
||||
Uint64 lastRecordingGeneration = 0;
|
||||
// The same, for the last recorded command that WRITES the image (a render pass it is an
|
||||
// attachment of, a clear, a blit/copy into it, a mip generation, a storage-image binding).
|
||||
// An upload into an image written this recording has to be ordered after that write
|
||||
// (VulkanRenderer::OrderPendingUploadAfterRecording).
|
||||
Uint64 lastRecordingWriteGeneration = 0;
|
||||
// Snapshot of ITextureObject::GetContentVersion() at the last successful sync;
|
||||
// lets SyncTexture skip the whole re-check/re-upload when content is unchanged.
|
||||
Uint64 syncedContentVersion = 0;
|
||||
@@ -362,6 +367,7 @@ public:
|
||||
std::swap(this->storageUsageResolved, that.storageUsageResolved);
|
||||
std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion);
|
||||
std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration);
|
||||
std::swap(this->lastRecordingWriteGeneration, that.lastRecordingWriteGeneration);
|
||||
std::swap(this->syncedContentVersion, that.syncedContentVersion);
|
||||
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
|
||||
std::swap(this->syncedShapeVersion, that.syncedShapeVersion);
|
||||
@@ -551,6 +557,26 @@ public:
|
||||
Bool WasTouchedThisRecording(const TextureResource& resource) const {
|
||||
return resource.lastRecordingGeneration == m_recordingGeneration;
|
||||
}
|
||||
// A write is a use too. See TextureResource::lastRecordingWriteGeneration.
|
||||
void StampResourceRecordingWrite(TextureResource& resource) const {
|
||||
resource.lastRecordingGeneration = m_recordingGeneration;
|
||||
resource.lastRecordingWriteGeneration = m_recordingGeneration;
|
||||
}
|
||||
void StampTextureRecordingWrite(MG_State::GLState::ITextureObject* texture);
|
||||
// Upload-ordering queries (VulkanRenderer::OrderPendingUploadAfterRecording). None of them
|
||||
// syncs, so nothing pending is uploaded by asking. All answer for the STORAGE texture.
|
||||
//
|
||||
// Uploads are recorded into a batch submitted AHEAD of the frame's recording
|
||||
// (FlushPendingUploads), so an upload into an image the open recording already WROTE lands
|
||||
// before that write, which then overwrites the newer texels. (An image the recording only
|
||||
// read keeps the old trade: its earlier reads see the newer texels.)
|
||||
Bool HasLiveImage(MG_State::GLState::ITextureObject& textureOrView) const;
|
||||
// The next sync would record client texels into the image that is live now: same shape (a
|
||||
// shape change mints a new image instead) and content moved since the last sync.
|
||||
Bool HasPendingTexelUpload(MG_State::GLState::ITextureObject& textureOrView) const;
|
||||
Bool WasTextureWrittenThisRecording(MG_State::GLState::ITextureObject& textureOrView) const;
|
||||
// The live resource, or null.
|
||||
TextureResource* FindTextureResource(MG_State::GLState::ITextureObject& textureOrView);
|
||||
// Records that this texture is bound to a GL image unit, so its image must carry
|
||||
// VK_IMAGE_USAGE_STORAGE_BIT. Must be called before NeedsStorageImagePreparation, and
|
||||
// therefore before the render pass is committed: an image that has to be upgraded is
|
||||
|
||||
@@ -1801,6 +1801,9 @@ void main() {
|
||||
outBinding.label, texture->GetExternalIndex());
|
||||
return false;
|
||||
}
|
||||
if (!isReadFramebuffer) {
|
||||
textureManager.StampResourceRecordingWrite(*resource);
|
||||
}
|
||||
|
||||
outBinding.image = resource->image;
|
||||
outBinding.trackedLayout = &resource->layout;
|
||||
@@ -1917,6 +1920,9 @@ void main() {
|
||||
outBinding.label, texture->GetExternalIndex(), static_cast<Uint32>(requiredAspectMask));
|
||||
return false;
|
||||
}
|
||||
if (!isReadFramebuffer) {
|
||||
textureManager.StampResourceRecordingWrite(*resource);
|
||||
}
|
||||
|
||||
outBinding.image = resource->image;
|
||||
outBinding.trackedLayout = &resource->layout;
|
||||
@@ -1957,6 +1963,7 @@ void main() {
|
||||
texture.GetExternalIndex(), mipLevel, resource->mipLevels);
|
||||
return false;
|
||||
}
|
||||
textureManager.StampResourceRecordingWrite(*resource);
|
||||
|
||||
outBinding.image = resource->image;
|
||||
outBinding.trackedLayout = &resource->layout;
|
||||
@@ -6651,6 +6658,15 @@ void main() {
|
||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
activeRenderPass = nullptr;
|
||||
}
|
||||
// The syncs below upload each sampled texture's pending texels into a batch that runs
|
||||
// ahead of this recording; one whose image the recording already cleared or drew into
|
||||
// would be overwritten by that earlier work. Submit it first, here, before anything below
|
||||
// records into the frame or decides from the active pass.
|
||||
for (auto* sampledTexture : sampledTextures) {
|
||||
if (sampledTexture != nullptr && OrderPendingUploadAfterRecording(frame, *sampledTexture)) {
|
||||
activeRenderPass = nullptr;
|
||||
}
|
||||
}
|
||||
Bool needSampledTextureTransitions = false;
|
||||
auto& sampledResources = m_sampledResourcesScratch;
|
||||
sampledResources.assign(sampledTextures.size(), nullptr);
|
||||
@@ -7349,6 +7365,7 @@ void main() {
|
||||
m_renderPassManager->QueueRenderbufferClear(
|
||||
{.mask = GL_COLOR_BUFFER_BIT, .color = payload.color}, attachment);
|
||||
} else if (attachment.IsTexture()) {
|
||||
SettleTexelsBeforeQueuedClear(*fbo, attachment);
|
||||
m_clearManager->QueueClear({.mask = GL_COLOR_BUFFER_BIT, .color = payload.color},
|
||||
attachment);
|
||||
}
|
||||
@@ -7360,6 +7377,19 @@ void main() {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((deferredMask & GL_COLOR_BUFFER_BIT) != 0) {
|
||||
for (const auto drawBuffer : fbo->GetDrawBuffers()) {
|
||||
if (drawBuffer != FramebufferAttachmentType::None) {
|
||||
SettleTexelsBeforeQueuedClear(*fbo, fbo->GetAttachment(drawBuffer));
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((deferredMask & GL_DEPTH_BUFFER_BIT) != 0) {
|
||||
SettleTexelsBeforeQueuedClear(*fbo, fbo->GetAttachment(FramebufferAttachmentType::Depth));
|
||||
}
|
||||
if ((deferredMask & GL_STENCIL_BUFFER_BIT) != 0) {
|
||||
SettleTexelsBeforeQueuedClear(*fbo, fbo->GetAttachment(FramebufferAttachmentType::Stencil));
|
||||
}
|
||||
m_clearManager->QueueClear(deferredMask, payload, *fbo);
|
||||
m_renderPassManager->QueueRenderbufferClear(deferredMask, payload, *fbo);
|
||||
}
|
||||
@@ -7437,6 +7467,7 @@ void main() {
|
||||
if (!attachment.IsTexture()) {
|
||||
return;
|
||||
}
|
||||
SettleTexelsBeforeQueuedClear(framebuffer, attachment);
|
||||
m_clearManager->QueueClear(payload, attachment);
|
||||
};
|
||||
|
||||
@@ -7795,7 +7826,8 @@ void main() {
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::ITextureObject& texture) {
|
||||
MG_State::GLState::ITextureObject& texture,
|
||||
Bool uploadPendingTexels) {
|
||||
Vector<PendingClearEntry> pendingClears;
|
||||
if (!m_clearManager->GetPendingClears(&texture, pendingClears)) {
|
||||
return true;
|
||||
@@ -7807,8 +7839,9 @@ void main() {
|
||||
commandBuffer != m_frameContext.GetCurrent().commandBuffer,
|
||||
"MaterializePendingClearForTexture requires no active render pass on the target buffer");
|
||||
|
||||
auto* resource = m_textureManager->SyncTextureAndGetDescriptor(texture);
|
||||
if (resource == nullptr) {
|
||||
auto* resource = uploadPendingTexels ? m_textureManager->SyncTextureAndGetDescriptor(texture)
|
||||
: m_textureManager->FindTextureResource(texture);
|
||||
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
|
||||
// Declined sync (an incomplete texture, say). Nothing to clear into, and every line
|
||||
// below dereferences this - the assert that used to stand here is compiled out of
|
||||
// every build past DEBUG.
|
||||
@@ -7960,6 +7993,9 @@ void main() {
|
||||
"MaterializePendingClearForTexture: failed to transition textureId=%d to sampled layout",
|
||||
texture.GetExternalIndex());
|
||||
resource->layout = sampledLayout;
|
||||
// The clear is now a write in this recording, which is what orders a later upload into
|
||||
// the image after it (OrderPendingUploadAfterRecording).
|
||||
m_textureManager->StampResourceRecordingWrite(*resource);
|
||||
|
||||
m_clearManager->PopPendingClear(&texture);
|
||||
MGLOG_D("MaterializePendingClearForTexture: textureId=%d pending clear materialized",
|
||||
@@ -7967,6 +8003,62 @@ void main() {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::OrderPendingUploadAfterRecording(FrameContext::FrameData& frame,
|
||||
MG_State::GLState::ITextureObject& texture) {
|
||||
if (!m_textureManager->HasPendingTexelUpload(texture)) {
|
||||
return false;
|
||||
}
|
||||
// An open pass drawing into the image stamps that write only when it ends.
|
||||
if (const auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
|
||||
activeRenderPass != nullptr && ActiveRenderPassUsesTexture(*activeRenderPass, texture)) {
|
||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
}
|
||||
const Bool clearFirst = m_clearManager->HasPendingClear(&texture);
|
||||
if (!clearFirst && !m_textureManager->WasTextureWrittenThisRecording(texture)) {
|
||||
// Nothing recorded writes the image: the upload may run ahead of the recording.
|
||||
return false;
|
||||
}
|
||||
if (!frame.isCommandRecording) {
|
||||
m_frameContext.BeginCommandRecording();
|
||||
}
|
||||
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
|
||||
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
}
|
||||
if (clearFirst && !MaterializePendingClearForTexture(frame.commandBuffer, texture,
|
||||
/*uploadPendingTexels=*/false)) {
|
||||
MGLOG_E_ONCE("OrderPendingUploadAfterRecording: textureId=%d kept its queued clear; it may land over "
|
||||
"texels written after it",
|
||||
texture.GetExternalIndex());
|
||||
}
|
||||
if (!FlushPendingCommands()) {
|
||||
MGLOG_E_ONCE("OrderPendingUploadAfterRecording: submit failed; textureId=%d's upload may run ahead of "
|
||||
"earlier work on it",
|
||||
texture.GetExternalIndex());
|
||||
return false;
|
||||
}
|
||||
m_frameContext.BeginCommandRecording();
|
||||
// New command buffer: the same reset SetupDraw applies when it begins one.
|
||||
m_lastSampledSetValid = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void VulkanRenderer::SettleTexelsBeforeQueuedClear(
|
||||
const MG_State::GLState::FramebufferObject& framebuffer,
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
// The default framebuffer's attachments are placeholders: its clears land on the
|
||||
// swapchain image (MaterializePendingClearForDefaultFramebuffer) and it has no texels.
|
||||
if (framebuffer.IsDefaultFramebuffer() || !attachment.IsTexture() || attachment.IsRenderbuffer()) {
|
||||
return;
|
||||
}
|
||||
const auto texture = attachment.GetTexture();
|
||||
if (!texture || (m_textureManager->HasLiveImage(*texture) &&
|
||||
!m_textureManager->HasPendingTexelUpload(*texture))) {
|
||||
return;
|
||||
}
|
||||
OrderPendingUploadAfterRecording(m_frameContext.GetCurrent(), *texture);
|
||||
m_textureManager->SyncTextureAndGetDescriptor(*texture);
|
||||
}
|
||||
|
||||
Bool VulkanRenderer::MaterializeMultisamplePendingClear(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::ITextureObject& texture,
|
||||
VkTextureManager::TextureResource& resource,
|
||||
@@ -9667,6 +9759,7 @@ void main() {
|
||||
const Bool dstClearReady = materializeClear(dstEndpoint);
|
||||
MOBILEGL_ASSERT(dstClearReady, "%s: failed to materialize pending clear for destination objectId=%u",
|
||||
__func__, CopyImageEndpointName(dstEndpoint));
|
||||
m_textureManager->StampTextureRecordingWrite(dstEndpoint.Texture.get());
|
||||
|
||||
const VkImageLayout srcOriginalLayout = *srcImage.trackedLayout;
|
||||
const VkImageLayout dstOriginalLayout = *dstImage.trackedLayout;
|
||||
@@ -9875,6 +9968,13 @@ void main() {
|
||||
}
|
||||
|
||||
auto& frame = m_frameContext.GetCurrent();
|
||||
if (!readFbo->IsDefaultFramebuffer()) {
|
||||
// The read syncs the source texture, whose pending texels must land after work already
|
||||
// recorded against it (see OrderPendingUploadAfterRecording).
|
||||
if (const auto readTexture = readFbo->GetAttachment(readFbo->GetReadBuffer()).GetTexture()) {
|
||||
OrderPendingUploadAfterRecording(frame, *readTexture);
|
||||
}
|
||||
}
|
||||
if (!frame.isCommandRecording) {
|
||||
m_frameContext.BeginCommandRecording();
|
||||
}
|
||||
@@ -10667,6 +10767,9 @@ void main() {
|
||||
return;
|
||||
}
|
||||
|
||||
// The sync below uploads pending texels, which must land after work already recorded
|
||||
// against this image (see OrderPendingUploadAfterRecording).
|
||||
OrderPendingUploadAfterRecording(m_frameContext.GetCurrent(), *textureObject);
|
||||
auto* resource = m_textureManager->SyncTextureAndGetDescriptor(*textureObject);
|
||||
// Two shapes end up in the same place, and for the same reason: the GL level being read has
|
||||
// no GPU storage, so UploadDirtyMipLevels never wrote it and the CPU shadow is the ONLY copy
|
||||
@@ -10962,6 +11065,7 @@ void main() {
|
||||
resource = m_textureManager->SyncTextureAndGetDescriptor(*texture);
|
||||
MOBILEGL_ASSERT(resource != nullptr && resource->image != VK_NULL_HANDLE,
|
||||
"GenerateMipmap failed to resync the backend texture after allocating mip storage.");
|
||||
m_textureManager->StampResourceRecordingWrite(*resource);
|
||||
if (resource->layout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||
const VkImageLayout finalLayout = ResolveGenerateMipmapFinalLayout(resource->aspect);
|
||||
Bool transitioned = VkTextureManager::TransitionImageLayout(
|
||||
|
||||
@@ -1376,8 +1376,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
|
||||
Uint32 depthSlice, const VkClearValue& clearValue,
|
||||
VkImageLayout finalLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
|
||||
// Records the texture's queued clears. By default the texture is synced first, which
|
||||
// uploads any pending texels BEFORE the clear - right only for texels written before it.
|
||||
// `uploadPendingTexels = false` leaves them pending (OrderPendingUploadAfterRecording).
|
||||
Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
|
||||
MG_State::GLState::ITextureObject& texture);
|
||||
MG_State::GLState::ITextureObject& texture,
|
||||
Bool uploadPendingTexels = true);
|
||||
// Texture uploads run in a batch submitted ahead of the frame's recording, so the upload
|
||||
// the next sync of `texture` records would land BEFORE a write already recorded into the
|
||||
// same image (a render, clear, blit or copy), and before a clear still queued for it (such
|
||||
// a clear predates the texels: SettleTexelsBeforeQueuedClear). When either holds, this
|
||||
// records the queued clear without the texels and submits the recording, so the upload
|
||||
// follows both. An image the recording only READ keeps the old trade: no submit, and its
|
||||
// earlier reads see the newer texels. It may end the render pass and replace
|
||||
// frame.commandBuffer (recording again on return), so call it only where the caller holds
|
||||
// no render-pass or command-buffer state across it. Returns whether it submitted.
|
||||
Bool OrderPendingUploadAfterRecording(FrameContext::FrameData& frame,
|
||||
MG_State::GLState::ITextureObject& texture);
|
||||
// Called for each texture a glClear/glClearBuffer* is about to queue a clear on: uploads
|
||||
// whatever texels are still pending (and mints the image if there is none), so that any
|
||||
// texel pending when the clear is materialized was written after it.
|
||||
void SettleTexelsBeforeQueuedClear(const MG_State::GLState::FramebufferObject& framebuffer,
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment);
|
||||
// The multisample arm of the above. Split out rather than branched inline because it
|
||||
// shares none of the transfer path: a multisample image carries no TRANSFER_DST usage, so
|
||||
// neither the TRANSFER_DST transition nor vkCmdClearColorImage is legal on one.
|
||||
|
||||
@@ -89,6 +89,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/NonCoreImageFormatScenario.cpp
|
||||
Scenarios/ImageSizeAfterRespecScenario.cpp
|
||||
Scenarios/ImageBindableRemintScenario.cpp
|
||||
Scenarios/PartialUploadOverGpuContentScenario.cpp
|
||||
Scenarios/SsboDeclarationFormScenario.cpp
|
||||
Scenarios/Glsl420DeclarationScenario.cpp
|
||||
Scenarios/IoBlockNameCollisionScenario.cpp
|
||||
|
||||
@@ -0,0 +1,428 @@
|
||||
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PartialUploadOverGpuContentScenario.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
//
|
||||
// Scenario - A SMALL glTexSubImage INTO A LEVEL THE GPU HAS WRITTEN.
|
||||
//
|
||||
// Espryt keeps a CPU shadow of every level and syncs a pending client write by uploading the
|
||||
// region it covers (SyncMipmapsToBackend's sub-rect path). The shadow never sees a GPU write, so
|
||||
// only the region may go up: anything more puts the shadow's stale texels over what the GPU drew.
|
||||
// The region upload used to be skipped whenever the shadow's bytes had to be CONVERTED on the way
|
||||
// to the driver - a three-channel format the driver cannot render is stored four-channel and every
|
||||
// upload is repacked - and whenever the ES upload is shaped differently from the shadow (a 1D
|
||||
// array, stored as a 2D array one texel high): those levels went up WHOLE, and a 2x2 write erased
|
||||
// the rest of the level.
|
||||
//
|
||||
// Every case uploads a level, lets the GPU clear it, writes one 2x2 box, and reads the level back:
|
||||
// the clear outside the box, the write inside. The formats are picked by what they cost the ES
|
||||
// upload, which is per driver - on Mesa llvmpipe GL_SRGB8, GL_RGB8_SNORM and GL_RGB8UI are stored
|
||||
// four-channel (see ThreeChannelAttachmentScenario) while GL_RGB16F is native; ES has no renderable
|
||||
// three-channel format at all, so on a phone all four are converted. GL_RGBA8 is the control that
|
||||
// never converts. A format a backend cannot attach is skipped, not failed.
|
||||
//
|
||||
// The second half is about ORDER, with GL_RGBA8. Magma queues a clear until the texture's next
|
||||
// use and records texel uploads into a batch submitted ahead of the frame's recorded commands, so
|
||||
// a write issued after a clear of (or a draw into) the level reached the image FIRST and the
|
||||
// earlier clear or draw landed over it: the box read back green. Each order is covered - the
|
||||
// clear already recorded, still queued, synced by a draw or by the readback, the reverse order
|
||||
// in which the clear must win, and a draw instead of a clear.
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Harness/HeadlessGL.h"
|
||||
#include "../Harness/ScenarioFixture.h"
|
||||
|
||||
#ifdef GLAPI
|
||||
#undef GLAPI
|
||||
#endif
|
||||
#define GL_GLEXT_PROTOTYPES
|
||||
#include <GL/gl.h>
|
||||
#include <GL/glcorearb.h>
|
||||
#undef GL_GLEXT_PROTOTYPES
|
||||
|
||||
namespace MGITest {
|
||||
namespace {
|
||||
constexpr int kEdge = 8;
|
||||
constexpr int kBoxAt = 2;
|
||||
constexpr int kBoxEdge = 2;
|
||||
|
||||
constexpr const char* kVS = R"(#version 430 core
|
||||
void main() {
|
||||
vec2 positions[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
||||
gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
|
||||
}
|
||||
)";
|
||||
|
||||
// How a value in [0, 1] is spelled in the client data, and how the level is cleared and
|
||||
// read back. Integer levels spell 1 as kIntegerOne.
|
||||
enum class Encoding { Unorm8, Snorm8, Float32, Uint8 };
|
||||
constexpr unsigned kIntegerOne = 9;
|
||||
|
||||
struct Format {
|
||||
GLenum internalFormat;
|
||||
GLenum clientFormat;
|
||||
GLenum clientType;
|
||||
int components;
|
||||
Encoding encoding;
|
||||
};
|
||||
|
||||
std::vector<std::uint8_t> Fill(const Format& f, int texels, float r, float g, float b) {
|
||||
const float rgba[4] = {r, g, b, 1.0f};
|
||||
const std::size_t componentBytes = f.encoding == Encoding::Float32 ? 4u : 1u;
|
||||
std::vector<std::uint8_t> out(static_cast<std::size_t>(texels) * f.components * componentBytes);
|
||||
for (int t = 0; t < texels; ++t) {
|
||||
for (int c = 0; c < f.components; ++c) {
|
||||
std::uint8_t* dst = out.data() + (static_cast<std::size_t>(t) * f.components + c) * componentBytes;
|
||||
const float v = rgba[c];
|
||||
switch (f.encoding) {
|
||||
case Encoding::Unorm8: *dst = static_cast<std::uint8_t>(std::lround(v * 255.0f)); break;
|
||||
case Encoding::Snorm8:
|
||||
*dst = static_cast<std::uint8_t>(static_cast<std::int8_t>(std::lround(v * 127.0f)));
|
||||
break;
|
||||
case Encoding::Float32: std::memcpy(dst, &v, sizeof(v)); break;
|
||||
case Encoding::Uint8: *dst = static_cast<std::uint8_t>(std::lround(v * kIntegerOne)); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
class PartialUploadOverGpuContentScenario : public ScenarioTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ScenarioTest::SetUp();
|
||||
if (!Ready()) return;
|
||||
m_scratch = MakeColorFbo(4, 4);
|
||||
glGenVertexArrays(1, &m_vao);
|
||||
glGenFramebuffers(1, &m_fbo);
|
||||
FirstGLError();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
if (!Ready()) return;
|
||||
glUseProgram(0);
|
||||
for (GLuint p : m_programs) glDeleteProgram(p);
|
||||
if (!m_textures.empty()) glDeleteTextures(static_cast<GLsizei>(m_textures.size()), m_textures.data());
|
||||
if (m_vao) glDeleteVertexArrays(1, &m_vao);
|
||||
if (m_fbo) glDeleteFramebuffers(1, &m_fbo);
|
||||
BindDefaultFramebuffer();
|
||||
DestroyColorFbo(m_scratch);
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
FirstGLError();
|
||||
}
|
||||
|
||||
GLuint NewTexture(GLenum target) {
|
||||
GLuint t = 0;
|
||||
glGenTextures(1, &t);
|
||||
m_textures.push_back(t);
|
||||
glBindTexture(target, t);
|
||||
glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, 0);
|
||||
glBindTexture(target, 0);
|
||||
return t;
|
||||
}
|
||||
|
||||
// A draw that samples the texture, which is what syncs its pending writes to the
|
||||
// driver (an FBO attach alone may find the framebuffer already synced).
|
||||
void SyncBySampling(GLenum target, GLuint tex, const char* samplerType, const char* coord) {
|
||||
std::string fs = std::string("#version 430 core\nuniform ") + samplerType +
|
||||
" u_tex;\nout vec4 o_color;\nvoid main() { o_color = vec4(texelFetch(u_tex, " + coord +
|
||||
", 0)); }\n";
|
||||
std::string error;
|
||||
const GLuint program = CompileProgram(kVS, fs.c_str(), &error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
m_programs.push_back(program);
|
||||
BindFbo(m_scratch);
|
||||
glUseProgram(program);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(target, tex);
|
||||
glUniform1i(glGetUniformLocation(program, "u_tex"), 0);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
glBindVertexArray(0);
|
||||
glBindTexture(target, 0);
|
||||
glUseProgram(0);
|
||||
BindDefaultFramebuffer();
|
||||
glFinish();
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "sampling sync";
|
||||
}
|
||||
|
||||
// Attaches (tex, layer) to the scratch framebuffer; false when this backend cannot.
|
||||
bool Attach(GLuint tex, int layer) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
|
||||
if (layer >= 0) {
|
||||
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, tex, 0, layer);
|
||||
} else {
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
|
||||
}
|
||||
return glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
|
||||
}
|
||||
|
||||
void Detach() {
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
|
||||
BindDefaultFramebuffer();
|
||||
}
|
||||
|
||||
// The GPU write the shadow never sees: the whole level (or layer) to green.
|
||||
void GpuClearGreen(const Format& f) {
|
||||
if (f.encoding == Encoding::Uint8) {
|
||||
const GLuint green[4] = {0u, kIntegerOne, 0u, 1u};
|
||||
glClearBufferuiv(GL_COLOR, 0, green);
|
||||
} else {
|
||||
const GLfloat green[4] = {0.0f, 1.0f, 0.0f, 1.0f};
|
||||
glClearBufferfv(GL_COLOR, 0, green);
|
||||
}
|
||||
}
|
||||
|
||||
// The attached image's rgb, in units of "one", width x height texels.
|
||||
std::vector<float> ReadRgb(const Format& f, int width, int height) {
|
||||
std::vector<float> rgb(static_cast<std::size_t>(width * height) * 3u);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
if (f.encoding == Encoding::Uint8) {
|
||||
std::vector<GLuint> texels(static_cast<std::size_t>(width * height) * 4u, 0xA5A5A5A5u);
|
||||
glReadPixels(0, 0, width, height, GL_RGBA_INTEGER, GL_UNSIGNED_INT, texels.data());
|
||||
for (int i = 0; i < width * height; ++i)
|
||||
for (int c = 0; c < 3; ++c)
|
||||
rgb[i * 3 + c] = static_cast<float>(texels[i * 4 + c]) / kIntegerOne;
|
||||
} else {
|
||||
std::vector<GLfloat> texels(static_cast<std::size_t>(width * height) * 4u, -7.0f);
|
||||
glReadPixels(0, 0, width, height, GL_RGBA, GL_FLOAT, texels.data());
|
||||
for (int i = 0; i < width * height; ++i)
|
||||
for (int c = 0; c < 3; ++c) rgb[i * 3 + c] = texels[i * 4 + c];
|
||||
}
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "readback";
|
||||
return rgb;
|
||||
}
|
||||
|
||||
// Green everywhere but blue inside [boxX, boxX + boxW) x [boxY, boxY + boxH).
|
||||
static void ExpectClearAroundBox(const std::vector<float>& rgb, int width, int height, int boxX, int boxY,
|
||||
int boxW, int boxH) {
|
||||
int bad = 0;
|
||||
std::ostringstream first;
|
||||
for (int y = 0; y < height; ++y)
|
||||
for (int x = 0; x < width; ++x) {
|
||||
const bool inBox = x >= boxX && x < boxX + boxW && y >= boxY && y < boxY + boxH;
|
||||
const float want[3] = {0.0f, inBox ? 0.0f : 1.0f, inBox ? 1.0f : 0.0f};
|
||||
const float* got = &rgb[static_cast<std::size_t>(y * width + x) * 3u];
|
||||
bool texelBad = false;
|
||||
for (int c = 0; c < 3; ++c) texelBad |= std::fabs(got[c] - want[c]) > 0.02f;
|
||||
if (texelBad && bad++ == 0) {
|
||||
first << "(" << x << "," << y << ") got " << got[0] << "," << got[1] << "," << got[2]
|
||||
<< " want " << want[0] << "," << want[1] << "," << want[2];
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(bad, 0) << bad << " of " << width * height << " texels wrong; first " << first.str()
|
||||
<< " (red outside the box: the stale shadow went back up; green inside it: the "
|
||||
"write landed before the clear or draw)";
|
||||
}
|
||||
|
||||
// Upload red, sync, clear green on the GPU, sync, write a blue 2x2 box, sync, read.
|
||||
void Run2D(const Format& f) {
|
||||
const GLuint t = NewTexture(GL_TEXTURE_2D);
|
||||
const bool integer = f.encoding == Encoding::Uint8;
|
||||
const char* sampler = integer ? "usampler2D" : "sampler2D";
|
||||
const auto red = Fill(f, kEdge * kEdge, 1.0f, 0.0f, 0.0f);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(f.internalFormat), kEdge, kEdge, 0, f.clientFormat,
|
||||
f.clientType, red.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "definition";
|
||||
SyncBySampling(GL_TEXTURE_2D, t, sampler, "ivec2(0)");
|
||||
if (!Attach(t, -1)) {
|
||||
Detach();
|
||||
GTEST_SKIP() << "the format does not attach on backend " << Gl().BackendName();
|
||||
}
|
||||
GpuClearGreen(f);
|
||||
Detach();
|
||||
SyncBySampling(GL_TEXTURE_2D, t, sampler, "ivec2(0)");
|
||||
const auto blue = Fill(f, kBoxEdge * kBoxEdge, 0.0f, 0.0f, 1.0f);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, kBoxAt, kBoxAt, kBoxEdge, kBoxEdge, f.clientFormat, f.clientType,
|
||||
blue.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
SyncBySampling(GL_TEXTURE_2D, t, sampler, "ivec2(0)");
|
||||
ASSERT_TRUE(Attach(t, -1));
|
||||
const auto rgb = ReadRgb(f, kEdge, kEdge);
|
||||
Detach();
|
||||
ExpectClearAroundBox(rgb, kEdge, kEdge, kBoxAt, kBoxAt, kBoxEdge, kBoxEdge);
|
||||
}
|
||||
|
||||
// Building blocks of the RGBA8 ordering cases: the same texels, issued in the orders
|
||||
// a backend that defers work may get wrong.
|
||||
static Format Rgba8() { return {GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, 4, Encoding::Unorm8}; }
|
||||
|
||||
GLuint NewRedRgba8() {
|
||||
const GLuint t = NewTexture(GL_TEXTURE_2D);
|
||||
const auto red = Fill(Rgba8(), kEdge * kEdge, 1.0f, 0.0f, 0.0f);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, red.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
EXPECT_EQ(FirstGLError(), 0u) << "definition";
|
||||
return t;
|
||||
}
|
||||
|
||||
void SampleRgba8(GLuint t) { SyncBySampling(GL_TEXTURE_2D, t, "sampler2D", "ivec2(0)"); }
|
||||
|
||||
void ClearRgba8Green(GLuint t) {
|
||||
ASSERT_TRUE(Attach(t, -1));
|
||||
GpuClearGreen(Rgba8());
|
||||
Detach();
|
||||
}
|
||||
|
||||
// A draw that writes green over the whole level.
|
||||
void DrawRgba8Green(GLuint t) {
|
||||
std::string error;
|
||||
const GLuint program = CompileProgram(
|
||||
kVS, "#version 430 core\nout vec4 o_color;\nvoid main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }\n",
|
||||
&error);
|
||||
ASSERT_NE(program, 0u) << error;
|
||||
m_programs.push_back(program);
|
||||
ASSERT_TRUE(Attach(t, -1));
|
||||
glViewport(0, 0, kEdge, kEdge);
|
||||
glUseProgram(program);
|
||||
glBindVertexArray(m_vao);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
Detach();
|
||||
glViewport(0, 0, Gl().Width(), Gl().Height());
|
||||
}
|
||||
|
||||
void WriteRgba8BlueBox(GLuint t) {
|
||||
const auto blue = Fill(Rgba8(), kBoxEdge * kBoxEdge, 0.0f, 0.0f, 1.0f);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, kBoxAt, kBoxAt, kBoxEdge, kBoxEdge, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
blue.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
// Green everywhere, and blue in the box when `boxWins`.
|
||||
void ExpectRgba8(GLuint t, bool boxWins) {
|
||||
ASSERT_TRUE(Attach(t, -1));
|
||||
const auto rgb = ReadRgb(Rgba8(), kEdge, kEdge);
|
||||
Detach();
|
||||
ExpectClearAroundBox(rgb, kEdge, kEdge, kBoxAt, kBoxAt, boxWins ? kBoxEdge : 0, boxWins ? kBoxEdge : 0);
|
||||
}
|
||||
|
||||
GLuint m_vao = 0, m_fbo = 0;
|
||||
ColorFbo m_scratch{};
|
||||
std::vector<GLuint> m_programs;
|
||||
std::vector<GLuint> m_textures;
|
||||
};
|
||||
|
||||
// Control: the shadow's bytes go to the driver as they are.
|
||||
TEST_F(PartialUploadOverGpuContentScenario, Rgba8SubImageKeepsTheGpuClear) {
|
||||
if (!Ready()) return;
|
||||
Run2D({GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, 4, Encoding::Unorm8});
|
||||
}
|
||||
|
||||
TEST_F(PartialUploadOverGpuContentScenario, Srgb8SubImageKeepsTheGpuClear) {
|
||||
if (!Ready()) return;
|
||||
Run2D({GL_SRGB8, GL_RGB, GL_UNSIGNED_BYTE, 3, Encoding::Unorm8});
|
||||
}
|
||||
|
||||
TEST_F(PartialUploadOverGpuContentScenario, Rgb8SnormSubImageKeepsTheGpuClear) {
|
||||
if (!Ready()) return;
|
||||
Run2D({GL_RGB8_SNORM, GL_RGB, GL_BYTE, 3, Encoding::Snorm8});
|
||||
}
|
||||
|
||||
TEST_F(PartialUploadOverGpuContentScenario, Rgb16fSubImageKeepsTheGpuClear) {
|
||||
if (!Ready()) return;
|
||||
Run2D({GL_RGB16F, GL_RGB, GL_FLOAT, 3, Encoding::Float32});
|
||||
}
|
||||
|
||||
TEST_F(PartialUploadOverGpuContentScenario, Rgb8uiSubImageKeepsTheGpuClear) {
|
||||
if (!Ready()) return;
|
||||
Run2D({GL_RGB8UI, GL_RGB_INTEGER, GL_UNSIGNED_BYTE, 3, Encoding::Uint8});
|
||||
}
|
||||
|
||||
// A 1D array's ES image is a 2D array one texel high with the layers in depth, so the
|
||||
// upload is shaped differently from the shadow (layers as rows) even with no conversion.
|
||||
TEST_F(PartialUploadOverGpuContentScenario, OneDimensionalArraySubImageKeepsTheGpuClearedLayer) {
|
||||
if (!Ready()) return;
|
||||
constexpr int kLayers = 2;
|
||||
const Format f{GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, 4, Encoding::Unorm8};
|
||||
const GLuint t = NewTexture(GL_TEXTURE_1D_ARRAY);
|
||||
const auto red = Fill(f, kEdge * kLayers, 1.0f, 0.0f, 0.0f);
|
||||
glBindTexture(GL_TEXTURE_1D_ARRAY, t);
|
||||
glTexImage2D(GL_TEXTURE_1D_ARRAY, 0, GL_RGBA8, kEdge, kLayers, 0, GL_RGBA, GL_UNSIGNED_BYTE, red.data());
|
||||
glBindTexture(GL_TEXTURE_1D_ARRAY, 0);
|
||||
ASSERT_EQ(FirstGLError(), 0u) << "definition";
|
||||
SyncBySampling(GL_TEXTURE_1D_ARRAY, t, "sampler1DArray", "ivec2(0)");
|
||||
if (!Attach(t, 1)) {
|
||||
Detach();
|
||||
GTEST_SKIP() << "a 1D array layer does not attach on backend " << Gl().BackendName();
|
||||
}
|
||||
GpuClearGreen(f);
|
||||
Detach();
|
||||
SyncBySampling(GL_TEXTURE_1D_ARRAY, t, "sampler1DArray", "ivec2(0)");
|
||||
const auto blue = Fill(f, kBoxEdge, 0.0f, 0.0f, 1.0f);
|
||||
glBindTexture(GL_TEXTURE_1D_ARRAY, t);
|
||||
glTexSubImage2D(GL_TEXTURE_1D_ARRAY, 0, kBoxAt, 1, kBoxEdge, 1, GL_RGBA, GL_UNSIGNED_BYTE, blue.data());
|
||||
glBindTexture(GL_TEXTURE_1D_ARRAY, 0);
|
||||
SyncBySampling(GL_TEXTURE_1D_ARRAY, t, "sampler1DArray", "ivec2(0)");
|
||||
ASSERT_TRUE(Attach(t, 1));
|
||||
const auto layer1 = ReadRgb(f, kEdge, 1);
|
||||
Detach();
|
||||
ExpectClearAroundBox(layer1, kEdge, 1, kBoxAt, 0, kBoxEdge, 1);
|
||||
}
|
||||
|
||||
// The orders below matter to a backend that defers GPU work (Magma queues clears and runs
|
||||
// texel uploads in a batch ahead of the frame's recorded commands); every one is plain GL.
|
||||
|
||||
// The clear is still queued when the write arrives: it has to reach the level first.
|
||||
TEST_F(PartialUploadOverGpuContentScenario, SubImageAfterAQueuedClearLandsOnTheClear) {
|
||||
if (!Ready()) return;
|
||||
const GLuint t = NewRedRgba8();
|
||||
SampleRgba8(t);
|
||||
ClearRgba8Green(t);
|
||||
WriteRgba8BlueBox(t);
|
||||
SampleRgba8(t);
|
||||
ExpectRgba8(t, true);
|
||||
}
|
||||
|
||||
// Same, with the readback as the first thing to sync the write.
|
||||
TEST_F(PartialUploadOverGpuContentScenario, SubImageAfterAQueuedClearLandsOnTheClearForAReadback) {
|
||||
if (!Ready()) return;
|
||||
const GLuint t = NewRedRgba8();
|
||||
SampleRgba8(t);
|
||||
ClearRgba8Green(t);
|
||||
WriteRgba8BlueBox(t);
|
||||
ExpectRgba8(t, true);
|
||||
}
|
||||
|
||||
// The other order: the write is still pending when the clear arrives, and the clear wins.
|
||||
TEST_F(PartialUploadOverGpuContentScenario, ClearAfterAPendingSubImageWins) {
|
||||
if (!Ready()) return;
|
||||
const GLuint t = NewRedRgba8();
|
||||
SampleRgba8(t);
|
||||
WriteRgba8BlueBox(t);
|
||||
ClearRgba8Green(t);
|
||||
SampleRgba8(t);
|
||||
ExpectRgba8(t, false);
|
||||
}
|
||||
|
||||
// A draw into the level instead of a clear.
|
||||
TEST_F(PartialUploadOverGpuContentScenario, SubImageAfterADrawIntoTheLevelLandsOnTheDraw) {
|
||||
if (!Ready()) return;
|
||||
const GLuint t = NewRedRgba8();
|
||||
SampleRgba8(t);
|
||||
DrawRgba8Green(t);
|
||||
WriteRgba8BlueBox(t);
|
||||
SampleRgba8(t);
|
||||
ExpectRgba8(t, true);
|
||||
}
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
Reference in New Issue
Block a user