[client] tests: add framebuffer rendering validation

Add an optional EGL framebuffer capture path for the test transport.
Capture the fully composed frame before presentation and retain its
surface format and HDR state for validation.

Add GoogleTest integration tests which run the production client
under a headless Weston compositor and compare every output pixel
against an independent reference implementation.

The matrix covers BGRA, RGBA, BGR32, RGB24, PQ RGBA10 and scRGB
RGBA16F with full, moving, overlapping, maximum-count, invalid, null
and zero-area damage rectangles. Validate HDR-to-SDR conversion and
native HDR output metadata when supported by the compositor.

Keep tests opt-in through ENABLE_RENDER_TESTS and document how to run
the software and native HDR tiers.
This commit is contained in:
Geoffrey McRae
2026-07-30 14:53:38 +10:00
parent a1fc3d20d3
commit 74ae4f543e
11 changed files with 1172 additions and 6 deletions

View File

@@ -236,3 +236,47 @@ jobs:
run: | run: |
cd doc cd doc
make dirhtml SPHINXOPTS='-b spelling -W' -j$(nproc) make dirhtml SPHINXOPTS='-b spelling -W' -j$(nproc)
render-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install \
weston libgtest-dev libgl1-mesa-dri \
libdw-dev libunwind-dev \
libspice-protocol-dev nettle-dev \
libgl-dev libgles-dev \
libx11-dev libxss-dev libxi-dev libxinerama-dev \
libxcursor-dev libxpresent-dev \
libwayland-dev libxkbcommon-dev libfontconfig-dev \
libsamplerate0-dev libpipewire-0.3-dev libpulse-dev
- name: Configure
run: |
cmake -S client -B client/build-render-tests \
-DOPTIMIZE_FOR_NATIVE=OFF \
-DENABLE_LIBDECOR=OFF \
-DENABLE_TEST_TRANSPORT=ON \
-DENABLE_RENDER_TESTS=ON
- name: Build
run: cmake --build client/build-render-tests --parallel
- name: Run rendering matrix
run: |
ctest --test-dir client/build-render-tests \
--output-on-failure \
-R looking-glass-render-tests
- name: Upload failed captures
if: failure()
uses: actions/upload-artifact@v4
with:
name: render-test-failures
path: /tmp/lg-render-case.*

View File

@@ -36,6 +36,19 @@ add_feature_info(ENABLE_EGL ENABLE_EGL "EGL renderer.")
option(ENABLE_TEST_TRANSPORT "Enable the test transport" OFF) option(ENABLE_TEST_TRANSPORT "Enable the test transport" OFF)
add_feature_info(ENABLE_TEST_TRANSPORT ENABLE_TEST_TRANSPORT add_feature_info(ENABLE_TEST_TRANSPORT ENABLE_TEST_TRANSPORT
"Synthetic frames for graphics-pipeline validation.") "Synthetic frames for graphics-pipeline validation.")
if(ENABLE_TEST_TRANSPORT)
add_definitions(-D ENABLE_TEST_TRANSPORT)
endif()
option(ENABLE_RENDER_TESTS "Build framebuffer rendering integration tests" OFF)
add_feature_info(ENABLE_RENDER_TESTS ENABLE_RENDER_TESTS
"Pixel-accurate EGL rendering tests using the test transport.")
if(ENABLE_RENDER_TESTS AND NOT ENABLE_TEST_TRANSPORT)
message(FATAL_ERROR "ENABLE_RENDER_TESTS requires ENABLE_TEST_TRANSPORT")
endif()
if(ENABLE_RENDER_TESTS AND NOT ENABLE_EGL)
message(FATAL_ERROR "ENABLE_RENDER_TESTS requires ENABLE_EGL")
endif()
option(ENABLE_BACKTRACE "Enable backtrace support on crash" ON) option(ENABLE_BACKTRACE "Enable backtrace support on crash" ON)
add_feature_info(ENABLE_BACKTRACE ENABLE_BACKTRACE "Backtrace support.") add_feature_info(ENABLE_BACKTRACE ENABLE_BACKTRACE "Backtrace support.")
@@ -205,6 +218,12 @@ target_link_libraries(looking-glass-client
cimgui cimgui
) )
if(ENABLE_RENDER_TESTS)
include(CTest)
enable_testing()
add_subdirectory(tests)
endif()
if (ENABLE_PIPEWIRE OR ENABLE_PULSEAUDIO) if (ENABLE_PIPEWIRE OR ENABLE_PULSEAUDIO)
add_definitions(-D ENABLE_AUDIO) add_definitions(-D ENABLE_AUDIO)
add_subdirectory(audiodevs) add_subdirectory(audiodevs)

View File

@@ -19,6 +19,7 @@
*/ */
#pragma once #pragma once
#include <stddef.h>
#include <stdint.h> #include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
@@ -110,6 +111,28 @@ typedef struct LG_RendererRect
} }
LG_RendererRect; LG_RendererRect;
typedef enum LG_RendererCaptureFormat
{
LG_CAPTURE_RGBA8,
LG_CAPTURE_RGB10_A2,
LG_CAPTURE_RGBA32F,
}
LG_RendererCaptureFormat;
typedef struct LG_RendererCapture
{
unsigned int width;
unsigned int height;
size_t stride;
size_t dataSize;
LG_RendererCaptureFormat format;
bool hdr;
bool hdrPQ;
bool nativeHDR;
void * data;
}
LG_RendererCapture;
typedef enum LG_RendererCursor typedef enum LG_RendererCursor
{ {
LG_CURSOR_COLOR , LG_CURSOR_COLOR ,
@@ -229,6 +252,11 @@ typedef struct LG_RendererOps
const bool newFrame, const bool invalidateWindow, const bool newFrame, const bool invalidateWindow,
void (*preSwap)(void * udata), void * udata); void (*preSwap)(void * udata), void * udata);
/* Optional test/diagnostic readback of the fully composed framebuffer.
* Called on the render thread before swap while the graphics context is
* current. The caller owns capture->data and must free it. */
bool (*capture)(LG_Renderer * renderer, LG_RendererCapture * capture);
/* called to create a texture from the specified 32-bit RGB image data. This /* called to create a texture from the specified 32-bit RGB image data. This
* method is for use with Dear ImGui * method is for use with Dear ImGui
* Context: renderThread */ * Context: renderThread */

View File

