Route Texture2D through renderer GL

This commit is contained in:
2026-06-03 06:24:56 +02:00
parent 9971b2b7f2
commit ae69f7437f
6 changed files with 959 additions and 69 deletions

View File

@@ -466,8 +466,9 @@ Known local toolchain state:
viewport/scissor dispatch consumed by `App::draw` and `App::vr_draw_ui`, viewport/scissor dispatch consumed by `App::draw` and `App::vr_draw_ui`,
tested generic capability/buffer-clear dispatch consumed by VR draw state tested generic capability/buffer-clear dispatch consumed by VR draw state
setup, tested saved-state snapshot/restore dispatch consumed by the retained setup, tested saved-state snapshot/restore dispatch consumed by the retained
`gl_state` utility, plus renderer API to OpenGL token mapping and `gl_state` utility, tested texture lifecycle/readback dispatch consumed by
command-planning contracts used by the OpenGL parity work. the retained `Texture2D` utility, plus renderer API to OpenGL token mapping
and command-planning contracts used by the OpenGL parity work.
- `pano_cli plan-cloud-upload` exposes `pp_app_core` cloud upload availability, - `pano_cli plan-cloud-upload` exposes `pp_app_core` cloud upload availability,
new-document warning, publish prompt, and save-before-upload planning as JSON; new-document warning, publish prompt, and save-before-upload planning as JSON;
the live cloud upload command consumes the same start contract before the live cloud upload command consumes the same start contract before

View File

@@ -540,6 +540,10 @@ tested `pp_renderer_gl` saved-state dispatch contracts, covering capability
state, viewport, clear color, framebuffer/program bindings, active texture, state, viewport, clear color, framebuffer/program bindings, active texture,
2D texture slots, samplers, and cube-map binding without changing the legacy 2D texture slots, samplers, and cube-map binding without changing the legacy
utility's public fields. utility's public fields.
Legacy `Texture2D` allocation, binding, deletion, mipmap generation, region
update, and framebuffer readback now execute through tested `pp_renderer_gl`
texture dispatch contracts. This keeps the app API stable while moving another
resource lifecycle path behind the renderer backend boundary.
Windows RenderDoc frame capture hooks now also dispatch through Windows RenderDoc frame capture hooks now also dispatch through
`PlatformServices`, keeping capture integration in the platform service while `PlatformServices`, keeping capture integration in the platform service while
leaving non-Windows adapters as no-ops. leaving non-Windows adapters as no-ops.

View File

