[client] egl: add portable texture upload fallback

This commit is contained in:
Geoffrey McRae
2026-09-12 10:45:04 +10:00
parent 7ea69f3d1c
commit 18541865f0
17 changed files with 1010 additions and 116 deletions

View File

@@ -1007,29 +1007,81 @@ if(ENABLE_EGL)
)
endforeach()
add_executable(texture-dmabuf-tests
texture_dmabuf_test.c
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
add_executable(texture-dmabuf-tests
texture_dmabuf_test.c
)
add_dependencies(texture-dmabuf-tests renderer_EGL)
target_include_directories(texture-dmabuf-tests PRIVATE
"${CMAKE_BINARY_DIR}/renderers/EGL/shader"
)
target_link_libraries(texture-dmabuf-tests
${EXE_FLAGS}
lg_common
)
set(TEXTURE_DMABUF_CASES
select
poll
saturation
release
identity
)
foreach(name IN LISTS TEXTURE_DMABUF_CASES)
add_test(NAME texture-dmabuf-${name}
COMMAND texture-dmabuf-tests ${name}
)
set_tests_properties(texture-dmabuf-${name} PROPERTIES
TIMEOUT 10
)
endforeach()
endif()
add_executable(postprocess-order-tests
postprocess_order_test.c
../renderers/EGL/postprocess_order.c
)
add_dependencies(texture-dmabuf-tests renderer_EGL)
target_include_directories(texture-dmabuf-tests PRIVATE
"${CMAKE_BINARY_DIR}/renderers/EGL/shader"
target_include_directories(postprocess-order-tests PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/../renderers/EGL"
)
target_link_libraries(texture-dmabuf-tests
target_link_libraries(postprocess-order-tests
${EXE_FLAGS}
lg_common
lg_egl_provider
)
set(TEXTURE_DMABUF_CASES
select
poll
saturation
release
identity
add_test(NAME postprocess-order-tests
COMMAND postprocess-order-tests
)
foreach(name IN LISTS TEXTURE_DMABUF_CASES)
add_test(NAME texture-dmabuf-${name}
COMMAND texture-dmabuf-tests ${name}
set_tests_properties(postprocess-order-tests PROPERTIES
TIMEOUT 10
)
add_executable(texture-upload-tests
texture_upload_test.c
../renderers/EGL/texture_buffer.c
)
target_include_directories(texture-upload-tests PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/../renderers/EGL"
"${PROJECT_TOP}/common/include"
)
target_link_libraries(texture-upload-tests
${EXE_FLAGS}
LGProtocol::LGProtocol
lg_egl_provider
)
set(TEXTURE_UPLOAD_CASES
direct
direct-full
direct-rect
persistent
persistent-full
persistent-rect
fallback
)
foreach(name IN LISTS TEXTURE_UPLOAD_CASES)
add_test(NAME texture-upload-${name}
COMMAND texture-upload-tests ${name}
)
set_tests_properties(texture-dmabuf-${name} PROPERTIES
set_tests_properties(texture-upload-${name} PROPERTIES
TIMEOUT 10
)
endforeach()

View File

@@ -0,0 +1,73 @@
/**
* Looking Glass
* Copyright © 2017-2026 The Looking Glass Authors
* https://looking-glass.io
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 59
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "test.h"
#include "../renderers/EGL/postprocess_order.h"
static void checkFilterOrder(EGL_Filter ** filters, EGL_Filter ** expected,
size_t count)
{
for (size_t i = 0; i < count; ++i)
CHECK(filters[i] == expected[i]);
}
int main(void)
{
EGL_Filter first = { .ops = { .id = "first" } };
EGL_Filter second = { .ops = { .id = "second" } };
EGL_Filter third = { .ops = { .id = "third" } };
EGL_Filter unknownA = { .ops = { .id = "unknown-a" } };
EGL_Filter unknownB = { .ops = { .id = "unknown-b" } };
EGL_Filter * filters [] =
{
&unknownA,
&second,
&unknownB,
&first,
&third,
};
EGL_Filter * expected[] =
{
&first,
&second,
&third,
&unknownA,
&unknownB,
};
StringList order = stringlist_new(false);
CHECK(order);
CHECK(stringlist_push(order, "first") == 0);
CHECK(stringlist_push(order, "second") == 1);
CHECK(stringlist_push(order, "third") == 2);
const size_t count = sizeof(filters) / sizeof(*filters);
egl_postProcessOrderFilters(filters, count, order);
checkFilterOrder(filters, expected, count);
stringlist_clear(order);
egl_postProcessOrderFilters(filters, count, order);
checkFilterOrder(filters, expected, count);
stringlist_free(&order);
return EXIT_SUCCESS;
}

View File

@@ -0,0 +1,493 @@
/**
* Looking Glass
* Copyright © 2017-2026 The Looking Glass Authors
* https://looking-glass.io
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 59
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "test.h"
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "../renderers/EGL/texture_buffer.h"
#include "../renderers/EGL/texture_util.c"
struct Trace
{
unsigned int nextBuffer;
unsigned int storage;
unsigned int map;
unsigned int unmap;
unsigned int deleted;
unsigned int bufferBinds;
unsigned int textureBinds;
unsigned int uploads;
unsigned int fences;
unsigned int finishes;
unsigned int flushes;
GLuint boundPBO;
GLuint boundTexture;
GLint unpackAlignment;
GLint unpackRowLength;
GLint uploadX;
GLint uploadY;
GLsizei uploadWidth;
GLsizei uploadHeight;
const void * uploadData;
bool failMap;
};
static struct Trace trace;
static uint8_t mappedStorage[4096];
struct EGLDynProcs g_egl_dynProcs;
_Thread_local EGL_StateCache g_eglState;
void debug_level(enum DebugLevel level, const char * file, unsigned int line,
const char * function, const char * format, ...)
{
}
static void fakeBufferStorage(GLenum target, GLsizeiptr size,
const void * data, GLbitfield flags)
{
CHECK(target == GL_PIXEL_UNPACK_BUFFER);
CHECK(size > 0);
CHECK(!data);
CHECK(flags & GL_MAP_PERSISTENT_BIT_EXT);
++trace.storage;
}
void glGenBuffers(GLsizei count, GLuint * buffers)
{
for (GLsizei i = 0; i < count; ++i)
buffers[i] = ++trace.nextBuffer;
}
void glBindBuffer(GLenum target, GLuint buffer)
{
if (target == GL_PIXEL_UNPACK_BUFFER)
{
trace.boundPBO = buffer;
++trace.bufferBinds;
}
}
void glActiveTexture(GLenum texture)
{
CHECK(texture == GL_TEXTURE0);
}
void glBindTexture(GLenum target, GLuint texture)
{
CHECK(target == GL_TEXTURE_2D);
trace.boundTexture = texture;
++trace.textureBinds;
}
void glPixelStorei(GLenum pname, GLint param)
{
switch (pname)
{
case GL_UNPACK_ALIGNMENT:
trace.unpackAlignment = param;
break;
case GL_UNPACK_ROW_LENGTH:
trace.unpackRowLength = param;
break;
default:
CHECK(false);
}
}
void glTexSubImage2D(GLenum target, GLint level, GLint xoffset,
GLint yoffset, GLsizei width, GLsizei height, GLenum format,
GLenum type, const void * pixels)
{
CHECK(target == GL_TEXTURE_2D);
CHECK(level == 0);
CHECK(format == GL_RGBA);
CHECK(type == GL_UNSIGNED_BYTE);
trace.uploadX = xoffset;
trace.uploadY = yoffset;
trace.uploadWidth = width;
trace.uploadHeight = height;
trace.uploadData = pixels;
++trace.uploads;
}
GLsync glFenceSync(GLenum condition, GLbitfield flags)
{
CHECK(condition == GL_SYNC_GPU_COMMANDS_COMPLETE);
CHECK(flags == 0);
++trace.fences;
return (GLsync)(uintptr_t)1;
}
void glFinish(void)
{
++trace.finishes;
}
void glFlush(void)
{
++trace.flushes;
}
void * glMapBufferRange(GLenum target, GLintptr offset, GLsizeiptr length,
GLbitfield access)
{
CHECK(target == GL_PIXEL_UNPACK_BUFFER);
CHECK(offset == 0);
CHECK(length <= (GLsizeiptr)sizeof(mappedStorage));
CHECK(access & GL_MAP_PERSISTENT_BIT_EXT);
++trace.map;
return trace.failMap ? NULL : mappedStorage;
}
GLboolean glUnmapBuffer(GLenum target)
{
CHECK(target == GL_PIXEL_UNPACK_BUFFER);
++trace.unmap;
return GL_TRUE;
}
void glDeleteBuffers(GLsizei count, const GLuint * buffers)
{
trace.deleted += count;
}
void egl_stateInvalidateShared(void)
{
memset(&g_eglState, 0, sizeof(g_eglState));
}
const char * gl_getErrorStr(void)
{
return "fake";
}
static void reset(void)
{
memset(&trace, 0, sizeof(trace));
memset(&g_eglState, 0, sizeof(g_eglState));
memset(&g_egl_dynProcs, 0, sizeof(g_egl_dynProcs));
g_egl_dynProcs.glBufferStorageEXT = fakeBufferStorage;
}
static EGL_TexFormat testFormat(void)
{
return (EGL_TexFormat)
{
.dataSize = 1024,
};
}
static void initTexture(TextureBuffer * texture, EGL_TexBufferMode mode)
{
memset(texture, 0, sizeof(*texture));
texture->base.format.width = 8;
texture->base.format.height = 4;
texture->base.format.stride = 10;
texture->base.format.pitch = 40;
texture->base.format.bpp = 4;
texture->base.format.format = GL_RGBA;
texture->base.format.dataType = GL_UNSIGNED_BYTE;
texture->base.frameToken = LG_RENDERER_FRAME_TOKEN_NONE;
texture->texCount = 1;
texture->tex[0] = 17;
texture->buf[0].mode = mode;
texture->buf[0].pbo =
mode == EGL_TEX_BUFFER_MODE_PERSISTENT ? 23 : 0;
texture->buf[0].map = mappedStorage;
texture->buf[0].size = sizeof(mappedStorage);
texture->buf[0].updated = true;
texture->slotToken[0] = 42;
texture->rIndex = -1;
LG_LOCK_INIT(texture->copyLock);
}
static void checkLockReleased(TextureBuffer * texture)
{
const bool locked = atomic_flag_test_and_set_explicit(
&texture->copyLock, memory_order_acquire);
CHECK(!locked);
atomic_flag_clear_explicit(&texture->copyLock, memory_order_release);
}
static void testDirect(void)
{
reset();
egl_texUtilSetBufferMode(EGL_TEX_BUFFER_MODE_DIRECT);
EGL_TexBuffer buffers[2] = { 0 };
const EGL_TexFormat format = testFormat();
CHECK(egl_texUtilGenBuffers(&format, buffers, 2));
CHECK(buffers[0].mode == EGL_TEX_BUFFER_MODE_DIRECT);
CHECK(buffers[1].mode == EGL_TEX_BUFFER_MODE_DIRECT);
CHECK(buffers[0].map);
CHECK(buffers[1].map);
CHECK(!buffers[0].pbo);
CHECK(!buffers[1].pbo);
CHECK(trace.storage == 0);
CHECK(trace.map == 0);
egl_texUtilFreeBuffers(buffers, 2);
CHECK(!buffers[0].map);
CHECK(!buffers[1].map);
CHECK(trace.deleted == 0);
}
static void testPersistent(void)
{
reset();
egl_texUtilSetBufferMode(EGL_TEX_BUFFER_MODE_PERSISTENT);
EGL_TexBuffer buffer = { 0 };
const EGL_TexFormat format = testFormat();
CHECK(egl_texUtilGenBuffers(&format, &buffer, 1));
CHECK(buffer.mode == EGL_TEX_BUFFER_MODE_PERSISTENT);
CHECK(buffer.map == mappedStorage);
CHECK(buffer.pbo == 1);
CHECK(trace.storage == 1);
CHECK(trace.map == 1);
egl_texUtilFreeBuffers(&buffer, 1);
CHECK(!buffer.map);
CHECK(!buffer.pbo);
CHECK(trace.unmap == 1);
CHECK(trace.deleted == 1);
}
static void testFallback(void)
{
reset();
trace.failMap = true;
egl_texUtilSetBufferMode(EGL_TEX_BUFFER_MODE_PERSISTENT);
EGL_TexBuffer buffer = { 0 };
const EGL_TexFormat format = testFormat();
CHECK(egl_texUtilGenBuffers(&format, &buffer, 1));
CHECK(buffer.mode == EGL_TEX_BUFFER_MODE_DIRECT);
CHECK(buffer.map);
CHECK(!buffer.pbo);
CHECK(trace.storage == 1);
CHECK(trace.map == 1);
CHECK(trace.unmap == 0);
CHECK(trace.deleted == 1);
egl_texUtilFreeBuffers(&buffer, 1);
CHECK(!buffer.map);
reset();
EGL_TexBuffer next = { 0 };
CHECK(egl_texUtilGenBuffers(&format, &next, 1));
CHECK(next.mode == EGL_TEX_BUFFER_MODE_PERSISTENT);
CHECK(next.map == mappedStorage);
CHECK(next.pbo == 1);
egl_texUtilFreeBuffers(&next, 1);
}
static void testDirectFull(void)
{
reset();
TextureBuffer texture;
initTexture(&texture, EGL_TEX_BUFFER_MODE_DIRECT);
texture.damage[0].full = true;
CHECK(egl_texBufferStreamProcess(&texture.base, 42) ==
EGL_TEX_STATUS_UPDATED);
CHECK(trace.bufferBinds == 1);
CHECK(trace.boundPBO == 0);
CHECK(trace.textureBinds == 1);
CHECK(trace.boundTexture == texture.tex[0]);
CHECK(trace.unpackAlignment == 1);
CHECK(trace.unpackRowLength == (GLint)texture.base.format.stride);
CHECK(trace.uploads == 1);
CHECK(trace.uploadX == 0);
CHECK(trace.uploadY == 0);
CHECK(trace.uploadWidth == (GLsizei)texture.base.format.width);
CHECK(trace.uploadHeight == (GLsizei)texture.base.format.height);
CHECK(trace.uploadData == mappedStorage);
CHECK(trace.fences == 0);
CHECK(trace.finishes == 0);
CHECK(trace.flushes == 0);
CHECK(!texture.buf[0].updated);
CHECK(!texture.damage[0].full);
CHECK(texture.damage[0].count == 0);
CHECK(texture.rIndex == 0);
CHECK(texture.bufIndex == 0);
CHECK(texture.base.frameToken == 42);
checkLockReleased(&texture);
}
static void testDirectRect(void)
{
reset();
TextureBuffer texture;
initTexture(&texture, EGL_TEX_BUFFER_MODE_DIRECT);
texture.damage[0].count = 1;
texture.damage[0].rects[0].x = 2;
texture.damage[0].rects[0].y = 1;
texture.damage[0].rects[0].width = 3;
texture.damage[0].rects[0].height = 2;
const size_t offset =
texture.damage[0].rects[0].y * texture.base.format.pitch +
texture.damage[0].rects[0].x * texture.base.format.bpp;
CHECK(egl_texBufferStreamProcess(&texture.base, 42) ==
EGL_TEX_STATUS_UPDATED);
CHECK(trace.bufferBinds == 1);
CHECK(trace.boundPBO == 0);
CHECK(trace.textureBinds == 1);
CHECK(trace.boundTexture == texture.tex[0]);
CHECK(trace.unpackAlignment == 1);
CHECK(trace.unpackRowLength == (GLint)texture.base.format.stride);
CHECK(trace.uploads == 1);
CHECK(trace.uploadX == 2);
CHECK(trace.uploadY == 1);
CHECK(trace.uploadWidth == 3);
CHECK(trace.uploadHeight == 2);
CHECK(trace.uploadData == mappedStorage + offset);
CHECK(trace.fences == 0);
CHECK(trace.finishes == 0);
CHECK(trace.flushes == 0);
CHECK(!texture.buf[0].updated);
CHECK(!texture.damage[0].full);
CHECK(texture.damage[0].count == 0);
CHECK(texture.rIndex == 0);
CHECK(texture.bufIndex == 0);
CHECK(texture.base.frameToken == 42);
checkLockReleased(&texture);
}
static void testPersistentFull(void)
{
reset();
TextureBuffer texture;
initTexture(&texture, EGL_TEX_BUFFER_MODE_PERSISTENT);
texture.damage[0].full = true;
CHECK(egl_texBufferStreamProcess(&texture.base, 42) ==
EGL_TEX_STATUS_UPDATED);
CHECK(trace.bufferBinds == 1);
CHECK(trace.boundPBO == texture.buf[0].pbo);
CHECK(trace.textureBinds == 1);
CHECK(trace.boundTexture == texture.tex[0]);
CHECK(trace.unpackAlignment == 1);
CHECK(trace.unpackRowLength == (GLint)texture.base.format.stride);
CHECK(trace.uploads == 1);
CHECK(trace.uploadX == 0);
CHECK(trace.uploadY == 0);
CHECK(trace.uploadWidth == (GLsizei)texture.base.format.width);
CHECK(trace.uploadHeight == (GLsizei)texture.base.format.height);
CHECK(!trace.uploadData);
CHECK(trace.fences == 1);
CHECK(trace.finishes == 0);
CHECK(trace.flushes == 1);
CHECK(texture.sync[0] == (GLsync)(uintptr_t)1);
CHECK(!texture.buf[0].updated);
CHECK(!texture.damage[0].full);
CHECK(texture.damage[0].count == 0);
CHECK(texture.rIndex == 0);
CHECK(texture.bufIndex == 0);
CHECK(texture.base.frameToken == 42);
checkLockReleased(&texture);
}
static void testPersistentRect(void)
{
reset();
TextureBuffer texture;
initTexture(&texture, EGL_TEX_BUFFER_MODE_PERSISTENT);
texture.damage[0].count = 1;
texture.damage[0].rects[0].x = 2;
texture.damage[0].rects[0].y = 1;
texture.damage[0].rects[0].width = 3;
texture.damage[0].rects[0].height = 2;
const uintptr_t offset =
texture.damage[0].rects[0].y * texture.base.format.pitch +
texture.damage[0].rects[0].x * texture.base.format.bpp;
CHECK(egl_texBufferStreamProcess(&texture.base, 42) ==
EGL_TEX_STATUS_UPDATED);
CHECK(trace.bufferBinds == 1);
CHECK(trace.boundPBO == texture.buf[0].pbo);
CHECK(trace.textureBinds == 1);
CHECK(trace.boundTexture == texture.tex[0]);
CHECK(trace.unpackAlignment == 1);
CHECK(trace.unpackRowLength == (GLint)texture.base.format.stride);
CHECK(trace.uploads == 1);
CHECK(trace.uploadX == 2);
CHECK(trace.uploadY == 1);
CHECK(trace.uploadWidth == 3);
CHECK(trace.uploadHeight == 2);
CHECK(trace.uploadData == (const void *)offset);
CHECK(trace.fences == 1);
CHECK(trace.finishes == 0);
CHECK(trace.flushes == 1);
CHECK(texture.sync[0] == (GLsync)(uintptr_t)1);
CHECK(!texture.buf[0].updated);
CHECK(!texture.damage[0].full);
CHECK(texture.damage[0].count == 0);
CHECK(texture.rIndex == 0);
CHECK(texture.bufIndex == 0);
CHECK(texture.base.frameToken == 42);
checkLockReleased(&texture);
}
struct Test
{
const char * name;
void (*run)(void);
};
static const struct Test tests[] =
{
{ "direct" , testDirect },
{ "persistent" , testPersistent },
{ "fallback" , testFallback },
{ "direct-full" , testDirectFull },
{ "direct-rect" , testDirectRect },
{ "persistent-full" , testPersistentFull },
{ "persistent-rect" , testPersistentRect },
};
int main(int argc, char ** argv)
{
CHECK(argc == 2);
for (unsigned int i = 0; i < sizeof(tests) / sizeof(tests[0]); ++i)
if (!strcmp(argv[1], tests[i].name))
{
tests[i].run();
return EXIT_SUCCESS;
}
return EXIT_FAILURE;
}