@@ -0,0 +1,49 @@
/**
* 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.
*/
#pragma once
#include <stdint.h>
#define LG_TEST_CAPTURE_MAGIC UINT64_C(0x4c47434150545552)
#define LG_TEST_CAPTURE_VERSION 1
typedef enum LG_TestCaptureFormat
{
LG_TEST_CAPTURE_RGBA8,
LG_TEST_CAPTURE_RGB10_A2,
LG_TEST_CAPTURE_RGBA32F,
}
LG_TestCaptureFormat;
enum
{
LG_TEST_CAPTURE_HDR = 0x1,
LG_TEST_CAPTURE_HDR_PQ = 0x2,
LG_TEST_CAPTURE_NATIVE_HDR = 0x4,
LG_TEST_CAPTURE_BOTTOM_UP = 0x8,
};
typedef struct LG_TestCaptureHeader
{
uint64_t magic;
uint32_t version;
uint32_t headerSize;
uint64_t frameSerial;
uint32_t sourceType;
uint32_t captureFormat;
uint32_t width;
uint32_t height;
uint32_t stride;
uint32_t flags;
uint64_t dataSize;
}
LG_TestCaptureHeader;

View File

@@ -39,6 +39,7 @@
#include "generator/output/cimgui_impl.h" #include "generator/output/cimgui_impl.h"
#include <math.h> #include <math.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include "egl_dynprocs.h" #include "egl_dynprocs.h"
@@ -131,6 +132,7 @@ struct Inst
bool surfaceSupportsPQ; bool surfaceSupportsPQ;
bool surfaceSupportsSCRGB; bool surfaceSupportsSCRGB;
LG_RendererCaptureFormat captureFormat;
bool hdr; // true if current frame format is HDR bool hdr; // true if current frame format is HDR
bool nativeHDR; // true only while the surface HDR description is active bool nativeHDR; // true only while the surface HDR description is active
bool nativeHDRPQ; bool nativeHDRPQ;
@@ -977,6 +979,8 @@ static bool egl_renderStartup(LG_Renderer * renderer, bool useDMA)
this->surfaceSupportsPQ = configs[chosen].pq; this->surfaceSupportsPQ = configs[chosen].pq;
this->surfaceSupportsSCRGB = configs[chosen].scRGB; this->surfaceSupportsSCRGB = configs[chosen].scRGB;
this->captureFormat = chosen == 0 ? LG_CAPTURE_RGBA32F :
chosen == 1 ? LG_CAPTURE_RGB10_A2 : LG_CAPTURE_RGBA8;
DEBUG_INFO("EGL config: %s%s", configDesc, DEBUG_INFO("EGL config: %s%s", configDesc,
this->surfaceSupportsPQ ? " (HDR capable)" : ""); this->surfaceSupportsPQ ? " (HDR capable)" : "");
@@ -1570,6 +1574,71 @@ static bool egl_render(LG_Renderer * renderer, LG_RendererRotate rotate,
return true; return true;
} }
static bool egl_capture(LG_Renderer * renderer, LG_RendererCapture * capture)
{
struct Inst * this = UPCAST(struct Inst, renderer);
if (!capture || this->width <= 0 || this->height <= 0)
return false;
size_t bytesPerPixel;
GLenum dataType;
switch (this->captureFormat)
{
case LG_CAPTURE_RGBA8:
bytesPerPixel = 4;
dataType = GL_UNSIGNED_BYTE;
break;
case LG_CAPTURE_RGB10_A2:
bytesPerPixel = 4;
dataType = GL_UNSIGNED_INT_2_10_10_10_REV;
break;
case LG_CAPTURE_RGBA32F:
bytesPerPixel = sizeof(float) * 4;
dataType = GL_FLOAT;
break;
default:
return false;
}
if ((size_t)this->width > SIZE_MAX / bytesPerPixel ||
(size_t)this->height >
SIZE_MAX / ((size_t)this->width * bytesPerPixel))
return false;
const size_t stride = (size_t)this->width * bytesPerPixel;
const size_t dataSize = stride * this->height;
void * data = malloc(dataSize);
if (!data)
return false;
while (glGetError() != GL_NO_ERROR)
;
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(0, 0, this->width, this->height, GL_RGBA, dataType, data);
if (glGetError() != GL_NO_ERROR)
{
free(data);
DEBUG_ERROR("Failed to read the composed EGL framebuffer");
return false;
}
*capture = (LG_RendererCapture) {
.width = this->width,
.height = this->height,
.stride = stride,
.dataSize = dataSize,
.format = this->captureFormat,
.hdr = this->format.hdr,
.hdrPQ = this->format.hdrPQ,
.nativeHDR = this->nativeHDR,
.data = data,
};
return true;
}
static void * egl_createTexture(LG_Renderer * renderer, static void * egl_createTexture(LG_Renderer * renderer,
int width, int height, uint8_t * data) int width, int height, uint8_t * data)
{ {
@@ -1661,6 +1730,7 @@ struct LG_RendererOps LGR_EGL =
.onFrame = egl_onFrame, .onFrame = egl_onFrame,
.renderStartup = egl_renderStartup, .renderStartup = egl_renderStartup,
.render = egl_render, .render = egl_render,
.capture = egl_capture,
.createTexture = egl_createTexture, .createTexture = egl_createTexture,
.freeTexture = egl_freeTexture, .freeTexture = egl_freeTexture,

View File

@@ -30,10 +30,12 @@
#include <fcntl.h> #include <fcntl.h>
#include <unistd.h> #include <unistd.h>
#include <errno.h> #include <errno.h>
#include <inttypes.h>
#include <string.h> #include <string.h>
#include <stdint.h> #include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
#include <math.h> #include <math.h>
#include <stdlib.h>
#include <stdatomic.h> #include <stdatomic.h>
#include <linux/input.h> #include <linux/input.h>
@@ -49,6 +51,7 @@
#include "common/paths.h" #include "common/paths.h"
#include "common/cpuinfo.h" #include "common/cpuinfo.h"
#include "common/ll.h" #include "common/ll.h"
#include "common/option.h"
#include "common/proctitle.h" #include "common/proctitle.h"
#include "message.h" #include "message.h"
@@ -66,6 +69,16 @@
#include "render_queue.h" #include "render_queue.h"
#include "evdev.h" #include "evdev.h"
#ifdef ENABLE_TEST_TRANSPORT
#include "interface/test_capture.h"
_Static_assert((int)LG_CAPTURE_RGBA8 == (int)LG_TEST_CAPTURE_RGBA8,
"capture format mismatch");
_Static_assert((int)LG_CAPTURE_RGB10_A2 == (int)LG_TEST_CAPTURE_RGB10_A2,
"capture format mismatch");
_Static_assert((int)LG_CAPTURE_RGBA32F == (int)LG_TEST_CAPTURE_RGBA32F,
"capture format mismatch");
#endif
// forwards // forwards
static int renderThread(void * unused); static int renderThread(void * unused);
@@ -86,6 +99,23 @@ struct CursorState g_cursor;
// this structure is initialized in config.c // this structure is initialized in config.c
struct AppParams g_params = { 0 }; struct AppParams g_params = { 0 };
#ifdef ENABLE_TEST_TRANSPORT
static struct
{
const char * path;
uint64_t targetSerial;
uint64_t lastSerial;
unsigned delay;
unsigned stableRenders;
bool enabled;
bool complete;
}
l_testCapture;
static atomic_uint_least64_t l_testFrameSerial;
static _Atomic(FrameType) l_testFrameType;
#endif
static void lgInit(void) static void lgInit(void)
{ {
g_state.formatValid = false; g_state.formatValid = false;
@@ -174,6 +204,74 @@ static void preSwapCallback(void * udata)
const uint64_t * renderStart = (const uint64_t *)udata; const uint64_t * renderStart = (const uint64_t *)udata;
ringbuffer_push(g_state.renderDuration, ringbuffer_push(g_state.renderDuration,
&(float) {(nanotime() - *renderStart) * 1e-6f}); &(float) {(nanotime() - *renderStart) * 1e-6f});
#ifdef ENABLE_TEST_TRANSPORT
if (!l_testCapture.enabled || l_testCapture.complete)
return;
const uint64_t serial =
atomic_load_explicit(&l_testFrameSerial, memory_order_acquire);
if (serial < l_testCapture.targetSerial)
return;
if (l_testCapture.lastSerial != serial)
{
l_testCapture.lastSerial = serial;
l_testCapture.stableRenders = 0;
return;
}
if (l_testCapture.stableRenders++ < l_testCapture.delay)
return;
l_testCapture.complete = true;
LG_RendererCapture capture;
if (!g_state.lgr->ops.capture ||
!RENDERER(capture, &capture))
{
DEBUG_ERROR("The selected renderer cannot capture its framebuffer");
app_setState(APP_STATE_SHUTDOWN);
return;
}
const FrameType sourceType =
atomic_load_explicit(&l_testFrameType, memory_order_acquire);
const LG_TestCaptureHeader header = {
.magic = LG_TEST_CAPTURE_MAGIC,
.version = LG_TEST_CAPTURE_VERSION,
.headerSize = sizeof(header),
.frameSerial = serial,
.sourceType = sourceType,
.captureFormat = capture.format,
.width = capture.width,
.height = capture.height,
.stride = capture.stride,
.flags = (capture.hdr ? LG_TEST_CAPTURE_HDR : 0) |
(capture.hdrPQ ? LG_TEST_CAPTURE_HDR_PQ : 0) |
(capture.nativeHDR ? LG_TEST_CAPTURE_NATIVE_HDR : 0) |
LG_TEST_CAPTURE_BOTTOM_UP,
.dataSize = capture.dataSize,
};
FILE * file = fopen(l_testCapture.path, "wb");
bool written = false;
if (file)
{
written =
fwrite(&header, sizeof(header), 1, file) == 1 &&
fwrite(capture.data, capture.dataSize, 1, file) == 1;
if (fclose(file) != 0)
written = false;
}
free(capture.data);
if (!written)
DEBUG_ERROR("Failed to write test capture to: %s", l_testCapture.path);
else
DEBUG_INFO("Wrote test capture for frame %" PRIu64 " to: %s",
serial, l_testCapture.path);
app_setState(APP_STATE_SHUTDOWN);
#endif
} }
static int renderThread(void * unused) static int renderThread(void * unused)
@@ -608,6 +706,10 @@ int main_frameThread(void * unused)
g_state.formatValid = true; g_state.formatValid = true;
formatVersion = format->version; formatVersion = format->version;
#ifdef ENABLE_TEST_TRANSPORT
atomic_store_explicit(&l_testFrameType, format->type,
memory_order_release);
#endif
DEBUG_INFO("Format: %s %ux%u (%ux%u) stride:%u pitch:%u rotation:%d hdr:%d pq:%d sdrWhite:%u nits", DEBUG_INFO("Format: %s %ux%u (%ux%u) stride:%u pitch:%u rotation:%d hdr:%d pq:%d sdrWhite:%u nits",
FrameTypeStr[format->type], format->frameWidth, format->frameHeight, FrameTypeStr[format->type], format->frameWidth, format->frameHeight,
format->dataWidth, format->dataHeight, format->stride, format->pitch, format->dataWidth, format->dataHeight, format->stride, format->pitch,
@@ -694,6 +796,10 @@ int main_frameThread(void * unused)
g_state.lastFrameTimeValid = true; g_state.lastFrameTimeValid = true;
atomic_fetch_add_explicit(&g_state.frameCount, 1, memory_order_relaxed); atomic_fetch_add_explicit(&g_state.frameCount, 1, memory_order_relaxed);
#ifdef ENABLE_TEST_TRANSPORT
atomic_store_explicit(&l_testFrameSerial, frame.serial,
memory_order_release);
#endif
if (g_state.jitRender) if (g_state.jitRender)
{ {
if (atomic_load_explicit(&g_state.pendingCount, memory_order_acquire) < 10) if (atomic_load_explicit(&g_state.pendingCount, memory_order_acquire) < 10)
@@ -1095,6 +1201,32 @@ static int transportSessionProbe(void * opaque)
static int lg_run(void) static int lg_run(void)
{ {
#ifdef ENABLE_TEST_TRANSPORT
memset(&l_testCapture, 0, sizeof(l_testCapture));
atomic_store_explicit(&l_testFrameSerial, 0, memory_order_relaxed);
atomic_store_explicit(&l_testFrameType, FRAME_TYPE_INVALID,
memory_order_relaxed);
if (strcmp(g_params.transport, "test") == 0)
{
const char * capturePath = option_get_string("test", "captureFile");
const int captureFrame = option_get_int("test", "captureFrame");
const int captureDelay = option_get_int("test", "captureDelay");
if (capturePath)
{
if (captureFrame < 1 || captureDelay < 0)
{
DEBUG_ERROR("test capture requires captureFrame >= 1 and "
"captureDelay >= 0");
return -1;
}
l_testCapture.path = capturePath;
l_testCapture.targetSerial = captureFrame;
l_testCapture.delay = captureDelay;
l_testCapture.enabled = true;
}
}
#endif
g_cursor.sens = g_params.mouseSens; g_cursor.sens = g_params.mouseSens;
if (g_cursor.sens < -9) g_cursor.sens = -9; if (g_cursor.sens < -9) g_cursor.sens = -9;
else if (g_cursor.sens > 9) g_cursor.sens = 9; else if (g_cursor.sens > 9) g_cursor.sens = 9;

View File

@@ -0,0 +1,43 @@
cmake_minimum_required(VERSION 3.10)
find_package(GTest REQUIRED)
find_program(WESTON_EXECUTABLE NAMES weston)
if(NOT WESTON_EXECUTABLE)
message(FATAL_ERROR "weston is required to run framebuffer rendering tests")
endif()
add_executable(looking-glass-render-tests
render_test.cpp
)
add_dependencies(looking-glass-render-tests looking-glass-client)
set_target_properties(looking-glass-render-tests PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
)
target_compile_definitions(looking-glass-render-tests PRIVATE
LG_CLIENT_PATH="$<TARGET_FILE:looking-glass-client>"
)
target_include_directories(looking-glass-render-tests PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/../include"
"${PROJECT_TOP}/common/include"
)
if(TARGET GTest::gtest_main)
target_link_libraries(looking-glass-render-tests GTest::gtest_main)
else()
target_link_libraries(looking-glass-render-tests GTest::Main)
endif()
add_test(NAME looking-glass-render-tests
COMMAND /bin/bash
"${CMAKE_CURRENT_SOURCE_DIR}/run-with-weston.sh"
"${WESTON_EXECUTABLE}"
"$<TARGET_FILE:looking-glass-render-tests>"
)
set_tests_properties(looking-glass-render-tests PROPERTIES
TIMEOUT 120
)

46
client/tests/README.md Normal file
View File

@@ -0,0 +1,46 @@
# Client rendering tests
These gtests run the production client with the synthetic `test` transport,
capture the fully composed framebuffer immediately before presentation, and
compare every pixel with an independent reference implementation.
The default CTest entry starts an isolated headless Weston compositor using
Mesa software rendering. It covers:
- BGRA, RGBA, packed BGR32, RGB24, PQ RGBA10, and scRGB RGBA16F;
- odd widths and 24-bit row pitches;
- the Cartesian matrix of every format with full-frame, moving old/new-box,
overlapping, maximum-count, invalid, null, and zero-area damage;
- HDR-to-SDR transfer, gamut conversion, and tone mapping.
When run on a color-managed Wayland compositor with a compatible EGL surface,
the same tests validate native RGB10A2 or FP16 output. Native PQ tests also
check the requested BT.2020 image description, reference white, mastering
luminance, MaxCLL, and MaxFALL in the client log.
## Building and running
```sh
cmake -S client -B client/build \
-DENABLE_TEST_TRANSPORT=ON \
-DENABLE_RENDER_TESTS=ON
cmake --build client/build
ctest --test-dir client/build --output-on-failure \
-R looking-glass-render-tests
```
GoogleTest and Weston are required when `ENABLE_RENDER_TESTS` is enabled.
Failed cases retain their capture and client log under `/tmp` and print the
artifact paths.
To exercise native HDR on the current Wayland session instead of the headless
SDR compositor:
```sh
client/build/tests/looking-glass-render-tests \
--gtest_filter='*rgba10*:*rgba16f*'
```
Framebuffer readback verifies the signal produced by Looking Glass. Verifying
a compositor's scanout and a physical panel's luminance remains a separate
hardware test.

View File

@@ -0,0 +1,556 @@
/**
* 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.
*/
#include <gtest/gtest.h>
extern "C"
{
#include "common/types.h"
#include "interface/test_capture.h"
}
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>
#include <thread>
#include <tuple>
#include <vector>
#include <fcntl.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
namespace
{
constexpr unsigned kWidth = 127;
constexpr unsigned kHeight = 95;
constexpr unsigned kFrameSerial = 4;
struct FormatCase
{
const char * name;
FrameType type;
bool hdr;
bool pq;
};
struct DamageCase
{
const char * name;
};
struct Capture
{
LG_TestCaptureHeader header {};
std::vector<uint8_t> data;
std::filesystem::path directory;
std::filesystem::path path;
std::filesystem::path log;
};
struct RGB
{
float r;
float g;
float b;
};
std::string readText(const std::filesystem::path & path)
{
std::ifstream input(path);
return std::string(
std::istreambuf_iterator<char>(input),
std::istreambuf_iterator<char>());
}
std::filesystem::path makeTempDirectory()
{
std::array<char, 64> value {};
std::snprintf(value.data(), value.size(), "/tmp/lg-render-case.XXXXXX");
char * result = mkdtemp(value.data());
if (!result)
return {};
return result;
}
int runClient(const FormatCase & format, const char * damage,
const std::filesystem::path & capture,
const std::filesystem::path & log)
{
const std::string size = std::to_string(kWidth) + "x" +
std::to_string(kHeight);
const std::string width = "test:width=" + std::to_string(kWidth);
const std::string height = "test:height=" + std::to_string(kHeight);
const std::string formatArg = std::string("test:format=") + format.name;
const std::string damageArg = std::string("test:damage=") + damage;
const std::string count = "test:frameCount=" +
std::to_string(kFrameSerial);
const std::string captureFile = "test:captureFile=" + capture.string();
const std::string captureFrame = "test:captureFrame=" +
std::to_string(kFrameSerial);
std::vector<std::string> args = {
LG_CLIENT_PATH,
"app:transport=test",
"app:renderer=EGL",
width,
height,
formatArg,
damageArg,
"test:frameRate=60",
count,
"test:holdLastFrame=yes",
"test:realtime=no",
captureFile,
captureFrame,
"test:captureDelay=12",
"win:size=" + size,
"win:autoResize=no",
"win:allowResize=no",
"win:quickSplash=yes",
"win:alerts=no",
"win:noScreensaver=no",
"spice:enable=no",
"egl:multisample=no",
"egl:scale=1",
};
std::vector<char *> argv;
argv.reserve(args.size() + 1);
for (std::string & arg : args)
argv.push_back(arg.data());
argv.push_back(nullptr);
const pid_t pid = fork();
if (pid < 0)
return -1;
if (pid == 0)
{
setenv("XDG_CONFIG_HOME", capture.parent_path().c_str(), 1);
setenv("LIBGL_ALWAYS_SOFTWARE", "1", 1);
const int logFd = open(log.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0600);
if (logFd >= 0)
{
dup2(logFd, STDOUT_FILENO);
dup2(logFd, STDERR_FILENO);
close(logFd);
}
execv(LG_CLIENT_PATH, argv.data());
_exit(127);
}
int status = 0;
const auto deadline =
std::chrono::steady_clock::now() + std::chrono::seconds(20);
while (std::chrono::steady_clock::now() < deadline)
{
const pid_t result = waitpid(pid, &status, WNOHANG);
if (result == pid)
return WIFEXITED(status) ? WEXITSTATUS(status) : 128 + WTERMSIG(status);
if (result < 0)
return -1;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
kill(pid, SIGTERM);
if (waitpid(pid, &status, 0) < 0)
return -1;
return 124;
}
Capture produceCapture(const FormatCase & format, const char * damage)
{
Capture result;
result.directory = makeTempDirectory();
if (result.directory.empty())
return result;
result.path = result.directory /
(std::string(format.name) + "-" + damage + ".lgcapture");
result.log = result.directory /
(std::string(format.name) + "-" + damage + ".log");
const int status = runClient(format, damage, result.path, result.log);
EXPECT_EQ(status, 0) << readText(result.log);
if (status != 0)
return result;
std::ifstream input(result.path, std::ios::binary);
EXPECT_TRUE(input.good()) << "capture missing; client log:\n" <<
readText(result.log);
if (!input)
return result;
input.read(reinterpret_cast<char *>(&result.header), sizeof(result.header));
EXPECT_EQ(input.gcount(), static_cast<std::streamsize>(sizeof(result.header)));
EXPECT_EQ(result.header.magic, LG_TEST_CAPTURE_MAGIC);
EXPECT_EQ(result.header.version, LG_TEST_CAPTURE_VERSION);
EXPECT_EQ(result.header.headerSize, sizeof(result.header));
EXPECT_EQ(result.header.frameSerial, kFrameSerial);
EXPECT_EQ(result.header.sourceType, static_cast<uint32_t>(format.type));
EXPECT_EQ(result.header.width, kWidth);
EXPECT_EQ(result.header.height, kHeight);
EXPECT_EQ(result.header.flags & LG_TEST_CAPTURE_HDR,
format.hdr ? static_cast<uint32_t>(LG_TEST_CAPTURE_HDR) : 0u);
EXPECT_EQ(result.header.flags & LG_TEST_CAPTURE_HDR_PQ,
format.pq ? static_cast<uint32_t>(LG_TEST_CAPTURE_HDR_PQ) : 0u);
if (result.header.flags & LG_TEST_CAPTURE_NATIVE_HDR)
{
const std::string clientLog = readText(result.log);
if (format.pq)
{
EXPECT_NE(clientLog.find(
"HDR image description requested (PQ, BT.2020, "
"referenceWhite:203 cd/m² maxLum:1000 cd/m²"),
std::string::npos);
EXPECT_NE(clientLog.find("maxCLL:1000 maxFALL:400"),
std::string::npos);
}
else
EXPECT_NE(clientLog.find(
"HDR image description requested (scRGB, Windows-scRGB)"),
std::string::npos);
}
result.data.resize(result.header.dataSize);
input.read(reinterpret_cast<char *>(result.data.data()), result.data.size());
EXPECT_EQ(input.gcount(), static_cast<std::streamsize>(result.data.size()));
return result;
}
RGB generatedColor(unsigned x, unsigned y, unsigned serial)
{
uint8_t r = static_cast<uint64_t>(x) * 255 / (kWidth - 1);
uint8_t g = static_cast<uint64_t>(y) * 255 / (kHeight - 1);
uint8_t b = ((x / 32) ^ (y / 32)) & 1 ? 0x30 : 0x90;
const unsigned boxSize = std::min({kWidth, kHeight, 64u});
const unsigned boxX = (serial * 7) % (kWidth - boxSize);
const unsigned boxY = (serial * 5) % (kHeight - boxSize);
if (x >= boxX && y >= boxY &&
x < boxX + boxSize && y < boxY + boxSize)
{
const uint32_t color = serial * UINT32_C(2654435761);
r = color >> 16;
g = color >> 8;
b = color;
}
return {
static_cast<float>(r) / 255.0f,
static_cast<float>(g) / 255.0f,
static_cast<float>(b) / 255.0f,
};
}
float linearToPQ(float linear)
{
constexpr float m1 = 2610.0f / 16384.0f;
constexpr float m2 = 2523.0f / 32.0f;
constexpr float c1 = 3424.0f / 4096.0f;
constexpr float c2 = 2413.0f / 128.0f;
constexpr float c3 = 2392.0f / 128.0f;
const float p = std::pow(std::max(linear, 0.0f), m1);
return std::pow((c1 + c2 * p) / (1.0f + c3 * p), m2);
}
float pqToLinear(float pq)
{
constexpr float m1inv = 16384.0f / 2610.0f;
constexpr float m2inv = 32.0f / 2523.0f;
constexpr float c1 = 3424.0f / 4096.0f;
constexpr float c2 = 2413.0f / 128.0f;
constexpr float c3 = 2392.0f / 128.0f;
const float p = std::pow(std::max(pq, 0.0f), m2inv);
const float d = std::max(p - c1, 0.0f) / (c2 - c3 * p);
return std::pow(d, m1inv);
}
float quantizePQ(float scRGB)
{
constexpr unsigned lutSize = 4096;
const unsigned index = scRGB <= 0.0f ? 0 :
scRGB >= 4.0f ? lutSize :
static_cast<unsigned>(scRGB * (lutSize / 4.0f) + 0.5f);
const float lutScRGB = static_cast<float>(index) * 4.0f / lutSize;
const float pq = linearToPQ(lutScRGB / 125.0f);
const unsigned code = static_cast<unsigned>(pq * 1023.0f + 0.5f);
return static_cast<float>(code) / 1023.0f;
}
uint16_t floatToHalf(float value)
{
uint32_t bits;
std::memcpy(&bits, &value, sizeof(bits));
const uint16_t sign = (bits >> 16) & 0x8000;
int exponent = ((bits >> 23) & 0xff) - 127 + 15;
uint32_t mantissa = bits & 0x7fffff;
if (exponent <= 0)
return sign;
if (exponent >= 31)
return sign | 0x7c00;
mantissa += 0x1000;
if (mantissa & 0x800000)
{
mantissa = 0;
if (++exponent >= 31)
return sign | 0x7c00;
}
return sign | (static_cast<uint16_t>(exponent) << 10) | (mantissa >> 13);
}
float halfToFloat(uint16_t value)
{
const uint32_t sign = static_cast<uint32_t>(value & 0x8000) << 16;
uint32_t exponent = (value >> 10) & 0x1f;
uint32_t mantissa = value & 0x3ff;
uint32_t bits;
if (exponent == 0)
bits = sign;
else if (exponent == 31)
bits = sign | 0x7f800000 | (mantissa << 13);
else
{
exponent = exponent - 15 + 127;
bits = sign | (exponent << 23) | (mantissa << 13);
}
float result;
std::memcpy(&result, &bits, sizeof(result));
return result;
}
RGB toBT2020(RGB value)
{
return {
value.r * 0.6274039f + value.g * 0.3292830f + value.b * 0.0433131f,
value.r * 0.0690973f + value.g * 0.9195404f + value.b * 0.0113623f,
value.r * 0.0163914f + value.g * 0.0880133f + value.b * 0.8955953f,
};
}
RGB toBT709(RGB value)
{
return {
value.r * 1.6604910f + value.g * -0.5876411f + value.b * -0.0728499f,
value.r * -0.1245505f + value.g * 1.1328999f + value.b * -0.0083494f,
value.r * -0.0181508f + value.g * -0.1005789f + value.b * 1.1187297f,
};
}
float linearToSRGB(float value)
{
return value >= 0.0031308f ?
std::pow(std::max(value, 0.0f), 1.0f / 2.4f) * 1.055f - 0.055f :
value * 12.92f;
}
RGB compressAndEncode(RGB value)
{
value.r = std::max(value.r, 0.0f);
value.g = std::max(value.g, 0.0f);
value.b = std::max(value.b, 0.0f);
const float peak = std::max({value.r, value.g, value.b});
if (peak > 0.0f)
{
const float compressed = peak < 0.75f ?
peak : 0.75f + (peak - 0.75f) * 0.25f;
const float scale = compressed / peak;
value.r *= scale;
value.g *= scale;
value.b *= scale;
}
value.r = linearToSRGB(std::clamp(value.r, 0.0f, 1.0f));
value.g = linearToSRGB(std::clamp(value.g, 0.0f, 1.0f));
value.b = linearToSRGB(std::clamp(value.b, 0.0f, 1.0f));
return value;
}
RGB expectedColor(const FormatCase & format, const Capture & capture,
unsigned x, unsigned y)
{
const RGB generated = generatedColor(x, y, kFrameSerial);
if (!format.hdr)
return generated;
if (format.type == FRAME_TYPE_RGBA16F)
{
RGB scRGB = {
halfToFloat(floatToHalf(generated.r * 4.0f)),
halfToFloat(floatToHalf(generated.g * 4.0f)),
halfToFloat(floatToHalf(generated.b * 4.0f)),
};
if (capture.header.flags & LG_TEST_CAPTURE_NATIVE_HDR)
return scRGB;
scRGB.r *= 80.0f / 250.0f;
scRGB.g *= 80.0f / 250.0f;
scRGB.b *= 80.0f / 250.0f;
return compressAndEncode(scRGB);
}
RGB linear709 = {
generated.r * 4.0f,
generated.g * 4.0f,
generated.b * 4.0f,
};
RGB encoded2020 = toBT2020(linear709);
encoded2020.r = quantizePQ(encoded2020.r);
encoded2020.g = quantizePQ(encoded2020.g);
encoded2020.b = quantizePQ(encoded2020.b);
if (capture.header.flags & LG_TEST_CAPTURE_NATIVE_HDR)
return encoded2020;
RGB linear2020 = {
pqToLinear(encoded2020.r) * (10000.0f / 250.0f),
pqToLinear(encoded2020.g) * (10000.0f / 250.0f),
pqToLinear(encoded2020.b) * (10000.0f / 250.0f),
};
return compressAndEncode(toBT709(linear2020));
}
RGB capturedColor(const Capture & capture, unsigned x, unsigned y)
{
const bool bottomUp =
capture.header.flags & LG_TEST_CAPTURE_BOTTOM_UP;
const unsigned row = bottomUp ? capture.header.height - 1 - y : y;
const uint8_t * pixel =
capture.data.data() + row * capture.header.stride;
switch (capture.header.captureFormat)
{
case LG_TEST_CAPTURE_RGBA8:
pixel += x * 4;
return {
pixel[0] / 255.0f,
pixel[1] / 255.0f,
pixel[2] / 255.0f,
};
case LG_TEST_CAPTURE_RGB10_A2:
{
uint32_t packed;
std::memcpy(&packed, pixel + x * sizeof(packed), sizeof(packed));
return {
static_cast<float>( packed & 0x3ff) / 1023.0f,
static_cast<float>((packed >> 10) & 0x3ff) / 1023.0f,
static_cast<float>((packed >> 20) & 0x3ff) / 1023.0f,
};
}
case LG_TEST_CAPTURE_RGBA32F:
{
std::array<float, 4> value;
std::memcpy(value.data(), pixel + x * sizeof(value), sizeof(value));
return {value[0], value[1], value[2]};
}
default:
ADD_FAILURE() << "Unknown capture format: " <<
capture.header.captureFormat;
return {};
}
}
void compareReference(const FormatCase & format, const Capture & capture)
{
float maxError = 0.0f;
unsigned maxX = 0;
unsigned maxY = 0;
RGB maxActual {};
RGB maxExpected {};
for (unsigned y = 0; y < capture.header.height; ++y)
for (unsigned x = 0; x < capture.header.width; ++x)
{
const RGB actual = capturedColor(capture, x, y);
const RGB expected = expectedColor(format, capture, x, y);
const float error = std::max({
std::abs(actual.r - expected.r),
std::abs(actual.g - expected.g),
std::abs(actual.b - expected.b),
});
if (error > maxError)
{
maxError = error;
maxX = x;
maxY = y;
maxActual = actual;
maxExpected = expected;
}
}
const float tolerance = format.hdr ? 0.012f : 0.006f;
EXPECT_LE(maxError, tolerance)
<< "max error at (" << maxX << ", " << maxY << ")"
<< "\nactual: " << maxActual.r << ", " << maxActual.g << ", "
<< maxActual.b
<< "\nexpected: " << maxExpected.r << ", " << maxExpected.g << ", "
<< maxExpected.b
<< "\ncapture retained at: " << capture.path;
}
using RenderCase = std::tuple<FormatCase, DamageCase>;
class RenderPipelineTest : public testing::TestWithParam<RenderCase>
{
};
TEST_P(RenderPipelineTest, MatchesReference)
{
const FormatCase & format = std::get<0>(GetParam());
const DamageCase & damage = std::get<1>(GetParam());
Capture capture = produceCapture(format, damage.name);
ASSERT_FALSE(capture.data.empty()) << "artifacts: " << capture.directory;
compareReference(format, capture);
if (!testing::Test::HasFailure())
std::filesystem::remove_all(capture.directory);
}
std::string renderCaseName(
const testing::TestParamInfo<RenderCase> & info)
{
return std::string(std::get<0>(info.param).name) + "_" +
std::get<1>(info.param).name;
}
INSTANTIATE_TEST_SUITE_P(FormatDamageMatrix, RenderPipelineTest,
testing::Combine(
testing::Values(
FormatCase {"bgra", FRAME_TYPE_BGRA, false, false},
FormatCase {"rgba", FRAME_TYPE_RGBA, false, false},
FormatCase {"bgr32", FRAME_TYPE_BGR_32, false, false},
FormatCase {"rgb24", FRAME_TYPE_RGB_24, false, false},
FormatCase {"rgba10", FRAME_TYPE_RGBA10, true, true },
FormatCase {"rgba16f", FRAME_TYPE_RGBA16F, true, false}
),
testing::Values(
DamageCase {"full"},
DamageCase {"moving"},
DamageCase {"overlap"},
DamageCase {"max"},
DamageCase {"invalid"},
DamageCase {"null"},
DamageCase {"zero"}
)
),
renderCaseName);
} // namespace

View File

@@ -0,0 +1,62 @@
#!/bin/bash
set -euo pipefail
weston_bin=$1
shift
runtime_dir=$(mktemp -d /tmp/lg-render-tests.XXXXXX)
weston_pid=
cleanup()
{
if [[ -n "$weston_pid" ]]; then
kill "$weston_pid" 2>/dev/null || true
wait "$weston_pid" 2>/dev/null || true
fi
rm -rf -- "$runtime_dir"
}
trap cleanup EXIT
chmod 700 "$runtime_dir"
export XDG_RUNTIME_DIR="$runtime_dir"
export LIBGL_ALWAYS_SOFTWARE=1
unset DISPLAY WAYLAND_DISPLAY
socket_name=lg-render-tests
"$weston_bin" \
--backend=headless \
--renderer=gl \
--width=1024 \
--height=768 \
--idle-time=0 \
--no-config \
--socket="$socket_name" \
--log="$runtime_dir/weston.log" &
weston_pid=$!
startup_deadline=$((SECONDS + 30))
while (( SECONDS < startup_deadline )); do
if [[ -S "$runtime_dir/$socket_name" ]]; then
break
fi
if ! kill -0 "$weston_pid" 2>/dev/null; then
sed -n '1,240p' "$runtime_dir/weston.log" >&2
exit 1
fi
sleep 0.1
done
if [[ ! -S "$runtime_dir/$socket_name" ]]; then
echo "Timed out waiting for the Weston Wayland socket" >&2
sed -n '1,240p' "$runtime_dir/weston.log" >&2
exit 1
fi
export WAYLAND_DISPLAY="$socket_name"
set +e
"$@"
status=$?
set -e
if (( status != 0 )); then
sed -n '1,240p' "$runtime_dir/weston.log" >&2
fi
exit "$status"

View File

@@ -34,6 +34,17 @@
#define TEST_BUFFER_COUNT 2 #define TEST_BUFFER_COUNT 2
#define TEST_PQ_LUT_SIZE 4096 #define TEST_PQ_LUT_SIZE 4096
enum TestDamageMode
{
TEST_DAMAGE_MOVING,
TEST_DAMAGE_FULL,
TEST_DAMAGE_OVERLAP,
TEST_DAMAGE_MAX,
TEST_DAMAGE_INVALID,
TEST_DAMAGE_NULL,
TEST_DAMAGE_ZERO,
};
struct TestFormat struct TestFormat
{ {
const char * name; const char * name;
@@ -66,6 +77,8 @@ struct LG_Transport
unsigned frameCount; unsigned frameCount;
bool realtime; bool realtime;
bool cycleFormats; bool cycleFormats;
bool holdLastFrame;
enum TestDamageMode damageMode;
unsigned formatIndex; unsigned formatIndex;
bool connected; bool connected;
@@ -74,7 +87,7 @@ struct LG_Transport
uint64_t nextFrameTime; uint64_t nextFrameTime;
unsigned bufferIndex; unsigned bufferIndex;
struct TestBuffer buffers[TEST_BUFFER_COUNT]; struct TestBuffer buffers[TEST_BUFFER_COUNT];
FrameDamageRect damage[2]; FrameDamageRect damage[LG_TRANSPORT_MAX_DAMAGE_RECTS];
LG_TransportFrameFormat format; LG_TransportFrameFormat format;
uint16_t pqLUT[TEST_PQ_LUT_SIZE + 1]; uint16_t pqLUT[TEST_PQ_LUT_SIZE + 1];
}; };
@@ -126,6 +139,43 @@ static void test_setup(void)
.type = OPTION_TYPE_STRING, .type = OPTION_TYPE_STRING,
.value.x_string = "bgra", .value.x_string = "bgra",
}, },
{
.module = "test",
.name = "damage",
.description = "Damage mode: moving, full, overlap, max, invalid, "
"null, or zero",
.type = OPTION_TYPE_STRING,
.value.x_string = "moving",
},
{
.module = "test",
.name = "holdLastFrame",
.description =
"Keep the session alive after generating frameCount frames",
.type = OPTION_TYPE_BOOL,
.value.x_bool = false,
},
{
.module = "test",
.name = "captureFile",
.description = "Write a composed framebuffer capture to this file",
.type = OPTION_TYPE_STRING,
.value.x_string = NULL,
},
{
.module = "test",
.name = "captureFrame",
.description = "Capture after this synthetic frame serial is submitted",
.type = OPTION_TYPE_INT,
.value.x_int = 0,
},
{
.module = "test",
.name = "captureDelay",
.description = "Stable render passes to wait before framebuffer capture",
.type = OPTION_TYPE_INT,
.value.x_int = 4,
},
{0} {0}
}; };
@@ -141,6 +191,25 @@ static int test_findFormat(const char * name)
return -1; return -1;
} }
static int test_findDamageMode(const char * name)
{
static const char * names[] =
{
[TEST_DAMAGE_MOVING] = "moving",
[TEST_DAMAGE_FULL] = "full",
[TEST_DAMAGE_OVERLAP] = "overlap",
[TEST_DAMAGE_MAX] = "max",
[TEST_DAMAGE_INVALID] = "invalid",
[TEST_DAMAGE_NULL] = "null",
[TEST_DAMAGE_ZERO] = "zero",
};
for (unsigned i = 0; i < ARRAY_LENGTH(names); ++i)
if (strcmp(name, names[i]) == 0)
return (int)i;
return -1;
}
static float test_linearToPQ(float linear) static float test_linearToPQ(float linear)
{ {
const float m1 = 2610.0f / 16384.0f; const float m1 = 2610.0f / 16384.0f;
@@ -251,11 +320,13 @@ static bool test_create(LG_Transport ** result)
const int frameRate = option_get_int("test", "frameRate"); const int frameRate = option_get_int("test", "frameRate");
const int frameCount = option_get_int("test", "frameCount"); const int frameCount = option_get_int("test", "frameCount");
const char * format = option_get_string("test", "format"); const char * format = option_get_string("test", "format");
const char * damage = option_get_string("test", "damage");
const int damageMode = damage ? test_findDamageMode(damage) : -1;
const bool cycleFormats = strcmp(format, "cycle") == 0; const bool cycleFormats = strcmp(format, "cycle") == 0;
const int formatIndex = cycleFormats ? 0 : test_findFormat(format); const int formatIndex = cycleFormats ? 0 : test_findFormat(format);
const unsigned maxBytesPerPixel = formatIndex < 0 ? 0 : const unsigned maxBytesPerPixel = formatIndex < 0 ? 0 :
cycleFormats ? 8 : testFormats[formatIndex].bytesPerPixel; cycleFormats ? 8 : testFormats[formatIndex].bytesPerPixel;
if (width < 1 || height < 1 || !maxBytesPerPixel || if (width < 1 || height < 1 || !maxBytesPerPixel || damageMode < 0 ||
width > (int)(UINT32_MAX / maxBytesPerPixel) || width > (int)(UINT32_MAX / maxBytesPerPixel) ||
frameRate < 1 || frameRate > 1000000000 || frameCount < 0 || frameRate < 1 || frameRate > 1000000000 || frameCount < 0 ||
(size_t)width > (SIZE_MAX - sizeof(FrameBuffer)) / (size_t)width > (SIZE_MAX - sizeof(FrameBuffer)) /
@@ -273,6 +344,8 @@ static bool test_create(LG_Transport ** result)
this->frameCount = frameCount; this->frameCount = frameCount;
this->realtime = option_get_bool("test", "realtime"); this->realtime = option_get_bool("test", "realtime");
this->cycleFormats = cycleFormats; this->cycleFormats = cycleFormats;
this->holdLastFrame = option_get_bool("test", "holdLastFrame");
this->damageMode = damageMode;
this->formatIndex = (unsigned)formatIndex; this->formatIndex = (unsigned)formatIndex;
test_initPQLUT(this); test_initPQLUT(this);
@@ -489,7 +562,12 @@ static LG_TransportStatus test_nextFrame(LG_Transport * this, bool useDMA,
if (this->framePending) if (this->framePending)
return LG_TRANSPORT_ERROR; return LG_TRANSPORT_ERROR;
if (this->frameCount && this->serial >= this->frameCount) if (this->frameCount && this->serial >= this->frameCount)
return LG_TRANSPORT_END; {
if (!this->holdLastFrame)
return LG_TRANSPORT_END;
usleep(1000);
return LG_TRANSPORT_TIMEOUT;
}
if (this->realtime) if (this->realtime)
{ {
@@ -518,7 +596,8 @@ static LG_TransportStatus test_nextFrame(LG_Transport * this, bool useDMA,
frame->framebuffer = buffer->framebuffer; frame->framebuffer = buffer->framebuffer;
frame->dmaFD = -1; frame->dmaFD = -1;
if (this->serial == 1 || formatChanged) if (this->damageMode == TEST_DAMAGE_FULL ||
this->serial == 1 || formatChanged)
frame->damageRectsCount = 0; frame->damageRectsCount = 0;
else else
{ {
@@ -542,8 +621,46 @@ static LG_TransportStatus test_nextFrame(LG_Transport * this, bool useDMA,
.width = boxSize, .width = boxSize,
.height = boxSize, .height = boxSize,
}; };
frame->damageRects = this->damage; frame->damageRects = this->damage;
frame->damageRectsCount = 2; switch (this->damageMode)
{
case TEST_DAMAGE_MOVING:
frame->damageRectsCount = 2;
break;
case TEST_DAMAGE_OVERLAP:
this->damage[2] = this->damage[0];
this->damage[3] = this->damage[1];
frame->damageRectsCount = 4;
break;
case TEST_DAMAGE_MAX:
for (unsigned i = 2; i < ARRAY_LENGTH(this->damage); ++i)
this->damage[i] = (FrameDamageRect) {0};
frame->damageRectsCount = ARRAY_LENGTH(this->damage);
break;
case TEST_DAMAGE_INVALID:
this->damage[0] = (FrameDamageRect) {
.x = this->width, .y = 0, .width = 1, .height = 1
};
frame->damageRectsCount = 1;
break;
case TEST_DAMAGE_NULL:
frame->damageRects = NULL;
frame->damageRectsCount = 1;
break;
case TEST_DAMAGE_ZERO:
this->damage[2] = (FrameDamageRect) {0};
frame->damageRectsCount = 3;
break;
case TEST_DAMAGE_FULL:
DEBUG_UNREACHABLE();
break;
}
} }
this->framePending = true; this->framePending = true;