@@ -422,6 +422,192 @@ pp::foundation::Status clear_opengl_buffers(
return pp::foundation::Status::success(); return pp::foundation::Status::success();
} }
pp::foundation::Result<std::uint32_t> allocate_opengl_texture_2d(
OpenGlTexture2DAllocation allocation,
OpenGlTexture2DAllocationDispatch dispatch) noexcept
{
if (dispatch.gen_textures == nullptr
|| dispatch.bind_texture == nullptr
|| dispatch.tex_image_2d == nullptr) {
return pp::foundation::Result<std::uint32_t>::failure(
pp::foundation::Status::invalid_argument("OpenGL texture allocation dispatch callbacks must not be null"));
}
if (allocation.width <= 0
|| allocation.height <= 0
|| allocation.internal_format == 0
|| allocation.pixel_format == 0U
|| allocation.component_type == 0U) {
return pp::foundation::Result<std::uint32_t>::failure(
pp::foundation::Status::invalid_argument("OpenGL texture allocation parameters are invalid"));
}
std::uint32_t texture_id = 0U;
dispatch.gen_textures(1U, &texture_id);
if (texture_id == 0U) {
return pp::foundation::Result<std::uint32_t>::failure(
pp::foundation::Status::out_of_range("OpenGL texture allocation returned id 0"));
}
dispatch.bind_texture(texture_2d_target(), texture_id);
dispatch.tex_image_2d(
texture_2d_target(),
0,
allocation.internal_format,
allocation.width,
allocation.height,
0,
allocation.pixel_format,
allocation.component_type,
allocation.data);
dispatch.bind_texture(texture_2d_target(), default_framebuffer_id());
return pp::foundation::Result<std::uint32_t>::success(texture_id);
}
pp::foundation::Status delete_opengl_texture_2d(
std::uint32_t texture_id,
OpenGlTexture2DDeleteDispatch dispatch) noexcept
{
if (dispatch.delete_textures == nullptr) {
return pp::foundation::Status::invalid_argument("OpenGL texture delete dispatch callback must not be null");
}
if (texture_id == 0U) {
return pp::foundation::Status::success();
}
dispatch.delete_textures(1U, &texture_id);
return pp::foundation::Status::success();
}
pp::foundation::Status bind_opengl_texture_2d(
std::uint32_t texture_id,
OpenGlTexture2DBindDispatch dispatch) noexcept
{
if (dispatch.bind_texture == nullptr) {
return pp::foundation::Status::invalid_argument("OpenGL texture bind dispatch callback must not be null");
}
dispatch.bind_texture(texture_2d_target(), texture_id);
return pp::foundation::Status::success();
}
pp::foundation::Status update_opengl_texture_2d(
OpenGlTexture2DUpdate update,
OpenGlTexture2DUpdateDispatch dispatch) noexcept
{
if (dispatch.bind_texture == nullptr || dispatch.tex_sub_image_2d == nullptr) {
return pp::foundation::Status::invalid_argument("OpenGL texture update dispatch callbacks must not be null");
}
if (update.texture_id == 0U
|| update.width <= 0
|| update.height <= 0
|| update.pixel_format == 0U
|| update.component_type == 0U
|| update.data == nullptr) {
return pp::foundation::Status::invalid_argument("OpenGL texture update parameters are invalid");
}
dispatch.bind_texture(texture_2d_target(), update.texture_id);
dispatch.tex_sub_image_2d(
texture_2d_target(),
0,
0,
0,
update.width,
update.height,
update.pixel_format,
update.component_type,
update.data);
return pp::foundation::Status::success();
}
pp::foundation::Status generate_opengl_texture_2d_mipmaps(
std::uint32_t texture_id,
OpenGlTexture2DMipmapDispatch dispatch) noexcept
{
if (dispatch.bind_texture == nullptr || dispatch.generate_mipmap == nullptr) {
return pp::foundation::Status::invalid_argument("OpenGL texture mipmap dispatch callbacks must not be null");
}
if (texture_id == 0U) {
return pp::foundation::Status::invalid_argument("OpenGL texture mipmap target is invalid");
}
dispatch.bind_texture(texture_2d_target(), texture_id);
dispatch.generate_mipmap(texture_2d_target());
dispatch.bind_texture(texture_2d_target(), default_framebuffer_id());
return pp::foundation::Status::success();
}
pp::foundation::Result<OpenGlTexture2DReadbackResult> readback_opengl_texture_2d(
OpenGlTexture2DReadback readback,
OpenGlTexture2DReadbackDispatch dispatch) noexcept
{
if (dispatch.bind_texture == nullptr
|| dispatch.gen_framebuffers == nullptr
|| dispatch.get_integer == nullptr
|| dispatch.bind_framebuffer == nullptr
|| dispatch.framebuffer_texture_2d == nullptr
|| dispatch.check_framebuffer_status == nullptr
|| dispatch.read_pixels == nullptr
|| dispatch.delete_framebuffers == nullptr) {
return pp::foundation::Result<OpenGlTexture2DReadbackResult>::failure(
pp::foundation::Status::invalid_argument("OpenGL texture readback dispatch callbacks must not be null"));
}
if (readback.texture_id == 0U
|| readback.width <= 0
|| readback.height <= 0
|| readback.format.pixel_format == 0U
|| readback.format.component_type == 0U
|| readback.format.bytes_per_pixel == 0U
|| readback.pixels == nullptr) {
return pp::foundation::Result<OpenGlTexture2DReadbackResult>::failure(
pp::foundation::Status::invalid_argument("OpenGL texture readback parameters are invalid"));
}
std::uint32_t framebuffer_id = 0U;
dispatch.gen_framebuffers(1U, &framebuffer_id);
if (framebuffer_id == 0U) {
return pp::foundation::Result<OpenGlTexture2DReadbackResult>::failure(
pp::foundation::Status::out_of_range("OpenGL framebuffer allocation returned id 0"));
}
std::int32_t previous_framebuffer = 0;
dispatch.get_integer(draw_framebuffer_binding_query(), &previous_framebuffer);
dispatch.bind_texture(texture_2d_target(), readback.texture_id);
dispatch.bind_framebuffer(framebuffer_target(), framebuffer_id);
dispatch.framebuffer_texture_2d(
framebuffer_target(),
framebuffer_color_attachment(),
texture_2d_target(),
readback.texture_id,
0);
const auto framebuffer_status = dispatch.check_framebuffer_status(framebuffer_target());
OpenGlTexture2DReadbackResult result {
.framebuffer_status = framebuffer_status,
.pixels_read = false,
};
if (framebuffer_status == framebuffer_complete_status()) {
dispatch.read_pixels(
0,
0,
readback.width,
readback.height,
readback.format.pixel_format,
readback.format.component_type,
readback.pixels);
result.pixels_read = true;
}
dispatch.bind_framebuffer(framebuffer_target(), static_cast<std::uint32_t>(previous_framebuffer));
dispatch.delete_framebuffers(1U, &framebuffer_id);
return pp::foundation::Result<OpenGlTexture2DReadbackResult>::success(result);
}
std::uint32_t extension_count_query() noexcept std::uint32_t extension_count_query() noexcept
{ {
return gl_num_extensions; return gl_num_extensions;

View File

@@ -110,6 +110,37 @@ struct OpenGlRendererTextureFormat {
std::uint32_t bytes_per_pixel = 0; std::uint32_t bytes_per_pixel = 0;
}; };
struct OpenGlTexture2DAllocation {
std::int32_t width = 0;
std::int32_t height = 0;
std::int32_t internal_format = 0;
std::uint32_t pixel_format = 0;
std::uint32_t component_type = 0;
const void* data = nullptr;
};
struct OpenGlTexture2DUpdate {
std::uint32_t texture_id = 0;
std::int32_t width = 0;
std::int32_t height = 0;
std::uint32_t pixel_format = 0;
std::uint32_t component_type = 0;
const void* data = nullptr;
};
struct OpenGlTexture2DReadback {
std::uint32_t texture_id = 0;
std::int32_t width = 0;
std::int32_t height = 0;
OpenGlReadbackFormat format;
void* pixels = nullptr;
};
struct OpenGlTexture2DReadbackResult {
std::uint32_t framebuffer_status = 0;
bool pixels_read = false;
};
struct OpenGlWindowsWglContextConfig { struct OpenGlWindowsWglContextConfig {
std::array<std::int32_t, 9> context_attributes {}; std::array<std::int32_t, 9> context_attributes {};
std::array<std::int32_t, 15> pixel_format_attributes {}; std::array<std::int32_t, 15> pixel_format_attributes {};
@@ -154,6 +185,44 @@ using OpenGlUseProgramFn = void (*)(std::uint32_t program) noexcept;
using OpenGlBindFramebufferFn = void (*)(std::uint32_t target, std::uint32_t framebuffer) noexcept; using OpenGlBindFramebufferFn = void (*)(std::uint32_t target, std::uint32_t framebuffer) noexcept;
using OpenGlBindTextureFn = void (*)(std::uint32_t target, std::uint32_t texture) noexcept; using OpenGlBindTextureFn = void (*)(std::uint32_t target, std::uint32_t texture) noexcept;
using OpenGlBindSamplerFn = void (*)(std::uint32_t unit, std::uint32_t sampler) noexcept; using OpenGlBindSamplerFn = void (*)(std::uint32_t unit, std::uint32_t sampler) noexcept;
using OpenGlGenObjectsFn = void (*)(std::uint32_t count, std::uint32_t* ids) noexcept;
using OpenGlDeleteObjectsFn = void (*)(std::uint32_t count, const std::uint32_t* ids) noexcept;
using OpenGlTexImage2DFn = void (*)(
std::uint32_t target,
std::int32_t level,
std::int32_t internal_format,
std::int32_t width,
std::int32_t height,
std::int32_t border,
std::uint32_t pixel_format,
std::uint32_t component_type,
const void* data) noexcept;
using OpenGlTexSubImage2DFn = void (*)(
std::uint32_t target,
std::int32_t level,
std::int32_t x,
std::int32_t y,
std::int32_t width,
std::int32_t height,
std::uint32_t pixel_format,
std::uint32_t component_type,
const void* data) noexcept;
using OpenGlGenerateMipmapFn = void (*)(std::uint32_t target) noexcept;
using OpenGlFramebufferTexture2DFn = void (*)(
std::uint32_t target,
std::uint32_t attachment,
std::uint32_t texture_target,
std::uint32_t texture,
std::int32_t level) noexcept;
using OpenGlCheckFramebufferStatusFn = std::uint32_t (*)(std::uint32_t target) noexcept;
using OpenGlReadPixelsFn = void (*)(
std::int32_t x,
std::int32_t y,
std::int32_t width,
std::int32_t height,
std::uint32_t pixel_format,
std::uint32_t component_type,
void* pixels) noexcept;
struct OpenGlStateDispatch { struct OpenGlStateDispatch {
OpenGlCapabilityFn enable = nullptr; OpenGlCapabilityFn enable = nullptr;
@@ -228,6 +297,41 @@ struct OpenGlBufferClearDispatch {
OpenGlClearFn clear = nullptr; OpenGlClearFn clear = nullptr;
}; };
struct OpenGlTexture2DAllocationDispatch {
OpenGlGenObjectsFn gen_textures = nullptr;
OpenGlBindTextureFn bind_texture = nullptr;
OpenGlTexImage2DFn tex_image_2d = nullptr;
};
struct OpenGlTexture2DDeleteDispatch {
OpenGlDeleteObjectsFn delete_textures = nullptr;
};
struct OpenGlTexture2DBindDispatch {
OpenGlBindTextureFn bind_texture = nullptr;
};
struct OpenGlTexture2DUpdateDispatch {
OpenGlBindTextureFn bind_texture = nullptr;
OpenGlTexSubImage2DFn tex_sub_image_2d = nullptr;
};
struct OpenGlTexture2DMipmapDispatch {
OpenGlBindTextureFn bind_texture = nullptr;
OpenGlGenerateMipmapFn generate_mipmap = nullptr;
};
struct OpenGlTexture2DReadbackDispatch {
OpenGlBindTextureFn bind_texture = nullptr;
OpenGlGenObjectsFn gen_framebuffers = nullptr;
OpenGlGetIntegerFn get_integer = nullptr;
OpenGlBindFramebufferFn bind_framebuffer = nullptr;
OpenGlFramebufferTexture2DFn framebuffer_texture_2d = nullptr;
OpenGlCheckFramebufferStatusFn check_framebuffer_status = nullptr;
OpenGlReadPixelsFn read_pixels = nullptr;
OpenGlDeleteObjectsFn delete_framebuffers = nullptr;
};
[[nodiscard]] OpenGlCapabilities detect_opengl_capabilities( [[nodiscard]] OpenGlCapabilities detect_opengl_capabilities(
std::span<const std::string_view> extensions, std::span<const std::string_view> extensions,
OpenGlRuntime runtime) noexcept; OpenGlRuntime runtime) noexcept;
@@ -260,6 +364,24 @@ struct OpenGlBufferClearDispatch {
[[nodiscard]] pp::foundation::Status clear_opengl_buffers( [[nodiscard]] pp::foundation::Status clear_opengl_buffers(
std::uint32_t mask, std::uint32_t mask,
OpenGlBufferClearDispatch dispatch) noexcept; OpenGlBufferClearDispatch dispatch) noexcept;
[[nodiscard]] pp::foundation::Result<std::uint32_t> allocate_opengl_texture_2d(
OpenGlTexture2DAllocation allocation,
OpenGlTexture2DAllocationDispatch dispatch) noexcept;
[[nodiscard]] pp::foundation::Status delete_opengl_texture_2d(
std::uint32_t texture_id,
OpenGlTexture2DDeleteDispatch dispatch) noexcept;
[[nodiscard]] pp::foundation::Status bind_opengl_texture_2d(
std::uint32_t texture_id,
OpenGlTexture2DBindDispatch dispatch) noexcept;
[[nodiscard]] pp::foundation::Status update_opengl_texture_2d(
OpenGlTexture2DUpdate update,
OpenGlTexture2DUpdateDispatch dispatch) noexcept;
[[nodiscard]] pp::foundation::Status generate_opengl_texture_2d_mipmaps(
std::uint32_t texture_id,
OpenGlTexture2DMipmapDispatch dispatch) noexcept;
[[nodiscard]] pp::foundation::Result<OpenGlTexture2DReadbackResult> readback_opengl_texture_2d(
OpenGlTexture2DReadback readback,
OpenGlTexture2DReadbackDispatch dispatch) noexcept;
[[nodiscard]] std::uint32_t extension_count_query() noexcept; [[nodiscard]] std::uint32_t extension_count_query() noexcept;
[[nodiscard]] std::uint32_t extension_string_name() noexcept; [[nodiscard]] std::uint32_t extension_string_name() noexcept;

View File

@@ -9,24 +9,134 @@
namespace { namespace {
[[nodiscard]] GLenum texture_2d_target() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::texture_2d_target());
}
[[nodiscard]] GLenum texture_cube_map_target() noexcept [[nodiscard]] GLenum texture_cube_map_target() noexcept
{ {
return static_cast<GLenum>(pp::renderer::gl::texture_cube_map_target()); return static_cast<GLenum>(pp::renderer::gl::texture_cube_map_target());
} }
[[nodiscard]] GLenum framebuffer_target() noexcept void gen_opengl_textures(std::uint32_t count, std::uint32_t* ids) noexcept
{ {
return static_cast<GLenum>(pp::renderer::gl::framebuffer_target()); glGenTextures(static_cast<GLsizei>(count), reinterpret_cast<GLuint*>(ids));
} }
[[nodiscard]] GLenum draw_framebuffer_binding_query() noexcept void delete_opengl_textures(std::uint32_t count, const std::uint32_t* ids) noexcept
{ {
return static_cast<GLenum>(pp::renderer::gl::draw_framebuffer_binding_query()); glDeleteTextures(static_cast<GLsizei>(count), reinterpret_cast<const GLuint*>(ids));
}
void bind_opengl_texture(std::uint32_t target, std::uint32_t texture) noexcept
{
glBindTexture(static_cast<GLenum>(target), static_cast<GLuint>(texture));
}
void upload_opengl_texture_2d(
std::uint32_t target,
std::int32_t level,
std::int32_t internal_format,
std::int32_t width,
std::int32_t height,
std::int32_t border,
std::uint32_t pixel_format,
std::uint32_t component_type,
const void* data) noexcept
{
glTexImage2D(
static_cast<GLenum>(target),
static_cast<GLint>(level),
static_cast<GLint>(internal_format),
static_cast<GLsizei>(width),
static_cast<GLsizei>(height),
static_cast<GLint>(border),
static_cast<GLenum>(pixel_format),
static_cast<GLenum>(component_type),
data);
}
void update_opengl_texture_2d_region(
std::uint32_t target,
std::int32_t level,
std::int32_t x,
std::int32_t y,
std::int32_t width,
std::int32_t height,
std::uint32_t pixel_format,
std::uint32_t component_type,
const void* data) noexcept
{
glTexSubImage2D(
static_cast<GLenum>(target),
static_cast<GLint>(level),
static_cast<GLint>(x),
static_cast<GLint>(y),
static_cast<GLsizei>(width),
static_cast<GLsizei>(height),
static_cast<GLenum>(pixel_format),
static_cast<GLenum>(component_type),
data);
}
void generate_opengl_mipmap(std::uint32_t target) noexcept
{
glGenerateMipmap(static_cast<GLenum>(target));
}
void gen_opengl_framebuffers(std::uint32_t count, std::uint32_t* ids) noexcept
{
glGenFramebuffers(static_cast<GLsizei>(count), reinterpret_cast<GLuint*>(ids));
}
void delete_opengl_framebuffers(std::uint32_t count, const std::uint32_t* ids) noexcept
{
glDeleteFramebuffers(static_cast<GLsizei>(count), reinterpret_cast<const GLuint*>(ids));
}
void query_opengl_integer(std::uint32_t name, std::int32_t* value) noexcept
{
glGetIntegerv(static_cast<GLenum>(name), reinterpret_cast<GLint*>(value));
}
void bind_opengl_framebuffer(std::uint32_t target, std::uint32_t framebuffer) noexcept
{
glBindFramebuffer(static_cast<GLenum>(target), static_cast<GLuint>(framebuffer));
}
void attach_opengl_framebuffer_texture_2d(
std::uint32_t target,
std::uint32_t attachment,
std::uint32_t texture_target,
std::uint32_t texture,
std::int32_t level) noexcept
{
glFramebufferTexture2D(
static_cast<GLenum>(target),
static_cast<GLenum>(attachment),
static_cast<GLenum>(texture_target),
static_cast<GLuint>(texture),
static_cast<GLint>(level));
}
std::uint32_t check_opengl_framebuffer_status(std::uint32_t target) noexcept
{
return static_cast<std::uint32_t>(glCheckFramebufferStatus(static_cast<GLenum>(target)));
}
void read_opengl_pixels(
std::int32_t x,
std::int32_t y,
std::int32_t width,
std::int32_t height,
std::uint32_t pixel_format,
std::uint32_t component_type,
void* pixels) noexcept
{
glReadPixels(
static_cast<GLint>(x),
static_cast<GLint>(y),
static_cast<GLsizei>(width),
static_cast<GLsizei>(height),
static_cast<GLenum>(pixel_format),
static_cast<GLenum>(component_type),
pixels);
} }
} }
@@ -162,44 +272,33 @@ Image Texture2D::get_image() const noexcept
ret.create(m_width, m_height); ret.create(m_width, m_height);
App::I->render_task([&] App::I->render_task([&]
{ {
bind(); const auto readback = pp::renderer::gl::readback_opengl_texture_2d(
pp::renderer::gl::OpenGlTexture2DReadback {
GLuint fboID; .texture_id = static_cast<std::uint32_t>(m_tex),
glGenFramebuffers(1, &fboID); .width = m_width,
if (fboID == 0) .height = m_height,
.format = pp::renderer::gl::rgba8_readback_format(),
.pixels = ret.m_data.get(),
},
pp::renderer::gl::OpenGlTexture2DReadbackDispatch {
.bind_texture = bind_opengl_texture,
.gen_framebuffers = gen_opengl_framebuffers,
.get_integer = query_opengl_integer,
.bind_framebuffer = bind_opengl_framebuffer,
.framebuffer_texture_2d = attach_opengl_framebuffer_texture_2d,
.check_framebuffer_status = check_opengl_framebuffer_status,
.read_pixels = read_opengl_pixels,
.delete_framebuffers = delete_opengl_framebuffers,
});
if (!readback.ok()) {
LOG("Texture2D::get_image() failed because: %s", readback.status().message);
return; return;
GLint oldFboID;
glGetIntegerv(draw_framebuffer_binding_query(), &oldFboID);
glBindFramebuffer(framebuffer_target(), fboID);
glFramebufferTexture2D(
framebuffer_target(),
static_cast<GLenum>(pp::renderer::gl::framebuffer_color_attachment()),
texture_2d_target(),
m_tex,
0);
GLenum status = glCheckFramebufferStatus(framebuffer_target());
if (status == static_cast<GLenum>(pp::renderer::gl::framebuffer_complete_status()))
{
const auto readback = pp::renderer::gl::rgba8_readback_format();
glReadPixels(
0,
0,
m_width,
m_height,
static_cast<GLenum>(readback.pixel_format),
static_cast<GLenum>(readback.component_type),
ret.m_data.get());
} }
else
{ if (!readback.value().pixels_read) {
LOG("Texture2D::get_image() failed because: %s", LOG("Texture2D::get_image() failed because: %s",
pp::renderer::gl::framebuffer_status_name(static_cast<std::uint32_t>(status))); pp::renderer::gl::framebuffer_status_name(readback.value().framebuffer_status));
} }
glBindFramebuffer(framebuffer_target(), oldFboID);
glDeleteFramebuffers(1, &fboID);
}); });
return ret; return ret;
} }
@@ -249,17 +348,32 @@ bool Texture2D::create(int width, int height, GLint internal_format, GLint forma
App::I->render_task([=] App::I->render_task([=]
{ {
destroy(); destroy();
const auto component_type = pp::renderer::gl::texture_upload_type_for_internal_format(
static_cast<std::uint32_t>(internal_format));
const auto texture = pp::renderer::gl::allocate_opengl_texture_2d(
pp::renderer::gl::OpenGlTexture2DAllocation {
.width = width,
.height = height,
.internal_format = internal_format,
.pixel_format = static_cast<std::uint32_t>(format),
.component_type = component_type,
.data = data,
},
pp::renderer::gl::OpenGlTexture2DAllocationDispatch {
.gen_textures = gen_opengl_textures,
.bind_texture = bind_opengl_texture,
.tex_image_2d = upload_opengl_texture_2d,
});
if (!texture.ok()) {
LOG("Texture2D::create() failed because: %s", texture.status().message);
return;
}
m_width = width; m_width = width;
m_height = height; m_height = height;
m_format = format; m_format = format;
m_iformat = internal_format; m_iformat = internal_format;
glGenTextures(1, &m_tex); m_tex = static_cast<GLuint>(texture.value());
//LOG("TEX create %d", m_tex);
bind();
const auto ifmt = static_cast<GLenum>(
pp::renderer::gl::texture_upload_type_for_internal_format(static_cast<std::uint32_t>(internal_format)));
glTexImage2D(texture_2d_target(), 0, internal_format, width, height, 0, format, ifmt, data);
unbind();
}); });
return true; return true;
} }
@@ -281,9 +395,16 @@ void Texture2D::create_mipmaps()
{ {
App::I->render_task([this] App::I->render_task([this]
{ {
bind(); const auto status = pp::renderer::gl::generate_opengl_texture_2d_mipmaps(
glGenerateMipmap(texture_2d_target()); static_cast<std::uint32_t>(m_tex),
unbind(); pp::renderer::gl::OpenGlTexture2DMipmapDispatch {
.bind_texture = bind_opengl_texture,
.generate_mipmap = generate_opengl_mipmap,
});
if (!status.ok()) {
LOG("Texture2D::create_mipmaps() failed because: %s", status.message);
return;
}
has_mips = true; has_mips = true;
}); });
} }
@@ -331,7 +452,13 @@ void Texture2D::destroy()
{ {
App::I->render_task_async([id = m_tex] App::I->render_task_async([id = m_tex]
{ {
glDeleteTextures(1, &id); const auto status = pp::renderer::gl::delete_opengl_texture_2d(
static_cast<std::uint32_t>(id),
pp::renderer::gl::OpenGlTexture2DDeleteDispatch {
.delete_textures = delete_opengl_textures,
});
if (!status.ok())
LOG("Texture2D::destroy() failed because: %s", status.message);
}); });
m_tex = 0; m_tex = 0;
} }
@@ -340,30 +467,46 @@ void Texture2D::destroy()
void Texture2D::bind() const void Texture2D::bind() const
{ {
assert(App::I->is_render_thread()); assert(App::I->is_render_thread());
glBindTexture(texture_2d_target(), m_tex); const auto status = pp::renderer::gl::bind_opengl_texture_2d(
static_cast<std::uint32_t>(m_tex),
pp::renderer::gl::OpenGlTexture2DBindDispatch {
.bind_texture = bind_opengl_texture,
});
if (!status.ok())
LOG("Texture2D::bind() failed because: %s", status.message);
} }
void Texture2D::unbind() const void Texture2D::unbind() const
{ {
assert(App::I->is_render_thread()); assert(App::I->is_render_thread());
glBindTexture(texture_2d_target(), 0); const auto status = pp::renderer::gl::bind_opengl_texture_2d(
0U,
pp::renderer::gl::OpenGlTexture2DBindDispatch {
.bind_texture = bind_opengl_texture,
});
if (!status.ok())
LOG("Texture2D::unbind() failed because: %s", status.message);
} }
void Texture2D::update(const uint8_t* data) void Texture2D::update(const uint8_t* data)
{ {
App::I->render_task([this, data] App::I->render_task([this, data]
{ {
bind(); const auto status = pp::renderer::gl::update_opengl_texture_2d(
glTexSubImage2D( pp::renderer::gl::OpenGlTexture2DUpdate {
texture_2d_target(), .texture_id = static_cast<std::uint32_t>(m_tex),
0, .width = m_width,
0, .height = m_height,
0, .pixel_format = static_cast<std::uint32_t>(m_format),
m_width, .component_type = pp::renderer::gl::unsigned_byte_component_type(),
m_height, .data = data,
m_format, },
static_cast<GLenum>(pp::renderer::gl::unsigned_byte_component_type()), pp::renderer::gl::OpenGlTexture2DUpdateDispatch {
data); .bind_texture = bind_opengl_texture,
.tex_sub_image_2d = update_opengl_texture_2d_region,
});
if (!status.ok())
LOG("Texture2D::update() failed because: %s", status.message);
}); });
} }

