mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 14:18:31 +09:00
[Fix, Test] (MG_State, MG_Backend/DirectVulkan, MG_Backend/DirectGLES, MG_IntegrationTest): a respecified capture buffer left the transform feedback writing one store and the readback reading another
This commit is contained in:
@@ -603,7 +603,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void Ops_Respecify(BufferObject& bufferObject) {
|
void Ops_Respecify(BufferObject& bufferObject) {
|
||||||
auto* resource = ResourceOf(bufferObject);
|
auto* resource = ResourceOf(bufferObject);
|
||||||
if (!resource) return; // lazy: EnsureBufferResource full-uploads on creation
|
if (!resource) return; // lazy: EnsureBufferResource full-uploads on creation
|
||||||
if (resource->persistentMapped) return; // immutable persistent storage is never respecified
|
if (resource->persistentMapped) {
|
||||||
|
// The frontend writes straight into the storage mapped here, and it
|
||||||
|
// renewed that mapping for the redefined store before writing to it
|
||||||
|
// (BufferObject::RedefineStorage), so the new contents already are
|
||||||
|
// where a respecification would put them.
|
||||||
|
if (bufferObject.IsBackendPersistentMapped()) return;
|
||||||
|
// Renewal declined (a zero-sized store, or the map could not be
|
||||||
|
// retaken): the buffer is back on its CPU shadow and needs the
|
||||||
|
// ordinary respecification below.
|
||||||
|
resource->persistentMapped = false;
|
||||||
|
resource->persistentPtr = nullptr;
|
||||||
|
}
|
||||||
if (!CanTouchGLNow() || resource->id == 0 ||
|
if (!CanTouchGLNow() || resource->id == 0 ||
|
||||||
resource->contextGeneration != g_bufferContextGeneration) {
|
resource->contextGeneration != g_bufferContextGeneration) {
|
||||||
resource->pendingRespecify = true;
|
resource->pendingRespecify = true;
|
||||||
|
|||||||
@@ -379,6 +379,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
|||||||
BumpSliceEpoch(*resource);
|
BumpSliceEpoch(*resource);
|
||||||
// Any cached streaming slice refers to the previous contents.
|
// Any cached streaming slice refers to the previous contents.
|
||||||
resource->transientFrameSerial = 0;
|
resource->transientFrameSerial = 0;
|
||||||
|
if (resource->persistentMapped) {
|
||||||
|
if (bufferObject.IsBackendPersistentMapped()) {
|
||||||
|
// The frontend renewed its adoption of this storage for the redefined
|
||||||
|
// store before writing a byte of it (BufferObject::RedefineStorage), so
|
||||||
|
// the new contents are already HERE and there is no second copy to
|
||||||
|
// update. Swapping the storage is what must not happen: the mapping the
|
||||||
|
// frontend holds, and every read that resolves through it, would keep
|
||||||
|
// addressing the storage being released - which is how a transform
|
||||||
|
// feedback capture came to be written to one buffer and read back out
|
||||||
|
// of another.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The renewal did not happen (a zero-sized store, or the storage could not
|
||||||
|
// be created): the frontend is back on its CPU shadow, so this is an
|
||||||
|
// ordinary resident buffer again and the handling below applies.
|
||||||
|
resource->persistentMapped = false;
|
||||||
|
}
|
||||||
if (!resource->buffer.IsValid()) {
|
if (!resource->buffer.IsValid()) {
|
||||||
return; // streaming-only resource: shadow + serial are enough
|
return; // streaming-only resource: shadow + serial are enough
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ add_executable(MobileGLIntegrationTest
|
|||||||
Scenarios/FragmentOutputArrayIndexScenario.cpp
|
Scenarios/FragmentOutputArrayIndexScenario.cpp
|
||||||
Scenarios/BufferTextureScenario.cpp
|
Scenarios/BufferTextureScenario.cpp
|
||||||
Scenarios/VertexAttribBindingScenario.cpp
|
Scenarios/VertexAttribBindingScenario.cpp
|
||||||
|
Scenarios/XfbCaptureBufferReuseScenario.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(MobileGLIntegrationTest PRIVATE
|
target_include_directories(MobileGLIntegrationTest PRIVATE
|
||||||
|
|||||||
@@ -0,0 +1,307 @@
|
|||||||
|
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/XfbCaptureBufferReuseScenario.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
|
||||||
|
//
|
||||||
|
// ONE capture buffer, SEVERAL capture spans - the shape most KHR-GL4x cases that
|
||||||
|
// use transform feedback as a readback channel are built on. They allocate the
|
||||||
|
// capture buffer once in a setup step and then run span after span through it,
|
||||||
|
// so a defect that only shows from the second span onwards fails the whole case
|
||||||
|
// while the first span (and every single-span scenario in this suite) stays
|
||||||
|
// green. The first thing checked here is therefore not the capture itself but
|
||||||
|
// that the bytes the capture wrote are the bytes the readback reads.
|
||||||
|
//
|
||||||
|
// Two ways of reusing the buffer, because they exercise different machinery:
|
||||||
|
//
|
||||||
|
// * respecified between spans (glBufferData while the buffer is still bound to
|
||||||
|
// the transform-feedback binding point), which is what a test helper that
|
||||||
|
// poisons its capture buffer before every span does;
|
||||||
|
// * allocated ONCE with immutable storage and never touched again, which is
|
||||||
|
// what KHR-GL45.direct_state_access.vertex_arrays_enable_disable_attributes
|
||||||
|
// does - glBufferStorage(4 bytes) in its setup, then two draws.
|
||||||
|
//
|
||||||
|
// The negative control (a fresh buffer object per span) is a separate case
|
||||||
|
// rather than a parameter: it is the configuration that already worked, so it
|
||||||
|
// has to keep working for the others to mean anything.
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#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 float kPoison = -1234.0f;
|
||||||
|
// One vec4 per point, one point per draw.
|
||||||
|
constexpr std::size_t kCaptureFloats = 4;
|
||||||
|
constexpr std::size_t kCaptureBytes = kCaptureFloats * sizeof(float);
|
||||||
|
|
||||||
|
GLuint CompileShader(GLenum type, const std::string& source, std::string* log) {
|
||||||
|
const GLuint shader = glCreateShader(type);
|
||||||
|
const char* text = source.c_str();
|
||||||
|
glShaderSource(shader, 1, &text, nullptr);
|
||||||
|
glCompileShader(shader);
|
||||||
|
GLint status = GL_FALSE;
|
||||||
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
|
||||||
|
if (status == GL_FALSE) {
|
||||||
|
GLint length = 0;
|
||||||
|
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
||||||
|
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||||
|
glGetShaderInfoLog(shader, length + 1, nullptr, buffer.data());
|
||||||
|
if (log != nullptr) *log = buffer.data();
|
||||||
|
glDeleteShader(shader);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertex-only capture program: whatever the draw fetched at location 0 comes
|
||||||
|
// straight back out through the capture. Runs under GL_RASTERIZER_DISCARD, so
|
||||||
|
// there is no fragment stage.
|
||||||
|
GLuint BuildCaptureProgram(std::string* log) {
|
||||||
|
const std::string vertexSource = R"(#version 430 core
|
||||||
|
layout(location = 0) in vec4 vs_in_value;
|
||||||
|
out vec4 vs_out_value;
|
||||||
|
void main() {
|
||||||
|
vs_out_value = vs_in_value;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
const GLuint vertexShader = CompileShader(GL_VERTEX_SHADER, vertexSource, log);
|
||||||
|
if (vertexShader == 0) return 0;
|
||||||
|
const GLuint program = glCreateProgram();
|
||||||
|
glAttachShader(program, vertexShader);
|
||||||
|
const char* varying = "vs_out_value";
|
||||||
|
glTransformFeedbackVaryings(program, 1, &varying, GL_INTERLEAVED_ATTRIBS);
|
||||||
|
glLinkProgram(program);
|
||||||
|
glDeleteShader(vertexShader);
|
||||||
|
GLint status = GL_FALSE;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &status);
|
||||||
|
if (status == GL_FALSE) {
|
||||||
|
GLint length = 0;
|
||||||
|
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
|
||||||
|
std::vector<char> buffer(static_cast<std::size_t>(length) + 1, '\0');
|
||||||
|
glGetProgramInfoLog(program, length + 1, nullptr, buffer.data());
|
||||||
|
if (log != nullptr) *log = buffer.data();
|
||||||
|
glDeleteProgram(program);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
class XfbCaptureBufferReuseScenario : public ScenarioTest {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
ScenarioTest::SetUp();
|
||||||
|
if (!Ready()) return;
|
||||||
|
|
||||||
|
std::string log;
|
||||||
|
m_program = BuildCaptureProgram(&log);
|
||||||
|
ASSERT_NE(m_program, 0u) << "capture program failed to build: " << log;
|
||||||
|
|
||||||
|
glGenVertexArrays(1, &m_vao);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glGenBuffers(1, &m_vbo);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, kCaptureBytes, nullptr, GL_DYNAMIC_DRAW);
|
||||||
|
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, nullptr);
|
||||||
|
glEnableVertexAttribArray(0);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (!Ready()) return;
|
||||||
|
glBindVertexArray(0);
|
||||||
|
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
|
||||||
|
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
|
||||||
|
if (m_program != 0) glDeleteProgram(m_program);
|
||||||
|
glUseProgram(0);
|
||||||
|
ScenarioTest::TearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The vertex the next span will fetch and capture.
|
||||||
|
void SetVertex(float value) {
|
||||||
|
const float data[kCaptureFloats] = {value, value + 1.0f, value + 2.0f, value + 3.0f};
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
|
||||||
|
glBufferSubData(GL_ARRAY_BUFFER, 0, kCaptureBytes, data);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One capture span over the buffer currently bound to capture point 0.
|
||||||
|
void RunSpan() {
|
||||||
|
glEnable(GL_RASTERIZER_DISCARD);
|
||||||
|
glUseProgram(m_program);
|
||||||
|
glBindVertexArray(m_vao);
|
||||||
|
glBeginTransformFeedback(GL_POINTS);
|
||||||
|
glDrawArrays(GL_POINTS, 0, 1);
|
||||||
|
glEndTransformFeedback();
|
||||||
|
glDisable(GL_RASTERIZER_DISCARD);
|
||||||
|
glUseProgram(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ::testing::AssertionResult CapturedIs(const float* data, float value) {
|
||||||
|
for (std::size_t i = 0; i < kCaptureFloats; ++i) {
|
||||||
|
const float expected = value + static_cast<float>(i);
|
||||||
|
const float got = data[i];
|
||||||
|
if (got - expected > 0.01f || expected - got > 0.01f) {
|
||||||
|
return ::testing::AssertionFailure()
|
||||||
|
<< "component " << i << " is " << got << ", expected " << expected
|
||||||
|
<< (got == kPoison ? " (the capture never reached these bytes)" : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ::testing::AssertionSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint m_program = 0;
|
||||||
|
GLuint m_vao = 0;
|
||||||
|
GLuint m_vbo = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The negative control: one buffer object per span. This is the configuration
|
||||||
|
// every multi-span scenario in this suite works around the others with, so it
|
||||||
|
// has to hold or nothing below is interpretable.
|
||||||
|
TEST_F(XfbCaptureBufferReuseScenario, EverySpanIntoABufferObjectOfItsOwn) {
|
||||||
|
if (!Ready()) GTEST_SKIP();
|
||||||
|
|
||||||
|
for (int span = 0; span < 3; ++span) {
|
||||||
|
const float value = 10.0f * static_cast<float>(span + 1);
|
||||||
|
const std::vector<float> poison(kCaptureFloats, kPoison);
|
||||||
|
|
||||||
|
GLuint xfbBuffer = 0;
|
||||||
|
glGenBuffers(1, &xfbBuffer);
|
||||||
|
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||||
|
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, kCaptureBytes, poison.data(), GL_DYNAMIC_DRAW);
|
||||||
|
|
||||||
|
SetVertex(value);
|
||||||
|
RunSpan();
|
||||||
|
|
||||||
|
float readback[kCaptureFloats] = {kPoison, kPoison, kPoison, kPoison};
|
||||||
|
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, kCaptureBytes, readback);
|
||||||
|
EXPECT_TRUE(CapturedIs(readback, value)) << "span " << span;
|
||||||
|
|
||||||
|
glDeleteBuffers(1, &xfbBuffer);
|
||||||
|
}
|
||||||
|
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same three spans through ONE buffer object, respecified before each of
|
||||||
|
// them WHILE it is bound to capture point 0 - a helper poisoning its capture
|
||||||
|
// buffer, which is what makes "captured nothing" legible in the first place.
|
||||||
|
//
|
||||||
|
// A respecification is free to replace the storage underneath (that is what
|
||||||
|
// orphaning is), and on a buffer whose bytes the backend has already handed
|
||||||
|
// the frontend a pointer into, the replacement has to reach that pointer too.
|
||||||
|
// It did not: the capture wrote the new storage and the readback kept reading
|
||||||
|
// the old one, so every span after the first came back poison.
|
||||||
|
TEST_F(XfbCaptureBufferReuseScenario, EverySpanIntoOneRespecifiedBufferObject) {
|
||||||
|
if (!Ready()) GTEST_SKIP();
|
||||||
|
|
||||||
|
GLuint xfbBuffer = 0;
|
||||||
|
glGenBuffers(1, &xfbBuffer);
|
||||||
|
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||||
|
|
||||||
|
for (int span = 0; span < 3; ++span) {
|
||||||
|
const float value = 10.0f * static_cast<float>(span + 1);
|
||||||
|
const std::vector<float> poison(kCaptureFloats, kPoison);
|
||||||
|
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, kCaptureBytes, poison.data(), GL_DYNAMIC_DRAW);
|
||||||
|
|
||||||
|
SetVertex(value);
|
||||||
|
RunSpan();
|
||||||
|
|
||||||
|
float readback[kCaptureFloats] = {kPoison, kPoison, kPoison, kPoison};
|
||||||
|
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, kCaptureBytes, readback);
|
||||||
|
EXPECT_TRUE(CapturedIs(readback, value)) << "span " << span;
|
||||||
|
}
|
||||||
|
|
||||||
|
glDeleteBuffers(1, &xfbBuffer);
|
||||||
|
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A respecification that CHANGES the size, which is the case a re-pointing
|
||||||
|
// that only handled same-size storage would still get wrong - and, before the
|
||||||
|
// fix, the case that wrote the new (larger) contents through a mapping sized
|
||||||
|
// for the old ones.
|
||||||
|
TEST_F(XfbCaptureBufferReuseScenario, ARespecificationMayChangeTheCaptureBufferSize) {
|
||||||
|
if (!Ready()) GTEST_SKIP();
|
||||||
|
|
||||||
|
GLuint xfbBuffer = 0;
|
||||||
|
glGenBuffers(1, &xfbBuffer);
|
||||||
|
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||||
|
|
||||||
|
// Sized for one point, then for four, then back down to one.
|
||||||
|
const std::size_t pointCapacity[] = {1, 4, 1};
|
||||||
|
for (int span = 0; span < 3; ++span) {
|
||||||
|
const float value = 10.0f * static_cast<float>(span + 1);
|
||||||
|
const std::size_t floats = kCaptureFloats * pointCapacity[span];
|
||||||
|
const std::vector<float> poison(floats, kPoison);
|
||||||
|
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(floats * sizeof(float)),
|
||||||
|
poison.data(), GL_DYNAMIC_DRAW);
|
||||||
|
|
||||||
|
SetVertex(value);
|
||||||
|
RunSpan();
|
||||||
|
|
||||||
|
std::vector<float> readback(floats, kPoison);
|
||||||
|
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
|
||||||
|
static_cast<GLsizeiptr>(floats * sizeof(float)), readback.data());
|
||||||
|
EXPECT_TRUE(CapturedIs(readback.data(), value)) << "span " << span;
|
||||||
|
// The bytes past the one point the draw produced must still be the
|
||||||
|
// poison the respecification put there, not whatever the previous
|
||||||
|
// (differently sized) storage held.
|
||||||
|
for (std::size_t i = kCaptureFloats; i < floats; ++i) {
|
||||||
|
EXPECT_FLOAT_EQ(readback[i], kPoison) << "span " << span << " float " << i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
glDeleteBuffers(1, &xfbBuffer);
|
||||||
|
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The KHR-GL45.direct_state_access.vertex_arrays_enable_disable_attributes
|
||||||
|
// shape: the capture buffer gets IMMUTABLE storage once, in a setup step, and
|
||||||
|
// is never respecified - two spans simply run through it, each read back with
|
||||||
|
// glMapBuffer. Nothing here may depend on a respecification to reset the
|
||||||
|
// capture: glBeginTransformFeedback does that on its own.
|
||||||
|
TEST_F(XfbCaptureBufferReuseScenario, EverySpanIntoOneImmutableStorageBuffer) {
|
||||||
|
if (!Ready()) GTEST_SKIP();
|
||||||
|
|
||||||
|
GLuint xfbBuffer = 0;
|
||||||
|
glGenBuffers(1, &xfbBuffer);
|
||||||
|
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, xfbBuffer);
|
||||||
|
glBufferStorage(GL_TRANSFORM_FEEDBACK_BUFFER, kCaptureBytes, nullptr, GL_MAP_READ_BIT);
|
||||||
|
ASSERT_EQ(glGetError(), GL_NO_ERROR) << "glBufferStorage on the capture buffer";
|
||||||
|
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, xfbBuffer);
|
||||||
|
|
||||||
|
for (int span = 0; span < 3; ++span) {
|
||||||
|
const float value = 10.0f * static_cast<float>(span + 1);
|
||||||
|
SetVertex(value);
|
||||||
|
RunSpan();
|
||||||
|
|
||||||
|
const void* mapped = glMapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, GL_READ_ONLY);
|
||||||
|
ASSERT_NE(mapped, nullptr) << "span " << span << ": glMapBuffer returned null";
|
||||||
|
float readback[kCaptureFloats] = {kPoison, kPoison, kPoison, kPoison};
|
||||||
|
std::memcpy(readback, mapped, kCaptureBytes);
|
||||||
|
glUnmapBuffer(GL_TRANSFORM_FEEDBACK_BUFFER);
|
||||||
|
EXPECT_TRUE(CapturedIs(readback, value)) << "span " << span;
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0);
|
||||||
|
glDeleteBuffers(1, &xfbBuffer);
|
||||||
|
EXPECT_EQ(glGetError(), GL_NO_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
} // namespace MGITest
|
||||||
@@ -75,10 +75,39 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
NotifySubData(offset, size);
|
NotifySubData(offset, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BufferObject::Respecify(SizeT size, const void* data) {
|
// A (re)definition of the store is about to write `size` bytes through Bytes().
|
||||||
ReleaseMemory();
|
// Sizing the shadow is all that takes for a shadow-backed buffer, but a buffer
|
||||||
|
// whose bytes were adopted into backend GPU memory needs the adoption renewed
|
||||||
|
// first: the mapping it holds describes exactly the OLD store. Writing the new
|
||||||
|
// contents through it runs past its end the moment the store grows, and a backend
|
||||||
|
// that replaces the storage for the new store (which is what an orphaning
|
||||||
|
// respecification asks for) would leave that mapping - and therefore every later
|
||||||
|
// read of this buffer - addressing storage nothing writes to any more.
|
||||||
|
//
|
||||||
|
// Renewing rather than simply dropping is what keeps the common case free: a
|
||||||
|
// redefinition at the same size gets the same mapping back without any storage
|
||||||
|
// being created, which is also what makes the backend's respecify a no-op (the
|
||||||
|
// new bytes are already in the storage it would otherwise upload to).
|
||||||
|
void BufferObject::RedefineStorage(SizeT size) {
|
||||||
|
const Bool wasGpuResident = m_resource.IsGpuResident();
|
||||||
|
if (wasGpuResident) {
|
||||||
|
// A capture or a shader write may still be running against the very bytes
|
||||||
|
// that are about to be overwritten.
|
||||||
|
SyncGpuWrites();
|
||||||
|
m_resource.ReleasePersistentMap();
|
||||||
|
}
|
||||||
m_size = size;
|
m_size = size;
|
||||||
m_resource.ResizeShadow(size);
|
m_resource.ResizeShadow(size);
|
||||||
|
if (wasGpuResident) {
|
||||||
|
// Declining is allowed (a zero-sized store, a backend without the op): the
|
||||||
|
// buffer simply goes back to the CPU-shadow model it had before adoption.
|
||||||
|
EnsureGpuResidentStorage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BufferObject::Respecify(SizeT size, const void* data) {
|
||||||
|
ReleaseMemory();
|
||||||
|
RedefineStorage(size);
|
||||||
if (data && size > 0) {
|
if (data && size > 0) {
|
||||||
Memcpy(m_resource.Bytes(), data, size);
|
Memcpy(m_resource.Bytes(), data, size);
|
||||||
}
|
}
|
||||||
@@ -96,8 +125,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
void BufferObject::AllocateImmutableStorage(SizeT size, const void* data, GLbitfield storageFlags) {
|
void BufferObject::AllocateImmutableStorage(SizeT size, const void* data, GLbitfield storageFlags) {
|
||||||
ReleaseMemory();
|
ReleaseMemory();
|
||||||
m_size = size;
|
RedefineStorage(size);
|
||||||
m_resource.ResizeShadow(size);
|
|
||||||
if (data) {
|
if (data) {
|
||||||
Memcpy(m_resource.Bytes(), data, size);
|
Memcpy(m_resource.Bytes(), data, size);
|
||||||
} else if (size > 0) {
|
} else if (size > 0) {
|
||||||
|
|||||||
@@ -205,6 +205,9 @@ namespace MobileGL {
|
|||||||
void SetBackendResource(SharedPtr<BackendBufferResource> resource);
|
void SetBackendResource(SharedPtr<BackendBufferResource> resource);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// Sizes the store for a (re)definition, renewing an adopted GPU-resident
|
||||||
|
// mapping across it. See the definition for why the renewal is not optional.
|
||||||
|
void RedefineStorage(SizeT size);
|
||||||
void NotifyRespecify();
|
void NotifyRespecify();
|
||||||
void NotifySubData(SizeT offset, SizeT size);
|
void NotifySubData(SizeT offset, SizeT size);
|
||||||
void NotifyFlushMappedRange(Range1D range, Flags<BufferMappingAccessBit> appAccess);
|
void NotifyFlushMappedRange(Range1D range, Flags<BufferMappingAccessBit> appAccess);
|
||||||
|
|||||||
@@ -70,6 +70,15 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
m_shadow->shrink_to_fit();
|
m_shadow->shrink_to_fit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Give the adoption back: the bytes resolve against the shadow again (which
|
||||||
|
// the caller must (re)size, it was released on adoption). Used when the store
|
||||||
|
// itself is redefined - the mapping describes exactly the store that is going
|
||||||
|
// away, so it may neither be written through nor kept. It is NOT a general
|
||||||
|
// "unmap": a persistent map the application holds outlives every unmap by
|
||||||
|
// definition, and the calls that could redefine such a buffer's store are
|
||||||
|
// errors the frontend refuses before reaching here.
|
||||||
|
void ReleasePersistentMap() { m_gpuMapped = nullptr; }
|
||||||
|
|
||||||
// Backend GPU resource, owned here in both modes.
|
// Backend GPU resource, owned here in both modes.
|
||||||
const SharedPtr<BackendBufferResource>& Backend() const { return m_backend; }
|
const SharedPtr<BackendBufferResource>& Backend() const { return m_backend; }
|
||||||
void SetBackend(SharedPtr<BackendBufferResource> backend) { m_backend = std::move(backend); }
|
void SetBackend(SharedPtr<BackendBufferResource> backend) { m_backend = std::move(backend); }
|
||||||
|
|||||||
Reference in New Issue
Block a user