From 11dcaa887dad5daedbcfeb6e312ccae4d14d459b Mon Sep 17 00:00:00 2001 From: Hualet Wang Date: Sun, 11 Oct 2026 22:58:21 +0800 Subject: [PATCH 1/2] perf(shot): reduce X11 screenshot startup overhead Convert supported native RGB32 XImages by scanline and read CPU vendor metadata directly from procfs. Add pixel regressions and reproducible startup benchmarks. Log: Reduce startup overhead for X11 screenshots --- src/CMakeLists.txt | 2 + src/src.pro | 3 + src/utils.cpp | 27 ++-- src/utils/cpuvendor.cpp | 22 +++ src/utils/cpuvendor.h | 9 ++ src/utils/screengrabber.cpp | 18 ++- src/utils/ximageconverter.cpp | 50 +++++++ src/utils/ximageconverter.h | 12 ++ tests/startup/CMakeLists.txt | 26 ++++ tests/startup/README.md | 44 ++++++ tests/startup/benchmark_launch.py | 130 ++++++++++++++++++ tests/startup/benchmark_startup.cpp | 85 ++++++++++++ tests/startup/test_cpuvendor.cpp | 41 ++++++ tests/startup/test_startup.cpp | 91 ++++++++++++ tests/startup/ximagefixture.h | 53 +++++++ .../ut_screen_shot_recorder.pro | 2 + tests/ut_screen_shot_recorder/ut_utils_cov2.h | 32 ++--- 17 files changed, 598 insertions(+), 49 deletions(-) create mode 100644 src/utils/cpuvendor.cpp create mode 100644 src/utils/cpuvendor.h create mode 100644 src/utils/ximageconverter.cpp create mode 100644 src/utils/ximageconverter.h create mode 100644 tests/startup/CMakeLists.txt create mode 100644 tests/startup/README.md create mode 100644 tests/startup/benchmark_launch.py create mode 100644 tests/startup/benchmark_startup.cpp create mode 100644 tests/startup/test_cpuvendor.cpp create mode 100644 tests/startup/test_startup.cpp create mode 100644 tests/startup/ximagefixture.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f84d2b5da..1f993774f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -89,6 +89,8 @@ set ( deepin-screen-recorder_HDRS ) set ( deepin-screen-recorder_SRCS + utils/ximageconverter.cpp + utils/cpuvendor.cpp utils/log.cpp utils/calculaterect.cpp utils/shapesutils.cpp diff --git a/src/src.pro b/src/src.pro index 8a5ca01af..df846b159 100755 --- a/src/src.pro +++ b/src/src.pro @@ -265,6 +265,9 @@ lessThan(QT_MAJOR_VERSION, 6) { } # Sources +HEADERS += utils/ximageconverter.h utils/cpuvendor.h +SOURCES += utils/ximageconverter.cpp utils/cpuvendor.cpp + SOURCES += main.cpp \ capture.cpp \ main_window.cpp \ diff --git a/src/utils.cpp b/src/utils.cpp index 806155d2f..0fb6f24e2 100755 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "utils.h" +#include "utils/cpuvendor.h" #include "constant.h" #include "dbus_name.h" @@ -14,6 +15,7 @@ #include #include +#include #include #include #include @@ -698,27 +700,14 @@ void Utils::getAllWindowInfo( bool Utils::checkCpuIsZhaoxin() { qCInfo(dsrApp) << "checkCpuIsZhaoxin() called."; - QProcess process; - process.start("lscpu"); - process.waitForFinished(); - process.waitForReadyRead(); - QString comStr = process.readAllStandardOutput(); - process.close(); - QStringList lines = comStr.split('\n'); - QStringList filteredLines; - - // 过滤包含指定关键字的行 - for (const QString &line : lines) { - if (line.contains("CentaurHauls", Qt::CaseInsensitive)) { - filteredLines.append(line); - } - } - if (filteredLines.isEmpty()) { - qCInfo(dsrApp) << "CPU is not zhaoxin."; + QFile cpuInfo(QStringLiteral("/proc/cpuinfo")); + if (!cpuInfo.open(QIODevice::ReadOnly)) { + qCWarning(dsrApp) << "Cannot read CPU vendor:" << cpuInfo.errorString(); return false; } - qCInfo(dsrApp) << "CPU is zhaoxin."; - return true; + const bool isZhaoxin = CpuVendor::isZhaoxin(cpuInfo); + qCInfo(dsrApp) << "CPU is zhaoxin:" << isZhaoxin; + return isZhaoxin; } // 获取处理器名称 diff --git a/src/utils/cpuvendor.cpp b/src/utils/cpuvendor.cpp new file mode 100644 index 000000000..ce7b10e54 --- /dev/null +++ b/src/utils/cpuvendor.cpp @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later +#include "cpuvendor.h" +#include +#include + +bool CpuVendor::isZhaoxin(QIODevice &cpuInfo) +{ + if (!cpuInfo.isReadable()) + return false; + // /proc files can report size zero even while they contain readable data. + for (QByteArray line = cpuInfo.readLine(); !line.isEmpty(); line = cpuInfo.readLine()) { + const int separator = line.indexOf(':'); + if (separator < 0) + continue; + if (line.left(separator).trimmed().compare("vendor_id", Qt::CaseInsensitive) == 0 + && line.mid(separator + 1).trimmed().compare("CentaurHauls", Qt::CaseInsensitive) == 0) { + return true; + } + } + return false; +} diff --git a/src/utils/cpuvendor.h b/src/utils/cpuvendor.h new file mode 100644 index 000000000..0a0cd804e --- /dev/null +++ b/src/utils/cpuvendor.h @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later +#ifndef CPUVENDOR_H +#define CPUVENDOR_H +class QIODevice; +namespace CpuVendor { +bool isZhaoxin(QIODevice &cpuInfo); +} +#endif diff --git a/src/utils/screengrabber.cpp b/src/utils/screengrabber.cpp index dba86b49b..19b18dedb 100644 --- a/src/utils/screengrabber.cpp +++ b/src/utils/screengrabber.cpp @@ -22,6 +22,7 @@ // X11 headers for XGetImage workaround #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +#include "ximageconverter.h" #include #include #endif @@ -650,20 +651,17 @@ QPixmap ScreenGrabber::grabWithXGetImage(bool &ok, const QRect &rect) << "depth:" << ximg->depth << "bits_per_pixel:" << ximg->bits_per_pixel; // 将 XImage 转换为 QImage - QImage qimg(ximg->width, ximg->height, QImage::Format_RGB32); - for (int row = 0; row < ximg->height; ++row) { - for (int col = 0; col < ximg->width; ++col) { - unsigned long pixel = XGetPixel(ximg, col, row); - int r = (pixel >> 16) & 0xFF; - int g = (pixel >> 8) & 0xFF; - int b = pixel & 0xFF; - qimg.setPixel(col, row, qRgb(r, g, b)); - } - } + QImage qimg = XImageConverter::toImage(ximg); XDestroyImage(ximg); XCloseDisplay(dpy); + if (qimg.isNull()) { + qCWarning(dsrApp) << "[XGetImage] Failed to convert XImage"; + ok = false; + return QPixmap(); + } + // 转换为 QPixmap QPixmap fullPixmap = QPixmap::fromImage(qimg); qCDebug(dsrApp) << "[XGetImage] Full pixmap size:" << fullPixmap.size(); diff --git a/src/utils/ximageconverter.cpp b/src/utils/ximageconverter.cpp new file mode 100644 index 000000000..272c0794f --- /dev/null +++ b/src/utils/ximageconverter.cpp @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later +#include "ximageconverter.h" +#include +#include +#include +#include + +QImage XImageConverter::toImage(XImage *image) +{ + if (!image || !image->data || image->width <= 0 || image->height <= 0 + || image->bits_per_pixel <= 0 || image->bits_per_pixel > 32 + || image->xoffset < 0 || image->bytes_per_line <= 0 + || qint64(image->bytes_per_line) * 8 < qint64(image->width) * image->bits_per_pixel + || !image->f.get_pixel) { + return {}; + } + + QImage result(image->width, image->height, QImage::Format_RGB32); + if (result.isNull()) + return result; + + const int nativeOrder = QSysInfo::ByteOrder == QSysInfo::LittleEndian ? LSBFirst : MSBFirst; + const bool direct = image->format == ZPixmap && image->bits_per_pixel == 32 + && (image->depth == 24 || image->depth == 32) + && image->red_mask == 0xff0000 && image->green_mask == 0xff00 && image->blue_mask == 0xff + && image->byte_order == nativeOrder && image->xoffset == 0 + && image->bytes_per_line % alignof(quint32) == 0 + && reinterpret_cast(image->data) % alignof(quint32) == 0; + + for (int y = 0; y < image->height; ++y) { + if (direct) { + const char *source = image->data + qsizetype(y) * image->bytes_per_line; + QRgb *destination = reinterpret_cast(result.scanLine(y)); + for (int x = 0; x < image->width; ++x) { + quint32 pixel; + // memcpy avoids aliasing assumptions about Xlib-owned storage. + std::memcpy(&pixel, source + qsizetype(x) * sizeof(pixel), sizeof(pixel)); + destination[x] = pixel | 0xff000000u; + } + } else { + // Preserve the existing conversion for uncommon XImage layouts. + for (int x = 0; x < image->width; ++x) { + const unsigned long pixel = XGetPixel(image, x, y); + result.setPixel(x, y, qRgb((pixel >> 16) & 0xff, (pixel >> 8) & 0xff, pixel & 0xff)); + } + } + } + return result; +} diff --git a/src/utils/ximageconverter.h b/src/utils/ximageconverter.h new file mode 100644 index 000000000..16b0130f4 --- /dev/null +++ b/src/utils/ximageconverter.h @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later +#ifndef XIMAGECONVERTER_H +#define XIMAGECONVERTER_H +#include +struct _XImage; + +namespace XImageConverter { +// Returns an owned, opaque RGB32 image; it never retains the XImage buffer. +QImage toImage(_XImage *image); +} +#endif diff --git a/tests/startup/CMakeLists.txt b/tests/startup/CMakeLists.txt new file mode 100644 index 000000000..f33b47db5 --- /dev/null +++ b/tests/startup/CMakeLists.txt @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +# SPDX-License-Identifier: GPL-3.0-or-later +cmake_minimum_required(VERSION 3.16) +project(screenshot_startup_tests LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 17) +find_package(Qt6 REQUIRED COMPONENTS Core Gui) +find_package(X11 REQUIRED) +option(STARTUP_TEST_LEGACY "Run the fast-path regression against the original loop" OFF) +if(STARTUP_TEST_LEGACY) + add_executable(test_startup test_startup.cpp) + target_compile_definitions(test_startup PRIVATE STARTUP_TEST_LEGACY) +else() + add_executable(test_startup test_startup.cpp ../../src/utils/ximageconverter.cpp) +endif() +target_link_libraries(test_startup PRIVATE Qt6::Core Qt6::Gui X11::X11) +enable_testing() +add_test(NAME startup_regressions COMMAND test_startup) + +add_executable(test_cpuvendor test_cpuvendor.cpp ../../src/utils/cpuvendor.cpp) +target_link_libraries(test_cpuvendor PRIVATE Qt6::Core) +add_test(NAME cpuvendor_regressions COMMAND test_cpuvendor) +if(NOT STARTUP_TEST_LEGACY) + add_executable(benchmark_startup benchmark_startup.cpp + ../../src/utils/ximageconverter.cpp ../../src/utils/cpuvendor.cpp) + target_link_libraries(benchmark_startup PRIVATE Qt6::Core Qt6::Gui X11::X11) +endif() diff --git a/tests/startup/README.md b/tests/startup/README.md new file mode 100644 index 000000000..383316030 --- /dev/null +++ b/tests/startup/README.md @@ -0,0 +1,44 @@ +# Screenshot startup tests + +These focused tests need Qt6 Core/Gui development files, Xlib, CMake and a C++17 compiler. They do not require recording or camera dependencies and do not open a window. + +```sh +cmake -S tests/startup -B build/startup-tests -DCMAKE_BUILD_TYPE=Release +cmake --build build/startup-tests -j4 +ctest --test-dir build/startup-tests --output-on-failure +build/startup-tests/benchmark_startup +``` + +The pixel tests cover row padding, alpha, source ownership, byte order, 24-bit storage, source alignment, nonstandard depth/masks and invalid input. CPU tests use in-memory metadata, including empty, unreadable and misleading input. + +`benchmark_startup` alternates the old and new conversion order for 31 samples and compares every pixel. Add `--desktop` to also compare a real X11 desktop image in memory; it does not save the image. CPU timing compares `lscpu` with the procfs detector. + +The image regression can be run against the original conversion, where it must fail because native RGB32 still calls XGetPixel per pixel: + +```sh +cmake -S tests/startup -B build/startup-legacy -DSTARTUP_TEST_LEGACY=ON +cmake --build build/startup-legacy -j4 +ctest --test-dir build/startup-legacy --output-on-failure +``` + +Compare two full app builds through the screenshot D-Bus method: + +```sh +python3 tests/startup/benchmark_launch.py /path/to/before /path/to/after --samples 11 +``` + +This temporarily displays and closes each overlay, refuses to interfere with an existing screenshot/recording service, and only terminates its own subprocesses. It reports a new-process measurement with warm filesystem cache. The endpoint is showFullScreen return, not the first frame presented by the compositor. Results and per-run logs default to `/tmp/dsr-launch-benchmark`. + +## Local measurements + +Measured on X11 with Qt6, a 2880x1800 physical screen and DPR 1.75. Both variants used the same source baseline and Release build options. Eleven runs per variant were interleaved, each using a new process with warm filesystem cache: + +| Boundary | Before median | After median | +| --- | ---: | ---: | +| Process launch to showFullScreen return | 240.30 ms | 184.63 ms | +| StartScreenshot method entry to showFullScreen return | 152.15 ms | 95.57 ms | +| Process launch to service ready | 83.29 ms | 83.43 ms | + +For 31 alternating samples, conversion of a real desktop XImage fell from 80.94 ms to 4.11 ms with identical pixels; CPU detection fell from 11.39 ms to 0.71 ms. These component timings exclude capture, QPixmap creation and display. Application measurements use log receipt times and include logging/reader overhead; they do not measure keyboard dispatch, cold D-Bus activation, cold filesystem cache or compositor presentation. + +Both before/after builds covered only the screenshot main program (`src/src.pro`). The top-level subdirectory project, Dock plugins, pin-screenshots program and Debian package were not built. In particular, the missing `libdtk6tablerecognizer-dev` dependency still prevents validating the pin-screenshots build. Qt5, multiscreen, Wayland/Treeland and the full existing unit-test suite were not exercised. The optimized main program also saved a valid opaque 2880x1800 PNG in the local fullscreen-save smoke test. diff --git a/tests/startup/benchmark_launch.py b/tests/startup/benchmark_launch.py new file mode 100644 index 000000000..b83a0d3bd --- /dev/null +++ b/tests/startup/benchmark_launch.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +# SPDX-License-Identifier: GPL-3.0-or-later +"""Compare two built screenshot apps through their normal D-Bus entry point. + +Temporarily displays each screenshot overlay and terminates only the process +started by this script. Does not select, save, install or activate system apps. +The display boundary is showFullScreen return, not compositor presentation. +""" +import argparse +import json +import math +import os +from pathlib import Path +import queue +import statistics +import subprocess +import threading +import time + +BUS = ["gdbus", "call", "--session", "--dest", "org.freedesktop.DBus", + "--object-path", "/org/freedesktop/DBus"] +SHOT = ["gdbus", "call", "--session", "--dest", "com.deepin.Screenshot", + "--object-path", "/com/deepin/Screenshot", "--method", + "com.deepin.Screenshot.StartScreenshot"] + + +def owner(name): + result = subprocess.run(BUS + ["--method", "org.freedesktop.DBus.NameHasOwner", name], + capture_output=True, text=True, check=True, timeout=5) + return "true" in result.stdout + + +def sample(binary, log_path): + for name in ("com.deepin.Screenshot", "com.deepin.ScreenRecorder"): + if owner(name): + raise RuntimeError(f"{name} is already running; refusing to interfere") + env = dict(os.environ, QT_LOGGING_RULES="dsr.app.debug=true;dsr.app.info=true") + started = time.monotonic_ns() + process = subprocess.Popen([str(binary), "--dbus"], env=env, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, text=True, bufsize=1) + events = queue.Queue() + lines = [] + + def read_output(): + for line in process.stdout: + received = time.monotonic_ns() + lines.append(line) + events.put((received, line)) + events.put((time.monotonic_ns(), None)) + + reader = threading.Thread(target=read_output, daemon=True) + reader.start() + call = None + invoked = entered = shown = ready = None + try: + deadline = time.monotonic() + 10 + while time.monotonic() < deadline: + received, line = events.get(timeout=max(0.01, deadline - time.monotonic())) + if line is None: + raise RuntimeError(f"App exited before showing overlay: {process.poll()}") + if "DBus option set, waiting" in line and call is None: + ready = received + invoked = time.monotonic_ns() + call = subprocess.Popen(SHOT, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + if "StartScreenshot method called." in line: + entered = received + if "Showing window in full screen mode" in line: + shown = received + break + if not all((ready, invoked, entered, shown)): + raise RuntimeError("Missing startup timing boundaries") + stdout, stderr = call.communicate(timeout=5) + if call.returncode: + raise RuntimeError(f"D-Bus screenshot call failed: {stderr}") + # Allow the normal event loop to paint before closing our own process. + time.sleep(0.15) + return {"process_to_ready_ms": (ready - started) / 1e6, + "dbus_to_show_ms": (shown - entered) / 1e6, + "invoke_to_show_ms": (shown - invoked) / 1e6, + "process_to_show_ms": (shown - started) / 1e6} + finally: + if call and call.poll() is None: + call.terminate() + call.communicate(timeout=3) + if process.poll() is None: + process.terminate() + try: + process.wait(timeout=3) + except subprocess.TimeoutExpired: + process.kill() + process.wait(timeout=3) + reader.join(timeout=1) + process.stdout.close() + log_path.write_text("".join(lines)) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("before", type=Path) + parser.add_argument("after", type=Path) + parser.add_argument("--samples", type=int, default=11) + parser.add_argument("--output", type=Path, default=Path("/tmp/dsr-launch-benchmark")) + args = parser.parse_args() + if args.samples < 1: + parser.error("samples must be positive") + args.output.mkdir(parents=True, exist_ok=True) + samples = {"before": [], "after": []} + for index in range(args.samples): + names = ("before", "after") if index % 2 == 0 else ("after", "before") + for name in names: + samples[name].append(sample(getattr(args, name).resolve(), + args.output / f"{name}-{index:02}.log")) + summary = {} + for name, values in samples.items(): + summary[name] = {} + for metric in values[0]: + numbers = sorted(value[metric] for value in values) + summary[name][metric] = {"median": statistics.median(numbers), + "p95": numbers[math.ceil(len(numbers) * 0.95) - 1], + "min": min(numbers), "max": max(numbers)} + report = {"samples_per_variant": args.samples, + "boundary": "showFullScreen return; page cache warm, new process per sample", + "summary": summary, "samples": samples} + (args.output / "results.json").write_text(json.dumps(report, indent=2) + "\n") + print(json.dumps(summary, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/tests/startup/benchmark_startup.cpp b/tests/startup/benchmark_startup.cpp new file mode 100644 index 000000000..2924faeb4 --- /dev/null +++ b/tests/startup/benchmark_startup.cpp @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later +#include +#include +#include +#include +#include +#include +#include +#include "ximagefixture.h" +#include "../../src/utils/ximageconverter.h" +#include "../../src/utils/cpuvendor.h" + +static double median(std::vector samples) +{ + std::sort(samples.begin(), samples.end()); + return samples[samples.size() / 2]; +} +static void benchmarkImage(XImage *image, const QString &label) +{ + QTextStream output(stdout); + const QImage expected = legacyXImageConversion(image); + const QImage actual = XImageConverter::toImage(image); + if (expected != actual) + qFatal("Pixel mismatch during benchmark"); + std::vector before, after; + QElapsedTimer timer; + // Alternate order to reduce cache/temperature bias. + for (int i = 0; i < 31; ++i) { + for (int step = 0; step < 2; ++step) { + const bool fast = (i + step) % 2 == 0; + timer.start(); + QImage result = fast ? XImageConverter::toImage(image) : legacyXImageConversion(image); + const double elapsed = timer.nsecsElapsed() / 1e6; + if (result.isNull()) + qFatal("Conversion failed during benchmark"); + (fast ? after : before).push_back(elapsed); + } + } + output << label << ": legacy median " << median(before) << " ms, optimized median " + << median(after) << " ms; pixels identical\n"; +} +int main(int argc, char **argv) +{ + QCoreApplication app(argc, argv); + for (const QSize size : {QSize(2880, 1800), QSize(3840, 2160)}) { + XImageFixture fixture(size.width(), size.height()); + benchmarkImage(&fixture.image, QString("synthetic %1x%2").arg(size.width()).arg(size.height())); + } + if (app.arguments().contains("--desktop")) { + Display *display = XOpenDisplay(nullptr); + if (!display) + qFatal("Cannot open X display for real desktop benchmark"); + XWindowAttributes attributes{}; + XGetWindowAttributes(display, DefaultRootWindow(display), &attributes); + XImage *image = XGetImage(display, DefaultRootWindow(display), 0, 0, + attributes.width, attributes.height, AllPlanes, ZPixmap); + if (!image) + qFatal("Cannot capture desktop for benchmark"); + benchmarkImage(image, QStringLiteral("actual desktop")); + XDestroyImage(image); + XCloseDisplay(display); + } + std::vector before, after; + QElapsedTimer timer; + for (int i = 0; i < 31; ++i) { + timer.start(); + QProcess process; + process.start("lscpu"); + if (!process.waitForFinished(5000) || process.exitCode() != 0) + qFatal("lscpu failed during benchmark"); + const bool oldValue = process.readAllStandardOutput().toLower().contains("centaurhauls"); + before.push_back(timer.nsecsElapsed() / 1e6); + timer.start(); + QFile cpuInfo(QStringLiteral("/proc/cpuinfo")); + if (!cpuInfo.open(QIODevice::ReadOnly)) + qFatal("Cannot read cpuinfo during benchmark"); + const bool newValue = CpuVendor::isZhaoxin(cpuInfo); + after.push_back(timer.nsecsElapsed() / 1e6); + if (oldValue != newValue) + qFatal("CPU detection mismatch on this machine"); + } + QTextStream(stdout) << "CPU detection: lscpu median " << median(before) + << " ms, procfs median " << median(after) << " ms\n"; +} diff --git a/tests/startup/test_cpuvendor.cpp b/tests/startup/test_cpuvendor.cpp new file mode 100644 index 000000000..113e69366 --- /dev/null +++ b/tests/startup/test_cpuvendor.cpp @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later +#include +#include +#include "../../src/utils/cpuvendor.h" +static bool detect(QIODevice &source) +{ + return CpuVendor::isZhaoxin(source); +} +int main() +{ + int failures = 0; + const struct { QByteArray input; bool expected; } cases[] = { + {"vendor_id : CentaurHauls\n", true}, + {"processor : 0\n vendor_id\t: centaurhauls\r\n", true}, + {"vendor_id : GenuineIntel\n", false}, + {"vendor_id : AuthenticAMD\n", false}, + {"model name : CentaurHauls\n", false}, + {"vendor_id : CentaurHaulsExtra\n", false}, + {"vendor_id :\n", false}, + {"", false}, + {"CPU implementer : 0x41\n", false}, + {"vendor_id : GenuineIntel\n\nvendor_id : CentaurHauls", true} + }; + for (const auto &test : cases) { + QBuffer input; + input.setData(test.input); + input.open(QIODevice::ReadOnly); + if (detect(input) != test.expected) { + qCritical() << "FAIL: CPU vendor detection" << test.input; + ++failures; + } + } + QBuffer closed; + if (detect(closed)) { + qCritical() << "FAIL: Unreadable metadata must not identify a vendor"; + ++failures; + } + qInfo() << "CPU vendor tests:" << failures << "failures"; + return failures ? 1 : 0; +} diff --git a/tests/startup/test_startup.cpp b/tests/startup/test_startup.cpp new file mode 100644 index 000000000..87c83fed9 --- /dev/null +++ b/tests/startup/test_startup.cpp @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later +#include +#include "ximagefixture.h" +#ifndef STARTUP_TEST_LEGACY +#include "../../src/utils/ximageconverter.h" +#endif + +static int failures = 0; +static unsigned long calls = 0; +static unsigned long (*originalGetPixel)(XImage *, int, int); +static unsigned long countingGetPixel(XImage *image, int x, int y) +{ + ++calls; + return originalGetPixel(image, x, y); +} +static void expect(bool condition, const char *message) +{ + if (!condition) { + qCritical() << "FAIL:" << message; + ++failures; + } +} +static QImage convert(XImage *image) +{ +#ifdef STARTUP_TEST_LEGACY + return legacyXImageConversion(image); +#else + return XImageConverter::toImage(image); +#endif +} +static void checkLayout(int bits, bool native, int offset, bool fast) +{ + XImageFixture fixture(7, 5, bits, native, offset); + const QImage expected = legacyXImageConversion(&fixture.image); + // Poison the unused high byte: RGB32 must still become opaque. + if (bits == 32 && native) { + for (int y = 0; y < fixture.image.height; ++y) { + for (int x = 0; x < fixture.image.width; ++x) { + const int highByte = QSysInfo::ByteOrder == QSysInfo::LittleEndian ? 3 : 0; + fixture.image.data[y * fixture.image.bytes_per_line + x * 4 + highByte] = 0x42; + } + } + } + originalGetPixel = fixture.image.f.get_pixel; + fixture.image.f.get_pixel = countingGetPixel; + calls = 0; + const QImage result = convert(&fixture.image); + expect(result == expected, "All RGB pixels and alpha must match the original conversion"); + expect(fast ? calls == 0 : calls > 0, "Native aligned RGB32 must avoid per-pixel XGetPixel; other layouts must fall back"); + for (int y = 0; y < result.height(); ++y) { + for (int x = 0; x < result.width(); ++x) + expect(qAlpha(result.pixel(x, y)) == 255, "Captured pixels must be opaque"); + } + // The returned image must own its storage. + fixture.storage.clear(); + fixture.storage.shrink_to_fit(); + expect(result == expected, "Converted image must survive destruction of XImage storage"); +} +int main() +{ + checkLayout(32, true, 0, true); + checkLayout(32, false, 0, false); + checkLayout(24, true, 0, false); + checkLayout(32, true, 1, false); + expect(convert(nullptr).isNull(), "Null XImage must return a null image"); + XImage invalid{}; + expect(convert(&invalid).isNull(), "Empty XImage must return a null image"); + XImageFixture shallow(7, 5); + shallow.image.depth = 16; + XInitImage(&shallow.image); + expect(convert(&shallow.image) == legacyXImageConversion(&shallow.image), + "Nonstandard depth must preserve the legacy pixel mask"); + XImageFixture nonstandard(7, 5); + nonstandard.image.red_mask = 0xf800; + originalGetPixel = nonstandard.image.f.get_pixel; + nonstandard.image.f.get_pixel = countingGetPixel; + const QImage expected = legacyXImageConversion(&nonstandard.image); + calls = 0; + const QImage fallback = convert(&nonstandard.image); + expect(calls > 0 && fallback == expected, "Nonstandard masks must preserve the legacy fallback"); + XImageFixture invalidStride(7, 5); + invalidStride.image.bytes_per_line = 4; +#ifndef STARTUP_TEST_LEGACY + expect(convert(&invalidStride.image).isNull(), "Short source stride must be rejected"); + invalidStride.image.data = nullptr; + expect(convert(&invalidStride.image).isNull(), "Missing image data must be rejected"); +#endif + qInfo() << (failures ? "FAILED" : "PASSED") << failures << "failures"; + return failures ? 1 : 0; +} diff --git a/tests/startup/ximagefixture.h b/tests/startup/ximagefixture.h new file mode 100644 index 000000000..bf94f3046 --- /dev/null +++ b/tests/startup/ximagefixture.h @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include +#include +#include +#include +#include + +struct XImageFixture +{ + std::vector storage; + XImage image{}; + XImageFixture(int width, int height, int bits = 32, bool native = true, int offset = 0) + : storage((width * bits / 8 + 12) * height + offset) + { + image.width = width; + image.height = height; + image.format = ZPixmap; + image.data = storage.data() + offset; + const int nativeOrder = QSysInfo::ByteOrder == QSysInfo::LittleEndian ? LSBFirst : MSBFirst; + image.byte_order = native ? nativeOrder : (nativeOrder == LSBFirst ? MSBFirst : LSBFirst); + image.bitmap_unit = 32; + image.bitmap_bit_order = image.byte_order; + image.bitmap_pad = 32; + image.depth = 24; + image.bits_per_pixel = bits; + image.bytes_per_line = ((width * bits / 8 + 3) & ~3) + 8; + image.red_mask = 0xff0000; + image.green_mask = 0xff00; + image.blue_mask = 0xff; + if (!XInitImage(&image)) + qFatal("Could not initialize XImage fixture"); + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) + XPutPixel(&image, x, y, qRgb((x * 19 + y) % 256, (y * 53 + x) % 256, (x + y * 7) % 256)); + } + } +}; + +inline QImage legacyXImageConversion(XImage *image) +{ + if (!image || !image->data || image->width <= 0 || image->height <= 0) + return {}; + QImage result(image->width, image->height, QImage::Format_RGB32); + for (int y = 0; y < image->height; ++y) { + for (int x = 0; x < image->width; ++x) { + unsigned long pixel = XGetPixel(image, x, y); + result.setPixel(x, y, qRgb((pixel >> 16) & 0xff, (pixel >> 8) & 0xff, pixel & 0xff)); + } + } + return result; +} diff --git a/tests/ut_screen_shot_recorder/ut_screen_shot_recorder.pro b/tests/ut_screen_shot_recorder/ut_screen_shot_recorder.pro index 99b2a289d..09b336999 100644 --- a/tests/ut_screen_shot_recorder/ut_screen_shot_recorder.pro +++ b/tests/ut_screen_shot_recorder/ut_screen_shot_recorder.pro @@ -331,6 +331,8 @@ HEADERS += test_all_interfaces.h \ ut_scope_guard.h +SOURCES += ../../src/utils/ximageconverter.cpp ../../src/utils/cpuvendor.cpp + SOURCES += main.cpp \ ../../src/utils/audioutils.cpp \ ../../src/utils/baseutils.cpp \ diff --git a/tests/ut_screen_shot_recorder/ut_utils_cov2.h b/tests/ut_screen_shot_recorder/ut_utils_cov2.h index 4604df8a1..b22a62315 100644 --- a/tests/ut_screen_shot_recorder/ut_utils_cov2.h +++ b/tests/ut_screen_shot_recorder/ut_utils_cov2.h @@ -5,6 +5,8 @@ #pragma once #include #include +#include +#include "utils/cpuvendor.h" #include #include #include @@ -27,7 +29,7 @@ using namespace testing; // - isSysHighVersion1040 / isSysGreatEqualV23 (already partly covered, add // a fresh invocation to be safe). // - cursorMove with additional modifier combinations (Shift / Ctrl). -// - checkCpuIsZhaoxin via QProcess stub so no real `lscpu` is spawned. +// - CPU vendor parsing with in-memory metadata. // - Utils::instance() (singleton getter). // - notSupportWarn guarded by DDialog::exec stub. // @@ -135,32 +137,22 @@ TEST_F(UtilsCov2Test, isSysGreatEqualV23IsCallable) EXPECT_NO_FATAL_FAILURE((void)v); } -// ---------- checkCpuIsZhaoxin via stubbed QProcess ---------- -// Real checkCpuIsZhaoxin spawns `lscpu`; stub waitForFinished/readAll to keep -// the test hermetic and fast. The stub returns content with no CentaurHauls -// keyword so the function returns false deterministically. - -static void waitForFinished_stub(QProcess *) { return; } -static QByteArray readAllStandardOutput_stub() { return QByteArray("Vendor ID: GenuineIntel\n"); } +// ---------- CPU vendor metadata parsing ---------- TEST_F(UtilsCov2Test, checkCpuIsZhaoxinNoCentaurReturnsFalse) { - stub.set(ADDR(QProcess, waitForFinished), waitForFinished_stub); - stub.set(ADDR(QProcess, readAllStandardOutput), readAllStandardOutput_stub); - bool v = true; - EXPECT_NO_FATAL_FAILURE(v = Utils::checkCpuIsZhaoxin()); - EXPECT_FALSE(v); + QBuffer cpuInfo; + cpuInfo.setData("vendor_id : GenuineIntel\n"); + cpuInfo.open(QIODevice::ReadOnly); + EXPECT_FALSE(CpuVendor::isZhaoxin(cpuInfo)); } -static QByteArray readAllZhaoxin_stub() { return QByteArray("Vendor ID: CentaurHauls\n"); } - TEST_F(UtilsCov2Test, checkCpuIsZhaoxinWithCentaurReturnsTrue) { - stub.set(ADDR(QProcess, waitForFinished), waitForFinished_stub); - stub.set(ADDR(QProcess, readAllStandardOutput), readAllZhaoxin_stub); - bool v = false; - EXPECT_NO_FATAL_FAILURE(v = Utils::checkCpuIsZhaoxin()); - EXPECT_TRUE(v); + QBuffer cpuInfo; + cpuInfo.setData("vendor_id : CentaurHauls\n"); + cpuInfo.open(QIODevice::ReadOnly); + EXPECT_TRUE(CpuVendor::isZhaoxin(cpuInfo)); } // ---------- cursorMove with Shift / Ctrl modifiers ---------- From 67c91ce575d60f4a0fdeca21fd26bbde9b4971a3 Mon Sep 17 00:00:00 2001 From: Hualet Wang Date: Sun, 11 Oct 2026 23:42:32 +0800 Subject: [PATCH 2/2] perf(shot): adopt native XImage buffers and tidy review findings - Convert native RGB32 XImages in place and hand the Xlib buffer to QImage, removing a full-screen allocation and copy. - Relax validation so uncommon layouts take the legacy XGetPixel path instead of being rejected; write fallback pixels via scanLine. - Build ximageconverter only for Qt6, its sole user. - Update stale lscpu references in unit-test comments. Co-Authored-By: Claude --- src/CMakeLists.txt | 1 - src/src.pro | 8 ++- src/utils/ximageconverter.cpp | 70 +++++++++++-------- src/utils/ximageconverter.h | 4 +- tests/startup/README.md | 4 +- tests/startup/benchmark_startup.cpp | 7 +- tests/startup/test_startup.cpp | 29 +++++--- tests/startup/ximagefixture.h | 40 +++++++++-- .../ut_screen_shot_recorder.pro | 3 +- tests/ut_screen_shot_recorder/ut_utils_cov.h | 1 - tests/ut_screen_shot_recorder/ut_utils_ext.h | 2 +- tests/ut_screen_shot_recorder/ut_utils_ext2.h | 3 +- 12 files changed, 116 insertions(+), 56 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1f993774f..d65ea5ee2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -89,7 +89,6 @@ set ( deepin-screen-recorder_HDRS ) set ( deepin-screen-recorder_SRCS - utils/ximageconverter.cpp utils/cpuvendor.cpp utils/log.cpp utils/calculaterect.cpp diff --git a/src/src.pro b/src/src.pro index df846b159..2b72bb815 100755 --- a/src/src.pro +++ b/src/src.pro @@ -265,8 +265,12 @@ lessThan(QT_MAJOR_VERSION, 6) { } # Sources -HEADERS += utils/ximageconverter.h utils/cpuvendor.h -SOURCES += utils/ximageconverter.cpp utils/cpuvendor.cpp +HEADERS += utils/cpuvendor.h +SOURCES += utils/cpuvendor.cpp +equals(QT_MAJOR_VERSION, 6) { + HEADERS += utils/ximageconverter.h + SOURCES += utils/ximageconverter.cpp +} SOURCES += main.cpp \ capture.cpp \ diff --git a/src/utils/ximageconverter.cpp b/src/utils/ximageconverter.cpp index 272c0794f..a10ec8549 100644 --- a/src/utils/ximageconverter.cpp +++ b/src/utils/ximageconverter.cpp @@ -2,48 +2,58 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "ximageconverter.h" #include -#include #include #include -QImage XImageConverter::toImage(XImage *image) +namespace { +// True when the XImage buffer is already laid out as native-endian 0x??RRGGBB words. +bool isNativeRgb32(const XImage *image) { - if (!image || !image->data || image->width <= 0 || image->height <= 0 - || image->bits_per_pixel <= 0 || image->bits_per_pixel > 32 - || image->xoffset < 0 || image->bytes_per_line <= 0 - || qint64(image->bytes_per_line) * 8 < qint64(image->width) * image->bits_per_pixel - || !image->f.get_pixel) { - return {}; - } - - QImage result(image->width, image->height, QImage::Format_RGB32); - if (result.isNull()) - return result; - const int nativeOrder = QSysInfo::ByteOrder == QSysInfo::LittleEndian ? LSBFirst : MSBFirst; - const bool direct = image->format == ZPixmap && image->bits_per_pixel == 32 + return image->format == ZPixmap && image->bits_per_pixel == 32 && (image->depth == 24 || image->depth == 32) && image->red_mask == 0xff0000 && image->green_mask == 0xff00 && image->blue_mask == 0xff && image->byte_order == nativeOrder && image->xoffset == 0 + && image->bytes_per_line >= image->width * 4 && image->bytes_per_line % alignof(quint32) == 0 && reinterpret_cast(image->data) % alignof(quint32) == 0; +} + +void releaseXImageData(void *data) +{ + XFree(data); +} +} + +QImage XImageConverter::toImage(XImage *image) +{ + if (!image || !image->data || image->width <= 0 || image->height <= 0 || !image->f.get_pixel) + return {}; + + if (isNativeRgb32(image)) { + // Force the unused high byte opaque in place, then hand the buffer to QImage. + for (int y = 0; y < image->height; ++y) { + quint32 *row = reinterpret_cast(image->data + qsizetype(y) * image->bytes_per_line); + for (int x = 0; x < image->width; ++x) + row[x] |= 0xff000000u; + } + uchar *data = reinterpret_cast(image->data); + QImage result(data, image->width, image->height, image->bytes_per_line, + QImage::Format_RGB32, releaseXImageData, data); + if (!result.isNull()) + image->data = nullptr; + return result; + } + // Preserve the existing conversion for uncommon XImage layouts. + QImage result(image->width, image->height, QImage::Format_RGB32); + if (result.isNull()) + return result; for (int y = 0; y < image->height; ++y) { - if (direct) { - const char *source = image->data + qsizetype(y) * image->bytes_per_line; - QRgb *destination = reinterpret_cast(result.scanLine(y)); - for (int x = 0; x < image->width; ++x) { - quint32 pixel; - // memcpy avoids aliasing assumptions about Xlib-owned storage. - std::memcpy(&pixel, source + qsizetype(x) * sizeof(pixel), sizeof(pixel)); - destination[x] = pixel | 0xff000000u; - } - } else { - // Preserve the existing conversion for uncommon XImage layouts. - for (int x = 0; x < image->width; ++x) { - const unsigned long pixel = XGetPixel(image, x, y); - result.setPixel(x, y, qRgb((pixel >> 16) & 0xff, (pixel >> 8) & 0xff, pixel & 0xff)); - } + QRgb *row = reinterpret_cast(result.scanLine(y)); + for (int x = 0; x < image->width; ++x) { + const unsigned long pixel = XGetPixel(image, x, y); + row[x] = qRgb((pixel >> 16) & 0xff, (pixel >> 8) & 0xff, pixel & 0xff); } } return result; diff --git a/src/utils/ximageconverter.h b/src/utils/ximageconverter.h index 16b0130f4..cb8c59d4b 100644 --- a/src/utils/ximageconverter.h +++ b/src/utils/ximageconverter.h @@ -6,7 +6,9 @@ struct _XImage; namespace XImageConverter { -// Returns an owned, opaque RGB32 image; it never retains the XImage buffer. +// Returns an opaque RGB32 image. When the XImage already stores native RGB32 +// pixels, its buffer is reused: alpha is forced in place and image->data is set +// to nullptr. Callers must still release the XImage with XDestroyImage. QImage toImage(_XImage *image); } #endif diff --git a/tests/startup/README.md b/tests/startup/README.md index 383316030..91b9d9f1f 100644 --- a/tests/startup/README.md +++ b/tests/startup/README.md @@ -9,7 +9,7 @@ ctest --test-dir build/startup-tests --output-on-failure build/startup-tests/benchmark_startup ``` -The pixel tests cover row padding, alpha, source ownership, byte order, 24-bit storage, source alignment, nonstandard depth/masks and invalid input. CPU tests use in-memory metadata, including empty, unreadable and misleading input. +The pixel tests cover row padding, alpha, buffer adoption, byte order, 24-bit storage, source alignment, nonstandard depth/masks and invalid input. CPU tests use in-memory metadata, including empty, unreadable and misleading input. `benchmark_startup` alternates the old and new conversion order for 31 samples and compares every pixel. Add `--desktop` to also compare a real X11 desktop image in memory; it does not save the image. CPU timing compares `lscpu` with the procfs detector. @@ -39,6 +39,6 @@ Measured on X11 with Qt6, a 2880x1800 physical screen and DPR 1.75. Both variant | StartScreenshot method entry to showFullScreen return | 152.15 ms | 95.57 ms | | Process launch to service ready | 83.29 ms | 83.43 ms | -For 31 alternating samples, conversion of a real desktop XImage fell from 80.94 ms to 4.11 ms with identical pixels; CPU detection fell from 11.39 ms to 0.71 ms. These component timings exclude capture, QPixmap creation and display. Application measurements use log receipt times and include logging/reader overhead; they do not measure keyboard dispatch, cold D-Bus activation, cold filesystem cache or compositor presentation. +For 31 alternating samples, conversion of a real 2880x1800 desktop XImage fell from 55.00 ms to 2.22 ms with identical pixels; CPU detection fell from 6.60 ms to 0.67 ms. Native RGB32 images are converted in place and the Xlib buffer is adopted by the QImage, so there is no second full-screen allocation or copy; `XGetImage` itself (about 23 ms on the same machine) is now the dominant capture cost. These component timings exclude capture, QPixmap creation and display. Application measurements use log receipt times and include logging/reader overhead; they do not measure keyboard dispatch, cold D-Bus activation, cold filesystem cache or compositor presentation. Both before/after builds covered only the screenshot main program (`src/src.pro`). The top-level subdirectory project, Dock plugins, pin-screenshots program and Debian package were not built. In particular, the missing `libdtk6tablerecognizer-dev` dependency still prevents validating the pin-screenshots build. Qt5, multiscreen, Wayland/Treeland and the full existing unit-test suite were not exercised. The optimized main program also saved a valid opaque 2880x1800 PNG in the local fullscreen-save smoke test. diff --git a/tests/startup/benchmark_startup.cpp b/tests/startup/benchmark_startup.cpp index 2924faeb4..189195c25 100644 --- a/tests/startup/benchmark_startup.cpp +++ b/tests/startup/benchmark_startup.cpp @@ -20,7 +20,8 @@ static void benchmarkImage(XImage *image, const QString &label) { QTextStream output(stdout); const QImage expected = legacyXImageConversion(image); - const QImage actual = XImageConverter::toImage(image); + XImageClone check(*image); + const QImage actual = XImageConverter::toImage(&check.image); if (expected != actual) qFatal("Pixel mismatch during benchmark"); std::vector before, after; @@ -29,8 +30,10 @@ static void benchmarkImage(XImage *image, const QString &label) for (int i = 0; i < 31; ++i) { for (int step = 0; step < 2; ++step) { const bool fast = (i + step) % 2 == 0; + // The optimized conversion consumes its input, so each sample gets a fresh copy. + XImageClone input(*image); timer.start(); - QImage result = fast ? XImageConverter::toImage(image) : legacyXImageConversion(image); + QImage result = fast ? XImageConverter::toImage(&input.image) : legacyXImageConversion(&input.image); const double elapsed = timer.nsecsElapsed() / 1e6; if (result.isNull()) qFatal("Conversion failed during benchmark"); diff --git a/tests/startup/test_startup.cpp b/tests/startup/test_startup.cpp index 87c83fed9..73bea4cab 100644 --- a/tests/startup/test_startup.cpp +++ b/tests/startup/test_startup.cpp @@ -52,9 +52,12 @@ static void checkLayout(int bits, bool native, int offset, bool fast) for (int x = 0; x < result.width(); ++x) expect(qAlpha(result.pixel(x, y)) == 255, "Captured pixels must be opaque"); } - // The returned image must own its storage. - fixture.storage.clear(); - fixture.storage.shrink_to_fit(); +#ifndef STARTUP_TEST_LEGACY + expect(fast ? fixture.image.data == nullptr : fixture.image.data != nullptr, + "Native RGB32 buffers must be adopted; other layouts must stay with the XImage"); +#endif + // The returned image must not depend on storage the XImage still owns. + fixture.release(); expect(result == expected, "Converted image must survive destruction of XImage storage"); } int main() @@ -79,13 +82,19 @@ int main() calls = 0; const QImage fallback = convert(&nonstandard.image); expect(calls > 0 && fallback == expected, "Nonstandard masks must preserve the legacy fallback"); - XImageFixture invalidStride(7, 5); - invalidStride.image.bytes_per_line = 4; -#ifndef STARTUP_TEST_LEGACY - expect(convert(&invalidStride.image).isNull(), "Short source stride must be rejected"); - invalidStride.image.data = nullptr; - expect(convert(&invalidStride.image).isNull(), "Missing image data must be rejected"); -#endif + XImageFixture shortStride(7, 5); + shortStride.image.bytes_per_line = 4; + originalGetPixel = shortStride.image.f.get_pixel; + shortStride.image.f.get_pixel = countingGetPixel; + const QImage shortExpected = legacyXImageConversion(&shortStride.image); + calls = 0; + const QImage shortResult = convert(&shortStride.image); + expect(calls > 0 && shortResult == shortExpected, "Short source stride must use the legacy fallback"); + XImageFixture missingData(7, 5); + char *const missingStorage = missingData.image.data; + missingData.image.data = nullptr; + expect(convert(&missingData.image).isNull(), "Missing image data must be rejected"); + missingData.image.data = missingStorage; qInfo() << (failures ? "FAILED" : "PASSED") << failures << "failures"; return failures ? 1 : 0; } diff --git a/tests/startup/ximagefixture.h b/tests/startup/ximagefixture.h index bf94f3046..70efefff1 100644 --- a/tests/startup/ximagefixture.h +++ b/tests/startup/ximagefixture.h @@ -3,21 +3,26 @@ #pragma once #include #include -#include +#include +#include #include #include struct XImageFixture { - std::vector storage; + // malloc-backed like Xlib, because the converter may adopt and XFree the buffer. + char *storage = nullptr; XImage image{}; XImageFixture(int width, int height, int bits = 32, bool native = true, int offset = 0) - : storage((width * bits / 8 + 12) * height + offset) + : storage(static_cast(std::calloc((width * bits / 8 + 12) * height + offset, 1))) { + // An aligned nonzero offset could be adopted although it is not the allocation start. + if (offset % 4 == 0 && offset != 0) + qFatal("Fixture offsets must be zero or misaligned"); image.width = width; image.height = height; image.format = ZPixmap; - image.data = storage.data() + offset; + image.data = storage + offset; const int nativeOrder = QSysInfo::ByteOrder == QSysInfo::LittleEndian ? LSBFirst : MSBFirst; image.byte_order = native ? nativeOrder : (nativeOrder == LSBFirst ? MSBFirst : LSBFirst); image.bitmap_unit = 32; @@ -36,6 +41,33 @@ struct XImageFixture XPutPixel(&image, x, y, qRgb((x * 19 + y) % 256, (y * 53 + x) % 256, (x + y * 7) % 256)); } } + XImageFixture(const XImageFixture &) = delete; + XImageFixture &operator=(const XImageFixture &) = delete; + ~XImageFixture() { release(); } + // Mirrors XDestroyImage: frees the buffer unless the converter adopted it. + void release() + { + if (image.data) + std::free(storage); + image.data = nullptr; + storage = nullptr; + } +}; + +// Copies an XImage into a fresh malloc buffer so a consuming conversion can run repeatedly. +struct XImageClone +{ + XImage image; + explicit XImageClone(const XImage &source) + : image(source) + { + const size_t size = size_t(source.bytes_per_line) * source.height; + image.data = static_cast(std::malloc(size)); + std::memcpy(image.data, source.data, size); + } + XImageClone(const XImageClone &) = delete; + XImageClone &operator=(const XImageClone &) = delete; + ~XImageClone() { std::free(image.data); } }; inline QImage legacyXImageConversion(XImage *image) diff --git a/tests/ut_screen_shot_recorder/ut_screen_shot_recorder.pro b/tests/ut_screen_shot_recorder/ut_screen_shot_recorder.pro index 09b336999..7d05db59b 100644 --- a/tests/ut_screen_shot_recorder/ut_screen_shot_recorder.pro +++ b/tests/ut_screen_shot_recorder/ut_screen_shot_recorder.pro @@ -331,7 +331,8 @@ HEADERS += test_all_interfaces.h \ ut_scope_guard.h -SOURCES += ../../src/utils/ximageconverter.cpp ../../src/utils/cpuvendor.cpp +SOURCES += ../../src/utils/cpuvendor.cpp +equals(QT_MAJOR_VERSION, 6): SOURCES += ../../src/utils/ximageconverter.cpp SOURCES += main.cpp \ ../../src/utils/audioutils.cpp \ diff --git a/tests/ut_screen_shot_recorder/ut_utils_cov.h b/tests/ut_screen_shot_recorder/ut_utils_cov.h index 9d4f318de..fe718ea13 100644 --- a/tests/ut_screen_shot_recorder/ut_utils_cov.h +++ b/tests/ut_screen_shot_recorder/ut_utils_cov.h @@ -22,7 +22,6 @@ using namespace testing; // / cancelInputEvent1 / enableXGrabButton / disableXGrabButton / passInputEvent // (the X11 branches call XShapeCombineRectangles/XGrabButton/QX11Info and are // FORBIDDEN by the task constraints; the Wayland branches just log+return). -// - checkCpuIsZhaoxin with QProcess stubbed so no real `lscpu` is spawned. // - cursorMove additional key branches not exercised by ext2. // // The global Utils::isWaylandMode flag is flipped for the duration of each diff --git a/tests/ut_screen_shot_recorder/ut_utils_ext.h b/tests/ut_screen_shot_recorder/ut_utils_ext.h index e20808b2f..bbb593f93 100644 --- a/tests/ut_screen_shot_recorder/ut_utils_ext.h +++ b/tests/ut_screen_shot_recorder/ut_utils_ext.h @@ -21,7 +21,7 @@ using namespace testing; // Deliberately SKIPPED (touch Wayland / X11 / QProcess / modal dialog per the rules): // - passInputEvent, getInputEvent, cancelInputEvent, cancelInputEvent1 (XShapeCombineRectangles / QX11Info) // - enableXGrabButton, disableXGrabButton (XGrabButton / QX11Info) -// - checkCpuIsZhaoxin (spawns QProcess "lscpu") +// - checkCpuIsZhaoxin (host /proc/cpuinfo; parsing in ut_utils_cov2.h) // - cursorMove (QCursor::setPos / WaylandMouseSimulator) // - notSupportWarn (DDialog::exec modal loop) // - getAllWindowInfo (DForeignWindow + X11 geometry) diff --git a/tests/ut_screen_shot_recorder/ut_utils_ext2.h b/tests/ut_screen_shot_recorder/ut_utils_ext2.h index bbed5d9df..18b3ea6b0 100644 --- a/tests/ut_screen_shot_recorder/ut_utils_ext2.h +++ b/tests/ut_screen_shot_recorder/ut_utils_ext2.h @@ -16,7 +16,8 @@ using namespace testing; // Deliberately SKIPPED (X11/Wayland/modal/process dependencies): // - passInputEvent, getInputEvent, cancelInputEvent, cancelInputEvent1 // - enableXGrabButton, disableXGrabButton -// - checkCpuIsZhaoxin, notSupportWarn +// - checkCpuIsZhaoxin (host /proc/cpuinfo; parsing in ut_utils_cov2.h) +// - notSupportWarn // - getAllWindowInfo, cursorMove (QCursor::setPos) // - getCurrentAudioChannel (QProcess) // - blurRect, clearBlur (DWindowManager)