View File

@@ -38,6 +38,39 @@ struct RecordedOpenGlBindingCall {
std::uint32_t second = 0; std::uint32_t second = 0;
}; };
struct RecordedOpenGlTextureImageCall {
std::uint32_t target = 0;
std::int32_t level = 0;
std::int32_t internal_format = 0;
std::int32_t x = 0;
std::int32_t y = 0;
std::int32_t width = 0;
std::int32_t height = 0;
std::int32_t border = 0;
std::uint32_t pixel_format = 0;
std::uint32_t component_type = 0;
const void* data = nullptr;
bool sub_image = false;
};
struct RecordedOpenGlFramebufferAttachmentCall {
std::uint32_t target = 0;
std::uint32_t attachment = 0;
std::uint32_t texture_target = 0;
std::uint32_t texture = 0;
std::int32_t level = 0;
};
struct RecordedOpenGlReadPixelsCall {
std::int32_t x = 0;
std::int32_t y = 0;
std::int32_t width = 0;
std::int32_t height = 0;
std::uint32_t pixel_format = 0;
std::uint32_t component_type = 0;
void* pixels = nullptr;
};
std::vector<RecordedOpenGlStateCall> recorded_state_calls; std::vector<RecordedOpenGlStateCall> recorded_state_calls;
std::vector<std::uint32_t> recorded_string_queries; std::vector<std::uint32_t> recorded_string_queries;
std::vector<pp::renderer::gl::OpenGlDefaultClear> recorded_clear_calls; std::vector<pp::renderer::gl::OpenGlDefaultClear> recorded_clear_calls;
@@ -47,6 +80,18 @@ std::vector<std::uint32_t> recorded_integer_queries;
std::vector<std::uint32_t> recorded_float_queries; std::vector<std::uint32_t> recorded_float_queries;
std::vector<std::uint32_t> recorded_active_texture_calls; std::vector<std::uint32_t> recorded_active_texture_calls;
std::vector<RecordedOpenGlBindingCall> recorded_binding_calls; std::vector<RecordedOpenGlBindingCall> recorded_binding_calls;
std::vector<std::uint32_t> recorded_generated_texture_counts;
std::vector<std::uint32_t> recorded_deleted_textures;
std::vector<RecordedOpenGlTextureImageCall> recorded_texture_image_calls;
std::vector<std::uint32_t> recorded_mipmap_targets;
std::vector<std::uint32_t> recorded_generated_framebuffer_counts;
std::vector<std::uint32_t> recorded_deleted_framebuffers;
std::vector<RecordedOpenGlFramebufferAttachmentCall> recorded_framebuffer_attachment_calls;
std::vector<std::uint32_t> recorded_framebuffer_status_queries;
std::vector<RecordedOpenGlReadPixelsCall> recorded_read_pixels_calls;
std::uint32_t next_texture_id = 91U;
std::uint32_t next_framebuffer_id = 44U;
std::uint32_t configured_framebuffer_status = 0x8CD5U;
void record_enable(std::uint32_t state) noexcept void record_enable(std::uint32_t state) noexcept
{ {
@@ -236,6 +281,132 @@ void record_bind_sampler(std::uint32_t unit, std::uint32_t sampler) noexcept
}); });
} }
void record_gen_textures(std::uint32_t count, std::uint32_t* ids) noexcept
{
recorded_generated_texture_counts.push_back(count);
for (std::uint32_t i = 0U; i < count; ++i) {
ids[i] = next_texture_id + i;
}
}
void record_delete_textures(std::uint32_t count, const std::uint32_t* ids) noexcept
{
for (std::uint32_t i = 0U; i < count; ++i) {
recorded_deleted_textures.push_back(ids[i]);
}
}
void record_tex_image_2d(
std::uint32_t target,
std::int32_t level,
std::int32_t internal_format,
std::int32_t width,
std::int32_t height,
std::int32_t border,
std::uint32_t pixel_format,
std::uint32_t component_type,
const void* data) noexcept
{
recorded_texture_image_calls.push_back(RecordedOpenGlTextureImageCall {
.target = target,
.level = level,
.internal_format = internal_format,
.width = width,
.height = height,
.border = border,
.pixel_format = pixel_format,
.component_type = component_type,
.data = data,
});
}
void record_tex_sub_image_2d(
std::uint32_t target,
std::int32_t level,
std::int32_t x,
std::int32_t y,
std::int32_t width,
std::int32_t height,
std::uint32_t pixel_format,
std::uint32_t component_type,
const void* data) noexcept
{
recorded_texture_image_calls.push_back(RecordedOpenGlTextureImageCall {
.target = target,
.level = level,
.x = x,
.y = y,
.width = width,
.height = height,
.pixel_format = pixel_format,
.component_type = component_type,
.data = data,
.sub_image = true,
});
}
void record_generate_mipmap(std::uint32_t target) noexcept
{
recorded_mipmap_targets.push_back(target);
}
void record_gen_framebuffers(std::uint32_t count, std::uint32_t* ids) noexcept
{
recorded_generated_framebuffer_counts.push_back(count);
for (std::uint32_t i = 0U; i < count; ++i) {
ids[i] = next_framebuffer_id + i;
}
}
void record_delete_framebuffers(std::uint32_t count, const std::uint32_t* ids) noexcept
{
for (std::uint32_t i = 0U; i < count; ++i) {
recorded_deleted_framebuffers.push_back(ids[i]);
}
}
void record_framebuffer_texture_2d(
std::uint32_t target,
std::uint32_t attachment,
std::uint32_t texture_target,
std::uint32_t texture,
std::int32_t level) noexcept
{
recorded_framebuffer_attachment_calls.push_back(RecordedOpenGlFramebufferAttachmentCall {
.target = target,
.attachment = attachment,
.texture_target = texture_target,
.texture = texture,
.level = level,
});
}
std::uint32_t record_check_framebuffer_status(std::uint32_t target) noexcept
{
recorded_framebuffer_status_queries.push_back(target);
return configured_framebuffer_status;
}
void record_read_pixels(
std::int32_t x,
std::int32_t y,
std::int32_t width,
std::int32_t height,
std::uint32_t pixel_format,
std::uint32_t component_type,
void* pixels) noexcept
{
recorded_read_pixels_calls.push_back(RecordedOpenGlReadPixelsCall {
.x = x,
.y = y,
.width = width,
.height = height,
.pixel_format = pixel_format,
.component_type = component_type,
.pixels = pixels,
});
}
void detects_common_extension_capabilities(pp::tests::Harness& h) void detects_common_extension_capabilities(pp::tests::Harness& h)
{ {
constexpr std::array<std::string_view, 2> extensions { constexpr std::array<std::string_view, 2> extensions {
@@ -1317,6 +1488,261 @@ void rejects_incomplete_buffer_clear_dispatch(pp::tests::Harness& h)
PP_EXPECT(h, status.code == pp::foundation::StatusCode::invalid_argument); PP_EXPECT(h, status.code == pp::foundation::StatusCode::invalid_argument);
} }
void allocates_texture_2d_through_dispatch(pp::tests::Harness& h)
{
recorded_generated_texture_counts.clear();
recorded_binding_calls.clear();
recorded_texture_image_calls.clear();
next_texture_id = 91U;
const std::array<std::uint8_t, 4> pixels { 1U, 2U, 3U, 4U };
const auto texture = pp::renderer::gl::allocate_opengl_texture_2d(
pp::renderer::gl::OpenGlTexture2DAllocation {
.width = 8,
.height = 4,
.internal_format = 0x8058,
.pixel_format = 0x1908U,
.component_type = 0x1401U,
.data = pixels.data(),
},
pp::renderer::gl::OpenGlTexture2DAllocationDispatch {
.gen_textures = record_gen_textures,
.bind_texture = record_bind_texture,
.tex_image_2d = record_tex_image_2d,
});
PP_EXPECT(h, texture.ok());
PP_EXPECT(h, texture.value() == 91U);
PP_EXPECT(h, recorded_generated_texture_counts.size() == 1U);
PP_EXPECT(h, recorded_generated_texture_counts[0] == 1U);
PP_EXPECT(h, recorded_binding_calls.size() == 2U);
PP_EXPECT(h, recorded_binding_calls[0].kind == RecordedOpenGlBindingCall::Kind::bind_texture);
PP_EXPECT(h, recorded_binding_calls[0].first == 0x0DE1U);
PP_EXPECT(h, recorded_binding_calls[0].second == 91U);
PP_EXPECT(h, recorded_binding_calls[1].second == 0U);
PP_EXPECT(h, recorded_texture_image_calls.size() == 1U);
PP_EXPECT(h, recorded_texture_image_calls[0].target == 0x0DE1U);
PP_EXPECT(h, recorded_texture_image_calls[0].width == 8);
PP_EXPECT(h, recorded_texture_image_calls[0].height == 4);
PP_EXPECT(h, recorded_texture_image_calls[0].data == pixels.data());
}
void rejects_invalid_texture_2d_allocation(pp::tests::Harness& h)
{
const auto missing_dispatch = pp::renderer::gl::allocate_opengl_texture_2d(
pp::renderer::gl::OpenGlTexture2DAllocation {
.width = 1,
.height = 1,
.internal_format = 0x8058,
.pixel_format = 0x1908U,
.component_type = 0x1401U,
},
pp::renderer::gl::OpenGlTexture2DAllocationDispatch {
.gen_textures = record_gen_textures,
.bind_texture = record_bind_texture,
});
const auto invalid_dimensions = pp::renderer::gl::allocate_opengl_texture_2d(
pp::renderer::gl::OpenGlTexture2DAllocation {
.width = 0,
.height = 1,
.internal_format = 0x8058,
.pixel_format = 0x1908U,
.component_type = 0x1401U,
},
pp::renderer::gl::OpenGlTexture2DAllocationDispatch {
.gen_textures = record_gen_textures,
.bind_texture = record_bind_texture,
.tex_image_2d = record_tex_image_2d,
});
PP_EXPECT(h, !missing_dispatch.ok());
PP_EXPECT(h, missing_dispatch.status().code == pp::foundation::StatusCode::invalid_argument);
PP_EXPECT(h, !invalid_dimensions.ok());
PP_EXPECT(h, invalid_dimensions.status().code == pp::foundation::StatusCode::invalid_argument);
}
void deletes_and_binds_texture_2d_through_dispatch(pp::tests::Harness& h)
{
recorded_deleted_textures.clear();
recorded_binding_calls.clear();
const auto delete_status = pp::renderer::gl::delete_opengl_texture_2d(
27U,
pp::renderer::gl::OpenGlTexture2DDeleteDispatch {
.delete_textures = record_delete_textures,
});
const auto bind_status = pp::renderer::gl::bind_opengl_texture_2d(
27U,
pp::renderer::gl::OpenGlTexture2DBindDispatch {
.bind_texture = record_bind_texture,
});
PP_EXPECT(h, delete_status.ok());
PP_EXPECT(h, recorded_deleted_textures.size() == 1U);
PP_EXPECT(h, recorded_deleted_textures[0] == 27U);
PP_EXPECT(h, bind_status.ok());
PP_EXPECT(h, recorded_binding_calls.size() == 1U);
PP_EXPECT(h, recorded_binding_calls[0].first == 0x0DE1U);
PP_EXPECT(h, recorded_binding_calls[0].second == 27U);
}
void updates_texture_2d_through_dispatch(pp::tests::Harness& h)
{
recorded_binding_calls.clear();
recorded_texture_image_calls.clear();
const std::array<std::uint8_t, 4> pixels { 9U, 8U, 7U, 6U };
const auto status = pp::renderer::gl::update_opengl_texture_2d(
pp::renderer::gl::OpenGlTexture2DUpdate {
.texture_id = 31U,
.width = 2,
.height = 2,
.pixel_format = 0x1908U,
.component_type = 0x1401U,
.data = pixels.data(),
},
pp::renderer::gl::OpenGlTexture2DUpdateDispatch {
.bind_texture = record_bind_texture,
.tex_sub_image_2d = record_tex_sub_image_2d,
});
PP_EXPECT(h, status.ok());
PP_EXPECT(h, recorded_binding_calls.size() == 1U);
PP_EXPECT(h, recorded_binding_calls[0].second == 31U);
PP_EXPECT(h, recorded_texture_image_calls.size() == 1U);
PP_EXPECT(h, recorded_texture_image_calls[0].sub_image);
PP_EXPECT(h, recorded_texture_image_calls[0].width == 2);
PP_EXPECT(h, recorded_texture_image_calls[0].height == 2);
PP_EXPECT(h, recorded_texture_image_calls[0].data == pixels.data());
}
void generates_texture_2d_mipmaps_through_dispatch(pp::tests::Harness& h)
{
recorded_binding_calls.clear();
recorded_mipmap_targets.clear();
const auto status = pp::renderer::gl::generate_opengl_texture_2d_mipmaps(
41U,
pp::renderer::gl::OpenGlTexture2DMipmapDispatch {
.bind_texture = record_bind_texture,
.generate_mipmap = record_generate_mipmap,
});
PP_EXPECT(h, status.ok());
PP_EXPECT(h, recorded_binding_calls.size() == 2U);
PP_EXPECT(h, recorded_binding_calls[0].second == 41U);
PP_EXPECT(h, recorded_binding_calls[1].second == 0U);
PP_EXPECT(h, recorded_mipmap_targets.size() == 1U);
PP_EXPECT(h, recorded_mipmap_targets[0] == 0x0DE1U);
}
void reads_back_texture_2d_through_framebuffer_dispatch(pp::tests::Harness& h)
{
recorded_generated_framebuffer_counts.clear();
recorded_deleted_framebuffers.clear();
recorded_binding_calls.clear();
recorded_framebuffer_attachment_calls.clear();
recorded_framebuffer_status_queries.clear();
recorded_read_pixels_calls.clear();
next_framebuffer_id = 44U;
configured_framebuffer_status = 0x8CD5U;
std::array<std::uint8_t, 16> pixels {};
const auto result = pp::renderer::gl::readback_opengl_texture_2d(
pp::renderer::gl::OpenGlTexture2DReadback {
.texture_id = 56U,
.width = 2,
.height = 2,
.format = pp::renderer::gl::rgba8_readback_format(),
.pixels = pixels.data(),
},
pp::renderer::gl::OpenGlTexture2DReadbackDispatch {
.bind_texture = record_bind_texture,
.gen_framebuffers = record_gen_framebuffers,
.get_integer = record_get_integer,
.bind_framebuffer = record_bind_framebuffer,
.framebuffer_texture_2d = record_framebuffer_texture_2d,
.check_framebuffer_status = record_check_framebuffer_status,
.read_pixels = record_read_pixels,
.delete_framebuffers = record_delete_framebuffers,
});
PP_EXPECT(h, result.ok());
PP_EXPECT(h, result.value().pixels_read);
PP_EXPECT(h, result.value().framebuffer_status == 0x8CD5U);
PP_EXPECT(h, recorded_generated_framebuffer_counts.size() == 1U);
PP_EXPECT(h, recorded_generated_framebuffer_counts[0] == 1U);
PP_EXPECT(h, recorded_binding_calls.size() == 3U);
PP_EXPECT(h, recorded_binding_calls[0].kind == RecordedOpenGlBindingCall::Kind::bind_texture);
PP_EXPECT(h, recorded_binding_calls[0].second == 56U);
PP_EXPECT(h, recorded_binding_calls[1].kind == RecordedOpenGlBindingCall::Kind::bind_framebuffer);
PP_EXPECT(h, recorded_binding_calls[1].first == 0x8D40U);
PP_EXPECT(h, recorded_binding_calls[1].second == 44U);
PP_EXPECT(h, recorded_binding_calls[2].kind == RecordedOpenGlBindingCall::Kind::bind_framebuffer);
PP_EXPECT(h, recorded_binding_calls[2].second == 7U);
PP_EXPECT(h, recorded_framebuffer_attachment_calls.size() == 1U);
PP_EXPECT(h, recorded_framebuffer_attachment_calls[0].attachment == 0x8CE0U);
PP_EXPECT(h, recorded_framebuffer_attachment_calls[0].texture == 56U);
PP_EXPECT(h, recorded_read_pixels_calls.size() == 1U);
PP_EXPECT(h, recorded_read_pixels_calls[0].pixels == pixels.data());
PP_EXPECT(h, recorded_deleted_framebuffers.size() == 1U);
PP_EXPECT(h, recorded_deleted_framebuffers[0] == 44U);
}
void reports_incomplete_texture_2d_readback_framebuffer(pp::tests::Harness& h)
{
recorded_deleted_framebuffers.clear();
recorded_read_pixels_calls.clear();
next_framebuffer_id = 45U;
configured_framebuffer_status = 0x8CD6U;
std::array<std::uint8_t, 16> pixels {};
const auto result = pp::renderer::gl::readback_opengl_texture_2d(
pp::renderer::gl::OpenGlTexture2DReadback {
.texture_id = 57U,
.width = 2,
.height = 2,
.format = pp::renderer::gl::rgba8_readback_format(),
.pixels = pixels.data(),
},
pp::renderer::gl::OpenGlTexture2DReadbackDispatch {
.bind_texture = record_bind_texture,
.gen_framebuffers = record_gen_framebuffers,
.get_integer = record_get_integer,
.bind_framebuffer = record_bind_framebuffer,
.framebuffer_texture_2d = record_framebuffer_texture_2d,
.check_framebuffer_status = record_check_framebuffer_status,
.read_pixels = record_read_pixels,
.delete_framebuffers = record_delete_framebuffers,
});
PP_EXPECT(h, result.ok());
PP_EXPECT(h, !result.value().pixels_read);
PP_EXPECT(h, result.value().framebuffer_status == 0x8CD6U);
PP_EXPECT(h, recorded_read_pixels_calls.empty());
PP_EXPECT(h, recorded_deleted_framebuffers.back() == 45U);
}
void rejects_incomplete_texture_2d_readback_dispatch(pp::tests::Harness& h)
{
std::array<std::uint8_t, 4> pixels {};
const auto result = pp::renderer::gl::readback_opengl_texture_2d(
pp::renderer::gl::OpenGlTexture2DReadback {
.texture_id = 1U,
.width = 1,
.height = 1,
.format = pp::renderer::gl::rgba8_readback_format(),
.pixels = pixels.data(),
},
pp::renderer::gl::OpenGlTexture2DReadbackDispatch {
.bind_texture = record_bind_texture,
.gen_framebuffers = record_gen_framebuffers,
});
PP_EXPECT(h, !result.ok());
PP_EXPECT(h, result.status().code == pp::foundation::StatusCode::invalid_argument);
}
void maps_renderer_viewports_and_scissors(pp::tests::Harness& h) void maps_renderer_viewports_and_scissors(pp::tests::Harness& h)
{ {
const auto viewport = pp::renderer::gl::viewport_for_renderer_viewport( const auto viewport = pp::renderer::gl::viewport_for_renderer_viewport(
@@ -1711,6 +2137,14 @@ int main()
harness.run("rejects_incomplete_generic_capability_dispatch", rejects_incomplete_generic_capability_dispatch); harness.run("rejects_incomplete_generic_capability_dispatch", rejects_incomplete_generic_capability_dispatch);
harness.run("applies_buffer_clear_dispatch", applies_buffer_clear_dispatch); harness.run("applies_buffer_clear_dispatch", applies_buffer_clear_dispatch);
harness.run("rejects_incomplete_buffer_clear_dispatch", rejects_incomplete_buffer_clear_dispatch); harness.run("rejects_incomplete_buffer_clear_dispatch", rejects_incomplete_buffer_clear_dispatch);
harness.run("allocates_texture_2d_through_dispatch", allocates_texture_2d_through_dispatch);
harness.run("rejects_invalid_texture_2d_allocation", rejects_invalid_texture_2d_allocation);
harness.run("deletes_and_binds_texture_2d_through_dispatch", deletes_and_binds_texture_2d_through_dispatch);
harness.run("updates_texture_2d_through_dispatch", updates_texture_2d_through_dispatch);
harness.run("generates_texture_2d_mipmaps_through_dispatch", generates_texture_2d_mipmaps_through_dispatch);
harness.run("reads_back_texture_2d_through_framebuffer_dispatch", reads_back_texture_2d_through_framebuffer_dispatch);
harness.run("reports_incomplete_texture_2d_readback_framebuffer", reports_incomplete_texture_2d_readback_framebuffer);
harness.run("rejects_incomplete_texture_2d_readback_dispatch", rejects_incomplete_texture_2d_readback_dispatch);
harness.run("maps_renderer_viewports_and_scissors", maps_renderer_viewports_and_scissors); harness.run("maps_renderer_viewports_and_scissors", maps_renderer_viewports_and_scissors);
harness.run("maps_renderer_blend_state_tokens", maps_renderer_blend_state_tokens); harness.run("maps_renderer_blend_state_tokens", maps_renderer_blend_state_tokens);
harness.run("maps_renderer_color_write_masks", maps_renderer_color_write_masks); harness.run("maps_renderer_color_write_masks", maps_renderer_color_write_masks);