diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f84d2b5da..d65ea5ee2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -89,6 +89,7 @@ set ( deepin-screen-recorder_HDRS ) set ( deepin-screen-recorder_SRCS + utils/cpuvendor.cpp utils/log.cpp utils/calculaterect.cpp utils/shapesutils.cpp diff --git a/src/src.pro b/src/src.pro index 8a5ca01af..2b72bb815 100755 --- a/src/src.pro +++ b/src/src.pro @@ -265,6 +265,13 @@ lessThan(QT_MAJOR_VERSION, 6) { } # Sources +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 \ 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..a10ec8549 --- /dev/null +++ b/src/utils/ximageconverter.cpp @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later +#include "ximageconverter.h" +#include +#include +#include + +namespace { +// True when the XImage buffer is already laid out as native-endian 0x??RRGGBB words. +bool isNativeRgb32(const XImage *image) +{ + const int nativeOrder = QSysInfo::ByteOrder == QSysInfo::LittleEndian ? LSBFirst : MSBFirst; + 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) { + 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 new file mode 100644 index 000000000..cb8c59d4b --- /dev/null +++ b/src/utils/ximageconverter.h @@ -0,0 +1,14 @@ +// 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 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/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..91b9d9f1f --- /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, 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. + +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 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_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..189195c25 --- /dev/null +++ b/tests/startup/benchmark_startup.cpp @@ -0,0 +1,88 @@ +// 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); + XImageClone check(*image); + const QImage actual = XImageConverter::toImage(&check.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; + // The optimized conversion consumes its input, so each sample gets a fresh copy. + XImageClone input(*image); + timer.start(); + QImage result = fast ? XImageConverter::toImage(&input.image) : legacyXImageConversion(&input.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..73bea4cab --- /dev/null +++ b/tests/startup/test_startup.cpp @@ -0,0 +1,100 @@ +// 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"); + } +#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() +{ + 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 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 new file mode 100644 index 000000000..70efefff1 --- /dev/null +++ b/tests/startup/ximagefixture.h @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once +#include +#include +#include +#include +#include +#include + +struct XImageFixture +{ + // 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(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 + 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)); + } + } + 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) +{ + 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..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,6 +331,9 @@ HEADERS += test_all_interfaces.h \ ut_scope_guard.h +SOURCES += ../../src/utils/cpuvendor.cpp +equals(QT_MAJOR_VERSION, 6): SOURCES += ../../src/utils/ximageconverter.cpp + SOURCES += main.cpp \ ../../src/utils/audioutils.cpp \ ../../src/utils/baseutils.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_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 ---------- 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)