From b146c0168ac619f46d0d7fe53fa919730a93aac2 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Wed, 12 Aug 2026 03:29:23 +1000 Subject: [PATCH] [client] tests: cover render source queue --- client/tests/CMakeLists.txt | 34 ++ client/tests/render_queue_test.c | 647 +++++++++++++++++++++++++++++++ 2 files changed, 681 insertions(+) create mode 100644 client/tests/render_queue_test.c diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index 45cecad9..d9b6dfab 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -144,6 +144,40 @@ foreach(name IN LISTS MOUSE_CASES) ) endforeach() +add_executable(render-queue-tests + render_queue_test.c + ../src/render_queue.c +) +target_compile_definitions(render-queue-tests PRIVATE + CIMGUI_DEFINE_ENUMS_AND_STRUCTS=1 +) +target_include_directories(render-queue-tests PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/../src" + "${PROJECT_TOP}/repos/gui/cimgui" + "${PROJECT_TOP}/repos/gui/cimgui/imgui" +) +target_link_libraries(render-queue-tests + ${EXE_FLAGS} + lg_common +) +set(RENDER_QUEUE_CASES + generation + latest + prepare + presented + payload + validation + cache +) +foreach(name IN LISTS RENDER_QUEUE_CASES) + add_test(NAME render-queue-${name} + COMMAND render-queue-tests ${name} + ) + set_tests_properties(render-queue-${name} PROPERTIES + TIMEOUT 10 + ) +endforeach() + find_package(GTest REQUIRED) find_program(WESTON_EXECUTABLE NAMES weston) if(NOT WESTON_EXECUTABLE) diff --git a/client/tests/render_queue_test.c b/client/tests/render_queue_test.c new file mode 100644 index 00000000..f6bbd9b0 --- /dev/null +++ b/client/tests/render_queue_test.c @@ -0,0 +1,647 @@ +/** + * 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 "main.h" +#include "render_queue.h" +#include "test.h" + +#include "common/debug.h" + +#include +#include +#include +#include +#include + +#define ARRAY_LENGTH(a) (sizeof(a) / sizeof((a)[0])) + +struct AppState g_state; + +struct Fake +{ + LG_Renderer renderer; + struct LG_DisplayServerOps ds; + + bool prepResult; + unsigned int prepCount; + RenderQueueSource prepSource; + uint64_t prepGeneration; + uint64_t prepSerial; + + unsigned int appliedCount; + RenderQueueSource appliedSource; + uint64_t appliedGeneration; + uint64_t appliedSerial; + bool appliedSwSurface; + + unsigned int invalidates; + unsigned int showCount; + bool show; + unsigned int configCount; + int configWidth; + int configHeight; + unsigned int fillCount; + int fillX; + int fillY; + int fillWidth; + int fillHeight; + uint32_t fillColor; + unsigned int bitmapCount; + int bitmapX; + int bitmapY; + int bitmapWidth; + int bitmapHeight; + int bitmapStride; + bool bitmapTopDown; + uint8_t bitmap[64]; + size_t bitmapSize; + + unsigned int shapeCount; + LG_RendererCursor shapeType; + int shapeWidth; + int shapeHeight; + int shapePitch; + uint8_t shape[64]; + size_t shapeSize; + unsigned int cursorCount; + bool cursorVisible; + int cursorX; + int cursorY; + int cursorHX; + int cursorHY; + unsigned int colorCount; + LGColorTransformFlags colorFlags; + float colorScalar; + float colorMatrix; + float colorLut; + unsigned int whiteCount; + uint32_t whiteLevel; + + unsigned int hdrCount; + LG_RendererFormat hdr[8]; +}; + +static struct Fake f; + +void app_invalidateWindow(bool full) +{ + (void)full; + ++f.invalidates; +} + +static bool prep(void * opaque, RenderQueueSource source, + uint64_t generation, uint64_t serial) +{ + struct Fake * fake = opaque; + fake->prepCount++; + fake->prepSource = source; + fake->prepGeneration = generation; + fake->prepSerial = serial; + return fake->prepResult; +} + +static void applied(void * opaque, RenderQueueSource source, + uint64_t generation, uint64_t serial, bool swSurface) +{ + struct Fake * fake = opaque; + fake->appliedCount++; + fake->appliedSource = source; + fake->appliedGeneration = generation; + fake->appliedSerial = serial; + fake->appliedSwSurface = swSurface; +} + +static void swShow(LG_Renderer * renderer, bool show) +{ + (void)renderer; + ++f.showCount; + f.show = show; +} + +static void swConfig(LG_Renderer * renderer, int width, int height) +{ + (void)renderer; + ++f.configCount; + f.configWidth = width; + f.configHeight = height; +} + +static void swFill(LG_Renderer * renderer, int x, int y, + int width, int height, uint32_t color) +{ + (void)renderer; + ++f.fillCount; + f.fillX = x; + f.fillY = y; + f.fillWidth = width; + f.fillHeight = height; + f.fillColor = color; +} + +static void swBitmap(LG_Renderer * renderer, int x, int y, + int width, int height, int stride, uint8_t * data, bool topDown) +{ + (void)renderer; + ++f.bitmapCount; + f.bitmapX = x; + f.bitmapY = y; + f.bitmapWidth = width; + f.bitmapHeight = height; + f.bitmapStride = stride; + f.bitmapTopDown = topDown; + f.bitmapSize = (size_t)height * (size_t)stride; + CHECK(f.bitmapSize <= sizeof(f.bitmap)); + memcpy(f.bitmap, data, f.bitmapSize); +} + +static bool mouseShape(LG_Renderer * renderer, LG_RendererCursor type, + int width, int height, int pitch, const uint8_t * data) +{ + (void)renderer; + ++f.shapeCount; + f.shapeType = type; + f.shapeWidth = width; + f.shapeHeight = height; + f.shapePitch = pitch; + f.shapeSize = (size_t)height * (size_t)pitch; + CHECK(f.shapeSize <= sizeof(f.shape)); + memcpy(f.shape, data, f.shapeSize); + return true; +} + +static bool mouseEvent(LG_Renderer * renderer, bool visible, + int x, int y, int hx, int hy) +{ + (void)renderer; + ++f.cursorCount; + f.cursorVisible = visible; + f.cursorX = x; + f.cursorY = y; + f.cursorHX = hx; + f.cursorHY = hy; + return true; +} + +static void mouseColor(LG_Renderer * renderer, + const LGColorTransform * color) +{ + (void)renderer; + ++f.colorCount; + f.colorFlags = color->flags; + f.colorScalar = color->scalar; + f.colorMatrix = color->matrix[0][0]; + f.colorLut = color->lut[0][0]; +} + +static void mouseWhite(LG_Renderer * renderer, uint32_t level) +{ + (void)renderer; + ++f.whiteCount; + f.whiteLevel = level; +} + +static bool setHDR(const void * data) +{ + CHECK(f.hdrCount < ARRAY_LENGTH(f.hdr)); + f.hdr[f.hdrCount++] = *(const LG_RendererFormat *)data; + return true; +} + +static void start(void) +{ + memset(&f, 0, sizeof(f)); + memset(&g_state, 0, sizeof(g_state)); + + f.prepResult = true; + f.renderer.ops.onMouseShape = mouseShape; + f.renderer.ops.onMouseEvent = mouseEvent; + f.renderer.ops.onMouseColorTransform = mouseColor; + f.renderer.ops.onMouseWhiteLevel = mouseWhite; + f.renderer.ops.swSurfaceConfigure = swConfig; + f.renderer.ops.swSurfaceDrawFill = swFill; + f.renderer.ops.swSurfaceDrawBitmap = swBitmap; + f.renderer.ops.swSurfaceShow = swShow; + f.ds.setHDRImageDesc = setHDR; + g_state.lgr = &f.renderer; + g_state.ds = &f.ds; + + renderQueue_init(); + renderQueue_setSourceFns(prep, applied, &f); +} + +static void stop(void) +{ + renderQueue_setSourceFns(NULL, NULL, NULL); + renderQueue_free(); +} + +static void testGeneration(void) +{ + start(); + + const uint64_t generation = + renderQueue_sourceBegin(RENDER_QUEUE_SOURCE_PRIMARY); + CHECK(generation != 0); + + renderQueue_sourceSwSurfaceDrawFill(RENDER_QUEUE_SOURCE_PRIMARY, + generation, 1, 2, 3, 4, 5); + CHECK(renderQueue_sourceTransition(RENDER_QUEUE_SOURCE_PRIMARY, + generation, true, NULL) != 0); + renderQueue_sourceInvalidate(RENDER_QUEUE_SOURCE_PRIMARY, generation); + renderQueue_process(); + renderQueue_presented(); + + CHECK(f.fillCount == 0); + CHECK(f.prepCount == 0); + CHECK(f.appliedCount == 0); + CHECK(renderQueue_sourceTransition(RENDER_QUEUE_SOURCE_PRIMARY, + generation, false, NULL) == 0); + + const uint64_t current = + renderQueue_sourceBegin(RENDER_QUEUE_SOURCE_PRIMARY); + CHECK(current != 0); + CHECK(current != generation); + renderQueue_sourceInvalidate(RENDER_QUEUE_SOURCE_PRIMARY, generation); + CHECK(renderQueue_sourceTransition(RENDER_QUEUE_SOURCE_PRIMARY, + current, false, NULL) != 0); + renderQueue_process(); + CHECK(f.prepCount == 1); + CHECK(f.prepGeneration == current); + + stop(); +} + +static void testLatest(void) +{ + start(); + + const uint64_t primary = + renderQueue_sourceBegin(RENDER_QUEUE_SOURCE_PRIMARY); + const uint64_t fallback = + renderQueue_sourceBegin(RENDER_QUEUE_SOURCE_FALLBACK); + atomic_uint_least64_t published; + atomic_init(&published, 0); + + const uint64_t old = renderQueue_sourceTransition( + RENDER_QUEUE_SOURCE_PRIMARY, primary, false, &published); + CHECK(old != 0); + CHECK(atomic_load(&published) == old); + const uint64_t latest = renderQueue_sourceTransition( + RENDER_QUEUE_SOURCE_FALLBACK, fallback, true, &published); + CHECK(latest > old); + CHECK(atomic_load(&published) == latest); + + renderQueue_process(); + CHECK(f.prepCount == 1); + CHECK(f.prepSource == RENDER_QUEUE_SOURCE_FALLBACK); + CHECK(f.prepGeneration == fallback); + CHECK(f.prepSerial == latest); + CHECK(f.showCount == 1); + CHECK(f.show); + + renderQueue_presented(); + CHECK(f.appliedCount == 1); + CHECK(f.appliedSource == RENDER_QUEUE_SOURCE_FALLBACK); + CHECK(f.appliedGeneration == fallback); + CHECK(f.appliedSerial == latest); + CHECK(f.appliedSwSurface); + + stop(); +} + +static void testPrepare(void) +{ + start(); + + const uint64_t generation = + renderQueue_sourceBegin(RENDER_QUEUE_SOURCE_PRIMARY); + f.prepResult = false; + CHECK(renderQueue_sourceSwSurfaceConfigureTransition( + RENDER_QUEUE_SOURCE_PRIMARY, generation, 640, 480, NULL) != 0); + renderQueue_process(); + renderQueue_presented(); + + CHECK(f.prepCount == 1); + CHECK(f.configCount == 0); + CHECK(f.fillCount == 0); + CHECK(f.showCount == 0); + CHECK(f.appliedCount == 0); + + f.prepResult = true; + const uint64_t serial = renderQueue_sourceSwSurfaceConfigureTransition( + RENDER_QUEUE_SOURCE_PRIMARY, generation, 320, 200, NULL); + CHECK(serial != 0); + renderQueue_process(); + CHECK(f.configCount == 1); + CHECK(f.configWidth == 320); + CHECK(f.configHeight == 200); + CHECK(f.fillCount == 1); + CHECK(f.fillX == 0); + CHECK(f.fillY == 0); + CHECK(f.fillWidth == 320); + CHECK(f.fillHeight == 200); + CHECK(f.fillColor == 0); + CHECK(f.showCount == 1); + CHECK(f.show); + + renderQueue_presented(); + CHECK(f.appliedCount == 1); + CHECK(f.appliedSerial == serial); + CHECK(f.appliedSwSurface); + + stop(); +} + +static void testPresented(void) +{ + start(); + + const uint64_t primary = + renderQueue_sourceBegin(RENDER_QUEUE_SOURCE_PRIMARY); + const uint64_t fallback = + renderQueue_sourceBegin(RENDER_QUEUE_SOURCE_FALLBACK); + const uint64_t first = renderQueue_sourceTransition( + RENDER_QUEUE_SOURCE_PRIMARY, primary, false, NULL); + CHECK(first != 0); + renderQueue_process(); + CHECK(f.appliedCount == 0); + + const uint64_t latest = renderQueue_sourceTransition( + RENDER_QUEUE_SOURCE_FALLBACK, fallback, false, NULL); + CHECK(latest > first); + renderQueue_process(); + CHECK(f.appliedCount == 0); + + renderQueue_presented(); + CHECK(f.appliedCount == 1); + CHECK(f.appliedSource == RENDER_QUEUE_SOURCE_FALLBACK); + CHECK(f.appliedGeneration == fallback); + CHECK(f.appliedSerial == latest); + renderQueue_presented(); + CHECK(f.appliedCount == 1); + + stop(); +} + +static void testPayload(void) +{ + start(); + + const uint64_t generation = + renderQueue_sourceBegin(RENDER_QUEUE_SOURCE_PRIMARY); + uint8_t bitmap[] = { 1, 2, 3, 4, 5, 6, 7, 8 }; + const uint8_t bitmapExpected[] = { 1, 2, 3, 4, 5, 6, 7, 8 }; + uint8_t shape[] = { 11, 12, 13, 14, 15, 16, 17, 18 }; + const uint8_t shapeExpected[] = { 11, 12, 13, 14, 15, 16, 17, 18 }; + LGColorTransform color = + { + .flags = LG_COLOR_TRANSFORM_MATRIX | LG_COLOR_TRANSFORM_LUT, + .matrix[0][0] = 2.0f, + .scalar = 3.0f, + .lut[0][0] = 4.0f, + }; + + renderQueue_sourceSwSurfaceDrawBitmap(RENDER_QUEUE_SOURCE_PRIMARY, + generation, 5, 6, 2, 2, 4, bitmap, true); + renderQueue_sourceCursorImage(RENDER_QUEUE_SOURCE_PRIMARY, generation, + LG_CURSOR_COLOR, 2, 2, 4, shape); + renderQueue_sourceCursorColorTransform(RENDER_QUEUE_SOURCE_PRIMARY, + generation, &color); + renderQueue_sourceCursorState(RENDER_QUEUE_SOURCE_PRIMARY, generation, + true, 20, 21, 1, 2); + CHECK(renderQueue_sourceTransition(RENDER_QUEUE_SOURCE_PRIMARY, + generation, false, NULL) != 0); + + memset(bitmap, 0, sizeof(bitmap)); + memset(shape, 0, sizeof(shape)); + color.flags = 0; + color.matrix[0][0] = 0.0f; + color.scalar = 0.0f; + color.lut[0][0] = 0.0f; + renderQueue_process(); + + CHECK(f.bitmapCount == 1); + CHECK(f.bitmapX == 5); + CHECK(f.bitmapY == 6); + CHECK(f.bitmapWidth == 2); + CHECK(f.bitmapHeight == 2); + CHECK(f.bitmapStride == 4); + CHECK(f.bitmapTopDown); + CHECK(f.bitmapSize == sizeof(bitmapExpected)); + CHECK(memcmp(f.bitmap, bitmapExpected, sizeof(bitmapExpected)) == 0); + CHECK(f.shapeCount == 1); + CHECK(f.shapeType == LG_CURSOR_COLOR); + CHECK(f.shapeWidth == 2); + CHECK(f.shapeHeight == 2); + CHECK(f.shapePitch == 4); + CHECK(f.shapeSize == sizeof(shapeExpected)); + CHECK(memcmp(f.shape, shapeExpected, sizeof(shapeExpected)) == 0); + CHECK(f.colorCount == 1); + CHECK(f.colorFlags == + (LG_COLOR_TRANSFORM_MATRIX | LG_COLOR_TRANSFORM_LUT)); + CHECK(f.colorMatrix == 2.0f); + CHECK(f.colorScalar == 3.0f); + CHECK(f.colorLut == 4.0f); + CHECK(f.cursorCount == 1); + CHECK(f.cursorVisible); + CHECK(f.cursorX == 20); + CHECK(f.cursorY == 21); + CHECK(f.cursorHX == 1); + CHECK(f.cursorHY == 2); + + stop(); +} + +static void testValidation(void) +{ + start(); + + const uint64_t generation = + renderQueue_sourceBegin(RENDER_QUEUE_SOURCE_PRIMARY); + uint8_t data[16] = { 0 }; + + renderQueue_sourceSwSurfaceDrawBitmap(RENDER_QUEUE_SOURCE_PRIMARY, + generation, 0, 0, 0, 1, 4, data, false); + renderQueue_sourceSwSurfaceDrawBitmap(RENDER_QUEUE_SOURCE_PRIMARY, + generation, 0, 0, 1, 0, 4, data, false); + renderQueue_sourceSwSurfaceDrawBitmap(RENDER_QUEUE_SOURCE_PRIMARY, + generation, 0, 0, 1, 1, 0, data, false); + renderQueue_sourceSwSurfaceDrawBitmap(RENDER_QUEUE_SOURCE_PRIMARY, + generation, 0, 0, 1, 1, 4, NULL, false); + renderQueue_sourceCursorImage(RENDER_QUEUE_SOURCE_PRIMARY, generation, + LG_CURSOR_COLOR, 0, 1, 4, data); + renderQueue_sourceCursorImage(RENDER_QUEUE_SOURCE_PRIMARY, generation, + LG_CURSOR_COLOR, 1, 0, 4, data); + renderQueue_sourceCursorImage(RENDER_QUEUE_SOURCE_PRIMARY, generation, + LG_CURSOR_COLOR, 1, 1, 0, data); + renderQueue_sourceCursorImage(RENDER_QUEUE_SOURCE_PRIMARY, generation, + LG_CURSOR_COLOR, 1, 1, 4, NULL); + renderQueue_sourceCursorState(RENDER_QUEUE_SOURCE_PRIMARY, generation, + true, 1, 2, 3, 4); + CHECK(renderQueue_sourceTransition(RENDER_QUEUE_SOURCE_PRIMARY, + generation, false, NULL) != 0); + renderQueue_process(); + + CHECK(f.bitmapCount == 0); + CHECK(f.shapeCount == 0); + CHECK(f.cursorCount == 1); + CHECK(!f.cursorVisible); + + stop(); +} + +static void testCache(void) +{ + start(); + + const uint64_t generation = + renderQueue_sourceBegin(RENDER_QUEUE_SOURCE_PRIMARY); + const uint8_t shape[] = { 1, 2, 3, 4 }; + LGColorTransform color = + { + .flags = LG_COLOR_TRANSFORM_MATRIX, + .matrix[0][0] = 1.5f, + .scalar = 2.5f, + }; + LG_RendererFormat format = + { + .type = FRAME_TYPE_RGBA, + .hdr = true, + .hdrPQ = true, + .hdrMetadata = true, + .screenWidth = 1920, + .screenHeight = 1080, + .sdrWhiteLevel = 250, + }; + + renderQueue_sourceCursorImage(RENDER_QUEUE_SOURCE_PRIMARY, generation, + LG_CURSOR_COLOR, 1, 1, 4, shape); + renderQueue_sourceCursorState(RENDER_QUEUE_SOURCE_PRIMARY, generation, + true, 30, 31, 2, 3); + renderQueue_sourceCursorColorTransform(RENDER_QUEUE_SOURCE_PRIMARY, + generation, &color); + renderQueue_sourceCursorWhiteLevel(RENDER_QUEUE_SOURCE_PRIMARY, + generation, 250); + renderQueue_sourceSurfaceFormat(RENDER_QUEUE_SOURCE_PRIMARY, generation, + format, true); + renderQueue_process(); + CHECK(f.shapeCount == 0); + CHECK(f.cursorCount == 0); + CHECK(f.colorCount == 0); + CHECK(f.whiteCount == 0); + CHECK(f.hdrCount == 0); + + CHECK(renderQueue_sourceTransition(RENDER_QUEUE_SOURCE_PRIMARY, + generation, false, NULL) != 0); + renderQueue_process(); + CHECK(f.shapeCount == 1); + CHECK(f.cursorCount == 1); + CHECK(f.cursorVisible); + CHECK(f.colorCount == 1); + CHECK(f.colorScalar == 2.5f); + CHECK(f.whiteCount == 1); + CHECK(f.whiteLevel == 250); + CHECK(f.hdrCount == 1); + CHECK(f.hdr[0].type == FRAME_TYPE_RGBA); + CHECK(f.hdr[0].hdr); + CHECK(f.hdr[0].hdrPQ); + CHECK(f.hdr[0].screenWidth == 1920); + + renderQueue_sourceInvalidate(RENDER_QUEUE_SOURCE_PRIMARY, generation); + const uint64_t current = + renderQueue_sourceBegin(RENDER_QUEUE_SOURCE_PRIMARY); + CHECK(current != generation); + CHECK(renderQueue_sourceTransition(RENDER_QUEUE_SOURCE_PRIMARY, + current, false, NULL) != 0); + renderQueue_process(); + CHECK(f.shapeCount == 2); + CHECK(f.cursorCount == 2); + CHECK(f.cursorVisible); + CHECK(f.cursorX == 30); + CHECK(f.cursorY == 31); + CHECK(f.cursorHX == 2); + CHECK(f.cursorHY == 3); + CHECK(f.colorCount == 2); + CHECK(f.colorScalar == 2.5f); + CHECK(f.whiteCount == 2); + CHECK(f.whiteLevel == 250); + CHECK(f.hdrCount == 2); + CHECK(f.hdr[1].type == 0); + CHECK(!f.hdr[1].hdr); + + renderQueue_sourceClearCursor(RENDER_QUEUE_SOURCE_PRIMARY); + const uint64_t clear = + renderQueue_sourceBegin(RENDER_QUEUE_SOURCE_PRIMARY); + CHECK(renderQueue_sourceTransition(RENDER_QUEUE_SOURCE_PRIMARY, + clear, false, NULL) != 0); + renderQueue_process(); + CHECK(f.shapeCount == 2); + CHECK(f.cursorCount == 3); + CHECK(!f.cursorVisible); + CHECK(f.colorCount == 3); + CHECK(f.colorFlags == 0); + CHECK(f.colorScalar == 0.0f); + CHECK(f.whiteCount == 3); + CHECK(f.whiteLevel == LG_SDR_WHITE_LEVEL_DEFAULT); + + stop(); +} + +struct Test +{ + const char * name; + void (*run)(void); +}; + +static const struct Test tests[] = +{ + { "generation", testGeneration }, + { "latest" , testLatest }, + { "prepare" , testPrepare }, + { "presented" , testPresented }, + { "payload" , testPayload }, + { "validation", testValidation }, + { "cache" , testCache }, +}; + +int main(int argc, char ** argv) +{ + debug_init(); + + if (argc == 2) + { + for (size_t i = 0; i < ARRAY_LENGTH(tests); ++i) + if (strcmp(argv[1], tests[i].name) == 0) + { + tests[i].run(); + return 0; + } + + fprintf(stderr, "unknown test: %s\n", argv[1]); + return EXIT_FAILURE; + } + + if (argc != 1) + return EXIT_FAILURE; + + for (size_t i = 0; i < ARRAY_LENGTH(tests); ++i) + tests[i].run(); + return 0; +}