From b4c4931fc06275f8fd098622a67137338b6e7d71 Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Fri, 14 Aug 2026 14:41:46 +0200 Subject: [PATCH 01/38] Fix OpenGL context creation on Wayland --- app/main.cpp | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/app/main.cpp b/app/main.cpp index 46858055..db84cd4f 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -65,6 +65,25 @@ int main(int argc, char **argv) { // originalHandler = qInstallMessageHandler(filter_log); QQuickWindow::setGraphicsApi(QSGRendererInterface::GraphicsApi::OpenGLRhi); + + QSurfaceFormat fmt; + fmt.setDepthBufferSize(24); +#ifdef ALP_ENABLE_DEV_TOOLS + fmt.setOption(QSurfaceFormat::DebugContext); +#endif + + if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGL) { + qDebug("Requesting 3.3 core context"); + fmt.setRenderableType(QSurfaceFormat::OpenGL); + fmt.setVersion(3, 3); + fmt.setProfile(QSurfaceFormat::CoreProfile); + } else { + qDebug("Requesting 3.0 context"); + fmt.setVersion(3, 0); + } + + QSurfaceFormat::setDefaultFormat(fmt); + #if defined(ALP_ENABLE_DEV_TOOLS) || defined(__ANDROID__) QApplication app(argc, argv); #else @@ -121,23 +140,6 @@ int main(int argc, char **argv) } } - QSurfaceFormat fmt; - fmt.setDepthBufferSize(24); -#ifdef ALP_ENABLE_DEV_TOOLS - fmt.setOption(QSurfaceFormat::DebugContext); -#endif - - if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGL) { - qDebug("Requesting 3.3 core context"); - fmt.setVersion(3, 3); - fmt.setProfile(QSurfaceFormat::CoreProfile); - } else { - qDebug("Requesting 3.0 context"); - fmt.setVersion(3, 0); - } - - QSurfaceFormat::setDefaultFormat(fmt); - // create in main thread #ifdef ALP_ENABLE_DEV_TOOLS TimerFrontendManager::instance(); @@ -192,4 +194,3 @@ int main(int argc, char **argv) return app.exec(); } - From 959d00bf0d65c903982851230f39649e9bf908dc Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Mon, 17 Aug 2026 18:59:49 +0200 Subject: [PATCH 02/38] Add GPU texture compression benchmark --- CMakeLists.txt | 6 +- .../BenchmarkItem.cpp | 426 ++++++++++++++++++ .../BenchmarkItem.h | 64 +++ .../CMakeLists.txt | 63 +++ apps/texture_compression_benchmark/Main.qml | 181 ++++++++ .../android/AndroidManifest.xml | 17 + apps/texture_compression_benchmark/main.cpp | 41 ++ gl_engine/CMakeLists.txt | 2 + gl_engine/ShaderProgram.cpp | 40 +- gl_engine/ShaderProgram.h | 7 +- gl_engine/Texture.cpp | 253 +++++++++++ gl_engine/Texture.h | 50 ++ gl_engine/shaders/texture_compress.frag | 3 + gl_engine/shaders/texture_compress.vert | 186 ++++++++ unittests/gl_engine/texture.cpp | 96 ++++ 15 files changed, 1420 insertions(+), 15 deletions(-) create mode 100644 apps/texture_compression_benchmark/BenchmarkItem.cpp create mode 100644 apps/texture_compression_benchmark/BenchmarkItem.h create mode 100644 apps/texture_compression_benchmark/CMakeLists.txt create mode 100644 apps/texture_compression_benchmark/Main.qml create mode 100644 apps/texture_compression_benchmark/android/AndroidManifest.xml create mode 100644 apps/texture_compression_benchmark/main.cpp create mode 100644 gl_engine/shaders/texture_compress.frag create mode 100644 gl_engine/shaders/texture_compress.vert diff --git a/CMakeLists.txt b/CMakeLists.txt index 3594dccf..4dd9be3d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,7 @@ option(ALP_BUILD_UNITTESTS "include unit test targets in the buildsystem" ON) option(ALP_BUILD_GL_ENGINE "include the gl engine in the buildsystem" OFF) option(ALP_BUILD_PLAIN_RENDERER "include the plain renderer in the buildsystem" ON) option(ALP_BUILD_ALPINEAPP "include the qml app in the buildsystem" ON) +option(ALP_BUILD_TEXTURE_COMPRESSION_BENCHMARK "include the texture compression benchmark application" OFF) set(ALP_WEBGPU_DEFAULT ON) if (APPLE OR ANDROID) set(ALP_WEBGPU_DEFAULT OFF) @@ -124,7 +125,7 @@ endif() add_subdirectory(nucleus) -if (ALP_BUILD_GL_ENGINE OR ALP_BUILD_PLAIN_RENDERER OR ALP_BUILD_ALPINEAPP) +if (ALP_BUILD_GL_ENGINE OR ALP_BUILD_PLAIN_RENDERER OR ALP_BUILD_ALPINEAPP OR ALP_BUILD_TEXTURE_COMPRESSION_BENCHMARK) add_subdirectory(gl_engine) endif() if (ALP_BUILD_PLAIN_RENDERER) @@ -139,6 +140,9 @@ if (ALP_BUILD_ALPINEAPP) endif() add_subdirectory(app) endif() +if (ALP_BUILD_TEXTURE_COMPRESSION_BENCHMARK) + add_subdirectory(apps/texture_compression_benchmark) +endif() if (ALP_BUILD_WEBGPU_BASE OR ALP_BUILD_WEBGPU_ENGINE OR ALP_BUILD_WEBGPU_COMPUTE OR ALP_BUILD_WEBGPU_APP) include(${CMAKE_SOURCE_DIR}/cmake/SetupWebGPUPlatform.cmake) diff --git a/apps/texture_compression_benchmark/BenchmarkItem.cpp b/apps/texture_compression_benchmark/BenchmarkItem.cpp new file mode 100644 index 00000000..43419f7a --- /dev/null +++ b/apps/texture_compression_benchmark/BenchmarkItem.cpp @@ -0,0 +1,426 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2026 Adam Celarek + * SPDX-License-Identifier: GPL-3.0-or-later + *****************************************************************************/ + +#include "BenchmarkItem.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace { +using Raster = radix::Raster; +using Clock = std::chrono::steady_clock; + +struct Statistics { + double median = 0.0; + double p95 = 0.0; + double minimum = 0.0; + double maximum = 0.0; +}; + +Statistics statistics(std::vector values) +{ + Q_ASSERT(!values.empty()); + std::ranges::sort(values); + const auto percentile = [&](double fraction) { + const auto index = std::min(values.size() - 1, size_t(std::ceil(fraction * double(values.size()))) - 1); + return values[index]; + }; + return { percentile(0.5), percentile(0.95), values.front(), values.back() }; +} + +QJsonObject to_json(const Statistics& value) +{ + return { { QStringLiteral("median_ms"), value.median }, + { QStringLiteral("p95_ms"), value.p95 }, + { QStringLiteral("min_ms"), value.minimum }, + { QStringLiteral("max_ms"), value.maximum } }; +} + +double elapsed_ms(Clock::time_point start) +{ + return std::chrono::duration(Clock::now() - start).count(); +} + +double srgb_to_linear(uint8_t value) +{ + const auto normalised = double(value) / 255.0; + if (normalised <= 0.04045) + return normalised / 12.92; + return std::pow((normalised + 0.055) / 1.055, 2.4); +} + +double linear_psnr(const QImage& reconstructed, const Raster& source) +{ + double squared_error = 0.0; + for (int y = 0; y < reconstructed.height(); ++y) { + for (int x = 0; x < reconstructed.width(); ++x) { + const auto actual = reconstructed.pixel(x, y); + const auto expected = source.pixel({ x, y }); + const std::array actual_channels { qRed(actual) / 255.0, qGreen(actual) / 255.0, qBlue(actual) / 255.0 }; + const std::array expected_channels { + srgb_to_linear(expected.x), srgb_to_linear(expected.y), srgb_to_linear(expected.z) + }; + for (size_t channel = 0; channel < actual_channels.size(); ++channel) { + const auto difference = actual_channels[channel] - expected_channels[channel]; + squared_error += difference * difference; + } + } + } + const auto mse = squared_error / double(reconstructed.width() * reconstructed.height() * 3); + return mse == 0.0 ? std::numeric_limits::infinity() : 10.0 * std::log10(1.0 / mse); +} + +QImage reconstruct(gl_engine::Texture& texture, unsigned resolution) +{ + gl_engine::Framebuffer framebuffer( + gl_engine::Framebuffer::DepthFormat::None, { gl_engine::Framebuffer::ColourFormat::RGBA8 }, { resolution, resolution }); + framebuffer.bind(); + gl_engine::ShaderProgram shader(R"( + out highp vec2 texcoords; + void main() { + vec2 vertices[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); + gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0); + texcoords = 0.5 * gl_Position.xy + vec2(0.5); + })", + R"( + uniform lowp sampler2DArray texture_sampler; + in highp vec2 texcoords; + out lowp vec4 out_color; + void main() { + out_color = textureLod(texture_sampler, vec3(texcoords.x, 1.0 - texcoords.y, 0.0), 0.0); + })", + gl_engine::ShaderCodeSource::PLAINTEXT); + shader.bind(); + texture.bind(0); + shader.set_uniform("texture_sampler", 0); + gl_engine::helpers::create_screen_quad_geometry().draw(); + auto result = framebuffer.read_colour_attachment(0); + gl_engine::Framebuffer::unbind(); + return result; +} + +std::vector cpu_compress( + std::span sources, nucleus::utils::ColourTexture::Format algorithm, bool mipmaps) +{ + std::vector result; + result.reserve(sources.size()); + for (const auto& source : sources) { + if (mipmaps) { + result.push_back(nucleus::utils::generate_mipmapped_colour_texture(source, algorithm)); + } else { + nucleus::utils::MipmappedColourTexture levels; + levels.emplace_back(source, algorithm); + result.push_back(std::move(levels)); + } + } + return result; +} + +QString gl_string(GLenum name) +{ + const auto* value = QOpenGLContext::currentContext()->functions()->glGetString(name); + return value ? QString::fromLatin1(reinterpret_cast(value)) : QStringLiteral("unavailable"); +} + +} // namespace + +class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { +public: + void synchronize(QQuickFramebufferObject* item) override + { + auto* benchmark_item = static_cast(item); + m_item = benchmark_item; + m_window = benchmark_item->window(); + if (benchmark_item->m_request_serial == m_seen_serial) + return; + m_seen_serial = benchmark_item->m_request_serial; + m_effort = benchmark_item->m_effort; + m_batch_size = benchmark_item->m_batch_size; + m_iterations = benchmark_item->m_iterations; + m_mipmaps = benchmark_item->m_mipmaps; + m_pending = true; + } + + void render() override + { + if (!m_pending) + return; + m_pending = false; + m_window->beginExternalCommands(); + const auto [text, json] = run(); + m_window->endExternalCommands(); + QPointer item = m_item; + QMetaObject::invokeMethod(m_item, [item, text, json]() { + if (item) + item->publishResults(text, json); + }); + } + + QOpenGLFramebufferObject* createFramebufferObject(const QSize&) override + { + QOpenGLFramebufferObjectFormat format; + format.setAttachment(QOpenGLFramebufferObject::NoAttachment); + return new QOpenGLFramebufferObject(QSize(1, 1), format); + } + +private: + std::pair run() + { + constexpr unsigned resolution = 512; + QImage input(QStringLiteral(":/benchmark/merged.jpg")); + if (input.isNull()) + return { QStringLiteral("Unable to load the benchmark image."), QStringLiteral("{}") }; + input = input.convertToFormat(QImage::Format_RGBA8888).scaled( + int(resolution), int(resolution), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + const auto source = nucleus::tile::conversion::to_rgba8raster(input); + std::vector sources(size_t(m_batch_size), source); + std::vector layers(size_t(m_batch_size), 0u); + std::iota(layers.begin(), layers.end(), 0u); + + if (!gl_engine::TextureCompressor::is_supported()) { + QJsonObject root { + { QStringLiteral("renderer"), gl_string(GL_RENDERER) }, + { QStringLiteral("vendor"), gl_string(GL_VENDOR) }, + { QStringLiteral("version"), gl_string(GL_VERSION) }, + { QStringLiteral("supported"), false }, + { QStringLiteral("error"), QStringLiteral("Compressed texture arrays require WEBGL_compressed_texture_etc") }, + }; + const auto json = QString::fromUtf8(QJsonDocument(root).toJson(QJsonDocument::Indented)); + qInfo().noquote() << json; + return { QStringLiteral("GPU compression is unavailable: this WebGL device does not expose WEBGL_compressed_texture_etc."), json }; + } + + const auto algorithm = gl_engine::Texture::compression_algorithm(); + const auto filter = m_mipmaps ? gl_engine::Texture::Filter::MipMapLinear : gl_engine::Texture::Filter::Linear; + gl_engine::Texture cpu_destination(gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); + cpu_destination.setParams(filter, gl_engine::Texture::Filter::Linear); + cpu_destination.allocate_array(resolution, resolution, unsigned(m_batch_size)); + gl_engine::Texture gpu_destination(gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); + gpu_destination.setParams(filter, gl_engine::Texture::Filter::Linear); + gpu_destination.allocate_array(resolution, resolution, unsigned(m_batch_size)); + gl_engine::TextureCompressor gpu_compressor(resolution, resolution, unsigned(m_batch_size)); + + auto upload_cpu = [&](const std::vector& compressed) { + for (size_t layer = 0; layer < compressed.size(); ++layer) + cpu_destination.upload(compressed[layer], unsigned(layer)); + QOpenGLContext::currentContext()->extraFunctions()->glFinish(); + }; + + auto warmup_cpu = cpu_compress(sources, algorithm, m_mipmaps); + upload_cpu(warmup_cpu); + static_cast(gpu_compressor.compress(sources, + gpu_destination, + layers, + { .algorithm = algorithm, .effort = unsigned(m_effort), .generate_mipmaps = m_mipmaps })); + + std::vector cpu_compression_times; + std::vector cpu_total_times; + std::vector gpu_upload_times; + std::vector gpu_mipmap_times; + std::vector gpu_encoding_times; + std::vector gpu_compressed_upload_times; + std::vector gpu_total_times; + cpu_compression_times.reserve(size_t(m_iterations)); + cpu_total_times.reserve(size_t(m_iterations)); + gpu_total_times.reserve(size_t(m_iterations)); + + for (int iteration = 0; iteration < m_iterations; ++iteration) { + const auto cpu_start = Clock::now(); + auto compressed = cpu_compress(sources, algorithm, m_mipmaps); + const auto cpu_compression_time = elapsed_ms(cpu_start); + upload_cpu(compressed); + cpu_compression_times.push_back(cpu_compression_time); + cpu_total_times.push_back(elapsed_ms(cpu_start)); + + const auto gpu = gpu_compressor.compress(sources, + gpu_destination, + layers, + { .algorithm = algorithm, .effort = unsigned(m_effort), .generate_mipmaps = m_mipmaps }); + gpu_upload_times.push_back(gpu.timings.scratch_upload_ms); + gpu_mipmap_times.push_back(gpu.timings.mipmap_generation_ms); + gpu_encoding_times.push_back(gpu.timings.encoding_ms); + gpu_compressed_upload_times.push_back(gpu.timings.compressed_upload_ms); + gpu_total_times.push_back(gpu.timings.total_ms); + } + + const auto cpu_compression = statistics(cpu_compression_times); + const auto cpu_total = statistics(cpu_total_times); + const auto gpu_upload = statistics(gpu_upload_times); + const auto gpu_mipmap = statistics(gpu_mipmap_times); + const auto gpu_encoding = statistics(gpu_encoding_times); + const auto gpu_compressed_upload = statistics(gpu_compressed_upload_times); + const auto gpu_total = statistics(gpu_total_times); + const auto cpu_psnr = linear_psnr(reconstruct(cpu_destination, resolution), source); + const auto gpu_psnr = linear_psnr(reconstruct(gpu_destination, resolution), source); + const auto algorithm_name = algorithm == nucleus::utils::ColourTexture::Format::DXT1 ? QStringLiteral("DXT1 / BC1") : QStringLiteral("ETC1 in ETC2"); + + QJsonObject root { + { QStringLiteral("renderer"), gl_string(GL_RENDERER) }, + { QStringLiteral("vendor"), gl_string(GL_VENDOR) }, + { QStringLiteral("version"), gl_string(GL_VERSION) }, + { QStringLiteral("supported"), true }, + { QStringLiteral("algorithm"), algorithm_name }, + { QStringLiteral("timing_method"), QStringLiteral("glFinish-synchronised wall time") }, + { QStringLiteral("resolution"), int(resolution) }, + { QStringLiteral("batch_size"), m_batch_size }, + { QStringLiteral("iterations"), m_iterations }, + { QStringLiteral("effort"), m_effort }, + { QStringLiteral("mipmaps"), m_mipmaps }, + { QStringLiteral("cpu_compression"), to_json(cpu_compression) }, + { QStringLiteral("cpu_end_to_end"), to_json(cpu_total) }, + { QStringLiteral("gpu_scratch_upload"), to_json(gpu_upload) }, + { QStringLiteral("gpu_mipmap_generation"), to_json(gpu_mipmap) }, + { QStringLiteral("gpu_encoding"), to_json(gpu_encoding) }, + { QStringLiteral("gpu_compressed_upload"), to_json(gpu_compressed_upload) }, + { QStringLiteral("gpu_end_to_end"), to_json(gpu_total) }, + { QStringLiteral("cpu_psnr_db"), cpu_psnr }, + { QStringLiteral("gpu_psnr_db"), gpu_psnr }, + { QStringLiteral("cpu_tiles_per_second"), 1000.0 * m_batch_size / cpu_total.median }, + { QStringLiteral("gpu_tiles_per_second"), 1000.0 * m_batch_size / gpu_total.median }, + }; + const auto json = QString::fromUtf8(QJsonDocument(root).toJson(QJsonDocument::Indented)); + const auto line = [](QString label, Statistics value) { + return QStringLiteral("%1 median %2 ms p95 %3 ms").arg(label, -26).arg(value.median, 8, 'f', 3).arg(value.p95, 8, 'f', 3); + }; + QStringList summary { + QStringLiteral("%1 — %2 × %3, batch %4, effort %5, mipmaps %6") + .arg(algorithm_name) + .arg(resolution) + .arg(resolution) + .arg(m_batch_size) + .arg(m_effort) + .arg(m_mipmaps ? QStringLiteral("on") : QStringLiteral("off")), + gl_string(GL_RENDERER), + QStringLiteral("Timing: completion-synchronised wall time"), + QString(), + line(QStringLiteral("CPU compression"), cpu_compression), + line(QStringLiteral("CPU end-to-end"), cpu_total), + line(QStringLiteral("GPU scratch upload"), gpu_upload), + line(QStringLiteral("GPU mip generation"), gpu_mipmap), + line(QStringLiteral("GPU encoding"), gpu_encoding), + line(QStringLiteral("GPU compressed upload"), gpu_compressed_upload), + line(QStringLiteral("GPU end-to-end"), gpu_total), + QString(), + QStringLiteral("CPU completed throughput %1 tiles/s").arg(1000.0 * m_batch_size / cpu_total.median, 0, 'f', 1), + QStringLiteral("GPU completed throughput %1 tiles/s").arg(1000.0 * m_batch_size / gpu_total.median, 0, 'f', 1), + QStringLiteral("CPU PSNR %1 dB").arg(cpu_psnr, 0, 'f', 2), + QStringLiteral("GPU PSNR %1 dB").arg(gpu_psnr, 0, 'f', 2), + }; + qInfo().noquote() << json; + return { summary.join('\n'), json }; + } + + QPointer m_item; + QQuickWindow* m_window = nullptr; + unsigned m_seen_serial = 0; + int m_effort = 4; + int m_batch_size = 4; + int m_iterations = 7; + bool m_mipmaps = true; + bool m_pending = false; +}; + +BenchmarkItem::BenchmarkItem(QQuickItem* parent) + : QQuickFramebufferObject(parent) +{ +} + +QQuickFramebufferObject::Renderer* BenchmarkItem::createRenderer() const { return new BenchmarkRenderer; } + +int BenchmarkItem::effort() const { return m_effort; } +void BenchmarkItem::setEffort(int value) +{ + value = std::clamp(value, 0, 10); + if (m_effort == value) + return; + m_effort = value; + emit effortChanged(); +} + +int BenchmarkItem::batchSize() const { return m_batch_size; } +void BenchmarkItem::setBatchSize(int value) +{ + if (value != 1 && value != 4 && value != 16) + return; + if (m_batch_size == value) + return; + m_batch_size = value; + emit batchSizeChanged(); +} + +int BenchmarkItem::iterations() const { return m_iterations; } +void BenchmarkItem::setIterations(int value) +{ + value = std::clamp(value, 1, 50); + if (m_iterations == value) + return; + m_iterations = value; + emit iterationsChanged(); +} + +bool BenchmarkItem::mipmaps() const { return m_mipmaps; } +void BenchmarkItem::setMipmaps(bool value) +{ + if (m_mipmaps == value) + return; + m_mipmaps = value; + emit mipmapsChanged(); +} + +bool BenchmarkItem::running() const { return m_running; } +QString BenchmarkItem::resultText() const { return m_result_text; } +QString BenchmarkItem::resultJson() const { return m_result_json; } + +void BenchmarkItem::runBenchmark() +{ + if (m_running) + return; + m_running = true; + ++m_request_serial; + emit runningChanged(); + update(); +} + +void BenchmarkItem::copyResultJson() +{ + if (!m_result_json.isEmpty()) + QGuiApplication::clipboard()->setText(m_result_json); +} + +void BenchmarkItem::publishResults(const QString& text, const QString& json) +{ + m_result_text = text; + m_result_json = json; + m_running = false; + emit resultTextChanged(); + emit resultJsonChanged(); + emit runningChanged(); +} diff --git a/apps/texture_compression_benchmark/BenchmarkItem.h b/apps/texture_compression_benchmark/BenchmarkItem.h new file mode 100644 index 00000000..88ebd25a --- /dev/null +++ b/apps/texture_compression_benchmark/BenchmarkItem.h @@ -0,0 +1,64 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2026 Adam Celarek + * SPDX-License-Identifier: GPL-3.0-or-later + *****************************************************************************/ + +#pragma once + +#include +#include +#include + +class BenchmarkItem : public QQuickFramebufferObject { + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(int effort READ effort WRITE setEffort NOTIFY effortChanged) + Q_PROPERTY(int batchSize READ batchSize WRITE setBatchSize NOTIFY batchSizeChanged) + Q_PROPERTY(int iterations READ iterations WRITE setIterations NOTIFY iterationsChanged) + Q_PROPERTY(bool mipmaps READ mipmaps WRITE setMipmaps NOTIFY mipmapsChanged) + Q_PROPERTY(bool running READ running NOTIFY runningChanged) + Q_PROPERTY(QString resultText READ resultText NOTIFY resultTextChanged) + Q_PROPERTY(QString resultJson READ resultJson NOTIFY resultJsonChanged) + +public: + explicit BenchmarkItem(QQuickItem* parent = nullptr); + Renderer* createRenderer() const override; + + [[nodiscard]] int effort() const; + void setEffort(int value); + [[nodiscard]] int batchSize() const; + void setBatchSize(int value); + [[nodiscard]] int iterations() const; + void setIterations(int value); + [[nodiscard]] bool mipmaps() const; + void setMipmaps(bool value); + [[nodiscard]] bool running() const; + [[nodiscard]] QString resultText() const; + [[nodiscard]] QString resultJson() const; + + Q_INVOKABLE void runBenchmark(); + Q_INVOKABLE void copyResultJson(); + +signals: + void effortChanged(); + void batchSizeChanged(); + void iterationsChanged(); + void mipmapsChanged(); + void runningChanged(); + void resultTextChanged(); + void resultJsonChanged(); + +private: + friend class BenchmarkRenderer; + void publishResults(const QString& text, const QString& json); + + int m_effort = 4; + int m_batch_size = 4; + int m_iterations = 7; + bool m_mipmaps = true; + bool m_running = false; + unsigned m_request_serial = 0; + QString m_result_text = QStringLiteral("Run the benchmark to collect results."); + QString m_result_json; +}; diff --git a/apps/texture_compression_benchmark/CMakeLists.txt b/apps/texture_compression_benchmark/CMakeLists.txt new file mode 100644 index 00000000..f4619532 --- /dev/null +++ b/apps/texture_compression_benchmark/CMakeLists.txt @@ -0,0 +1,63 @@ +############################################################################# +# AlpineMaps.org +# Copyright (C) 2026 Adam Celarek +# SPDX-License-Identifier: GPL-3.0-or-later +############################################################################# + +project(texture-compression-benchmark LANGUAGES CXX) + +qt_add_executable(texture_compression_benchmark + main.cpp + BenchmarkItem.h + BenchmarkItem.cpp +) + +qt_add_qml_module(texture_compression_benchmark + URI TextureCompressionBenchmark + VERSION 1.0 + RESOURCE_PREFIX /qt/qml + QML_FILES Main.qml +) + +qt_add_resources(texture_compression_benchmark "benchmark_data" + PREFIX "/benchmark" + BASE "${CMAKE_SOURCE_DIR}/unittests/nucleus/data/quad" + FILES "${CMAKE_SOURCE_DIR}/unittests/nucleus/data/quad/merged.jpg" +) + +qt_add_resources(texture_compression_benchmark "fonts" + BASE "${alpineapp_fonts_SOURCE_DIR}" + PREFIX "/fonts" + FILES "${alpineapp_fonts_SOURCE_DIR}/Roboto/Roboto-Regular.ttf" +) + +target_link_libraries(texture_compression_benchmark PUBLIC gl_engine Qt::Quick Qt::QuickControls2) +alp_configure_target(texture_compression_benchmark) + +if (ANDROID) + set_target_properties(texture_compression_benchmark PROPERTIES + QT_ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android" + QT_ANDROID_PACKAGE_NAME "org.alpinemaps.texturecompressionbenchmark" + QT_ANDROID_VERSION_NAME "1.0" + QT_ANDROID_VERSION_CODE 1 + ) + install(TARGETS texture_compression_benchmark + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +endif() + +if (EMSCRIPTEN) + install( + FILES + "$/texture_compression_benchmark.js" + "$/texture_compression_benchmark.wasm" + "$/texture_compression_benchmark.html" + "$/qtloader.js" + DESTINATION "${ALP_WWW_INSTALL_DIR}/texture_compression_benchmark" + ) + install( + FILES "$/texture_compression_benchmark.worker.js" + DESTINATION "${ALP_WWW_INSTALL_DIR}/texture_compression_benchmark" + OPTIONAL + ) +endif() diff --git a/apps/texture_compression_benchmark/Main.qml b/apps/texture_compression_benchmark/Main.qml new file mode 100644 index 00000000..956d9146 --- /dev/null +++ b/apps/texture_compression_benchmark/Main.qml @@ -0,0 +1,181 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import TextureCompressionBenchmark + +ApplicationWindow { + id: root + + width: 900 + height: 760 + minimumWidth: 360 + minimumHeight: 640 + visible: true + title: qsTr("Texture Compression Benchmark") + LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft + LayoutMirroring.childrenInherit: true + + BenchmarkItem { + id: benchmark + x: 0 + y: 0 + z: -1 + width: 1 + height: 1 + } + + ScrollView { + id: scrollView + anchors.fill: parent + + ColumnLayout { + width: scrollView.availableWidth + spacing: 16 + + Label { + Layout.fillWidth: true + Layout.leftMargin: 20 + Layout.rightMargin: 20 + Layout.topMargin: 20 + text: qsTr("CPU versus GPU texture compression") + font.pointSize: 20 + font.weight: Font.Medium + wrapMode: Text.Wrap + } + + Label { + Layout.fillWidth: true + Layout.leftMargin: 20 + Layout.rightMargin: 20 + text: qsTr("Measures DXT1 or ETC1 compression of 512×512 ortho textures, including optional mipmaps and final texture-array upload. WebGL requires WEBGL_compressed_texture_etc.") + wrapMode: Text.Wrap + } + + GroupBox { + Layout.fillWidth: true + Layout.leftMargin: 20 + Layout.rightMargin: 20 + title: qsTr("Configuration") + + ColumnLayout { + anchors.fill: parent + spacing: 12 + + Label { + Layout.fillWidth: true + text: qsTr("GPU effort: %1").arg(benchmark.effort) + } + + Slider { + id: effortSlider + Layout.fillWidth: true + from: 0 + to: 10 + stepSize: 1 + value: benchmark.effort + enabled: !benchmark.running + onMoved: benchmark.effort = Math.round(value) + } + + RowLayout { + Layout.fillWidth: true + + Label { + Layout.fillWidth: true + text: qsTr("Batch size") + } + + ComboBox { + id: batchSizeBox + Layout.preferredWidth: 120 + model: [1, 4, 16] + currentIndex: 1 + enabled: !benchmark.running + onActivated: benchmark.batchSize = Number(currentText) + } + } + + RowLayout { + Layout.fillWidth: true + + Label { + Layout.fillWidth: true + text: qsTr("Measured iterations") + } + + SpinBox { + Layout.preferredWidth: 120 + from: 1 + to: 50 + value: benchmark.iterations + editable: true + enabled: !benchmark.running + onValueModified: benchmark.iterations = value + } + } + + CheckBox { + Layout.fillWidth: true + text: qsTr("Generate and compress mipmaps") + checked: benchmark.mipmaps + enabled: !benchmark.running + onToggled: benchmark.mipmaps = checked + } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 20 + Layout.rightMargin: 20 + + Button { + text: qsTr("Run benchmark") + enabled: !benchmark.running + highlighted: true + onClicked: benchmark.runBenchmark() + } + + BusyIndicator { + Layout.preferredWidth: 44 + Layout.preferredHeight: 44 + running: benchmark.running + visible: benchmark.running + } + + Label { + Layout.fillWidth: true + text: benchmark.running ? qsTr("Benchmarking; the display may pause to avoid contaminating GPU measurements.") : qsTr("Ready") + wrapMode: Text.Wrap + } + } + + GroupBox { + Layout.fillWidth: true + Layout.leftMargin: 20 + Layout.rightMargin: 20 + Layout.bottomMargin: 20 + title: qsTr("Results") + + ColumnLayout { + anchors.fill: parent + + TextArea { + Layout.fillWidth: true + Layout.minimumHeight: 300 + text: benchmark.resultText + readOnly: true + selectByMouse: true + wrapMode: TextEdit.Wrap + } + + Button { + text: qsTr("Copy JSON") + enabled: !benchmark.running && benchmark.resultJson.length > 0 + onClicked: benchmark.copyResultJson() + } + } + } + } + } +} diff --git a/apps/texture_compression_benchmark/android/AndroidManifest.xml b/apps/texture_compression_benchmark/android/AndroidManifest.xml new file mode 100644 index 00000000..81518d7f --- /dev/null +++ b/apps/texture_compression_benchmark/android/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/apps/texture_compression_benchmark/main.cpp b/apps/texture_compression_benchmark/main.cpp new file mode 100644 index 00000000..8c2ea468 --- /dev/null +++ b/apps/texture_compression_benchmark/main.cpp @@ -0,0 +1,41 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2026 Adam Celarek + * SPDX-License-Identifier: GPL-3.0-or-later + *****************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include + +int main(int argc, char** argv) +{ + QQuickWindow::setGraphicsApi(QSGRendererInterface::GraphicsApi::OpenGLRhi); + + QSurfaceFormat format; + if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGL) { + format.setRenderableType(QSurfaceFormat::OpenGL); + format.setVersion(3, 3); + format.setProfile(QSurfaceFormat::CoreProfile); + } else { + format.setVersion(3, 0); + } + QSurfaceFormat::setDefaultFormat(format); + + QGuiApplication application(argc, argv); + QCoreApplication::setOrganizationName(QStringLiteral("AlpineMaps.org")); + QCoreApplication::setApplicationName(QStringLiteral("TextureCompressionBenchmark")); + QGuiApplication::setApplicationDisplayName(QStringLiteral("Texture Compression Benchmark")); + QFontDatabase::addApplicationFont(QStringLiteral(":/fonts/Roboto/Roboto-Regular.ttf")); + application.setFont(QFont(QStringLiteral("Roboto"), 12, QFont::Normal)); + + QQmlApplicationEngine engine; + engine.loadFromModule("TextureCompressionBenchmark", "Main"); + if (engine.rootObjects().isEmpty()) + return -1; + return application.exec(); +} diff --git a/gl_engine/CMakeLists.txt b/gl_engine/CMakeLists.txt index e71be0c1..09712cce 100644 --- a/gl_engine/CMakeLists.txt +++ b/gl_engine/CMakeLists.txt @@ -87,6 +87,8 @@ qt_add_resources(gl_engine "shaders" shaders/tile_id.glsl shaders/track.frag shaders/track.vert + shaders/texture_compress.frag + shaders/texture_compress.vert shaders/turbo_colormap.glsl shaders/intersection.glsl shaders/eaws.glsl diff --git a/gl_engine/ShaderProgram.cpp b/gl_engine/ShaderProgram.cpp index 9e9ac75d..49c17abb 100644 --- a/gl_engine/ShaderProgram.cpp +++ b/gl_engine/ShaderProgram.cpp @@ -177,11 +177,16 @@ QString ShaderProgram::read_file_content_local(const QString& name) { // =========== MEMBER DECLARATIONS ======================= -ShaderProgram::ShaderProgram(QString vertex_shader, QString fragment_shader, ShaderCodeSource code_source, const std::vector& defines) +ShaderProgram::ShaderProgram(QString vertex_shader, + QString fragment_shader, + ShaderCodeSource code_source, + const std::vector& defines, + const std::vector& transform_feedback_varyings) : m_vertex_shader(vertex_shader) , m_fragment_shader(fragment_shader) , m_code_source(code_source) , m_defines(defines) + , m_transform_feedback_varyings(transform_feedback_varyings) { reload(); Q_ASSERT(m_q_shader_program); @@ -320,21 +325,30 @@ void ShaderProgram::reload() outputMeaningfullErrors(program->log(), vertexCode, m_vertex_shader); } else if (!program->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentCode)) { outputMeaningfullErrors(program->log(), fragmentCode, m_fragment_shader); - } else if (!program->link()) { + } else { + if (!m_transform_feedback_varyings.empty()) { + std::vector varyings; + varyings.reserve(m_transform_feedback_varyings.size()); + for (const auto& varying : m_transform_feedback_varyings) + varyings.push_back(varying.constData()); + QOpenGLContext::currentContext()->extraFunctions()->glTransformFeedbackVaryings( + program->programId(), GLsizei(varyings.size()), varyings.data(), GL_INTERLEAVED_ATTRIBS); + } + if (!program->link()) { #ifdef _MSC_VER - // when using msvc in github ci qDebug/Critical don't print when an assert fails - // effectively, we don't see any error - std::cerr << "error linking shader " << m_vertex_shader.toStdString() << "and" << m_fragment_shader.toStdString() << std::endl; - fflush(stderr); - fflush(stdout); + // when using msvc in github ci qDebug/Critical don't print when an assert fails + // effectively, we don't see any error + std::cerr << "error linking shader " << m_vertex_shader.toStdString() << "and" << m_fragment_shader.toStdString() << std::endl; + fflush(stderr); + fflush(stdout); #else - qCritical() << "error linking shader " << m_vertex_shader.toStdString() << "and" << m_fragment_shader.toStdString(); + qCritical() << "error linking shader " << m_vertex_shader.toStdString() << "and" << m_fragment_shader.toStdString(); #endif - } else { - // NO ERROR - m_q_shader_program = std::move(program); - m_cached_attribs.clear(); - m_cached_uniforms.clear(); + } else { + m_q_shader_program = std::move(program); + m_cached_attribs.clear(); + m_cached_uniforms.clear(); + } } } diff --git a/gl_engine/ShaderProgram.h b/gl_engine/ShaderProgram.h index 79a14916..9f4a5260 100644 --- a/gl_engine/ShaderProgram.h +++ b/gl_engine/ShaderProgram.h @@ -56,6 +56,7 @@ class ShaderProgram { QString m_fragment_shader; // either filename or native shader code ShaderCodeSource m_code_source; std::vector m_defines; + std::vector m_transform_feedback_varyings; #if ALP_ENABLE_SHADER_NETWORK_HOTRELOAD // A temporary cache for the downloaded shader files. @@ -84,7 +85,11 @@ class ShaderProgram { static void preprocess_shader_content_inplace(QString& base); public: - ShaderProgram(QString vertex_shader, QString fragment_shader, ShaderCodeSource code_source = ShaderCodeSource::FILE, const std::vector& defines = {}); + ShaderProgram(QString vertex_shader, + QString fragment_shader, + ShaderCodeSource code_source = ShaderCodeSource::FILE, + const std::vector& defines = {}, + const std::vector& transform_feedback_varyings = {}); int attribute_location(const std::string& name); void bind(); diff --git a/gl_engine/Texture.cpp b/gl_engine/Texture.cpp index 7d613d23..1a49e0dd 100644 --- a/gl_engine/Texture.cpp +++ b/gl_engine/Texture.cpp @@ -17,11 +17,16 @@ *****************************************************************************/ #include "Texture.h" +#include "ShaderProgram.h" #include "nucleus/utils/ColourTexture.h" #include #include #include +#include +#include +#include +#include #ifdef __EMSCRIPTEN__ #include #endif @@ -377,3 +382,251 @@ float gl_engine::Texture::max_anisotropy() return max_anisotropy; #endif } + +namespace { +template double measure_finished_gl(Callable&& callable) +{ + const auto start = std::chrono::steady_clock::now(); + std::forward(callable)(); + QOpenGLContext::currentContext()->extraFunctions()->glFinish(); + const auto end = std::chrono::steady_clock::now(); + return std::chrono::duration(end - start).count(); +} +} + +struct gl_engine::TextureCompressor::Impl { + unsigned width = 0; + unsigned height = 0; + unsigned max_batch_size = 0; + unsigned scratch_layers = 0; + GLuint scratch_texture = 0; + GLuint encoded_buffer = 0; + GLuint vertex_array = 0; + GLuint transform_feedback = 0; + std::unique_ptr dxt1_program; + std::unique_ptr etc1_program; + + Impl(unsigned texture_width, unsigned texture_height, unsigned maximum_batch_size) + : width(texture_width) + , height(texture_height) + , max_batch_size(maximum_batch_size) + { + Q_ASSERT(width > 0 && height > 0 && max_batch_size > 0); + auto* f = QOpenGLContext::currentContext()->extraFunctions(); + f->glGenBuffers(1, &encoded_buffer); + f->glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, encoded_buffer); + size_t maximum_size = 0; + for (unsigned level = 0; level < TextureCompressor::mip_level_count(width, height); ++level) { + maximum_size += TextureCompressor::compressed_level_size( + std::max(1u, width >> level), std::max(1u, height >> level)); + } + maximum_size *= max_batch_size; + f->glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, GLsizeiptr(maximum_size), nullptr, GL_STREAM_DRAW); + f->glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0); + f->glGenVertexArrays(1, &vertex_array); + f->glGenTransformFeedbacks(1, &transform_feedback); + + const std::vector varyings { QByteArrayLiteral("encoded_block") }; + dxt1_program = std::make_unique( + "texture_compress.vert", "texture_compress.frag", ShaderCodeSource::FILE, std::vector {}, varyings); + etc1_program = std::make_unique("texture_compress.vert", + "texture_compress.frag", + ShaderCodeSource::FILE, + std::vector { QStringLiteral("#define ALP_COMPRESS_ETC1") }, + varyings); + } + + ~Impl() + { + dxt1_program.reset(); + etc1_program.reset(); + if (!QOpenGLContext::currentContext()) + return; + auto* f = QOpenGLContext::currentContext()->extraFunctions(); + f->glDeleteTransformFeedbacks(1, &transform_feedback); + f->glDeleteVertexArrays(1, &vertex_array); + f->glDeleteBuffers(1, &encoded_buffer); + if (scratch_texture) + f->glDeleteTextures(1, &scratch_texture); + } + + void ensure_scratch_storage(unsigned layers) + { + if (scratch_layers == layers) + return; + auto* f = QOpenGLContext::currentContext()->extraFunctions(); + if (scratch_texture) + f->glDeleteTextures(1, &scratch_texture); + f->glGenTextures(1, &scratch_texture); + f->glBindTexture(GL_TEXTURE_2D_ARRAY, scratch_texture); + f->glTexStorage3D(GL_TEXTURE_2D_ARRAY, + GLsizei(TextureCompressor::mip_level_count(width, height)), + GL_RGBA8, + GLsizei(width), + GLsizei(height), + GLsizei(layers)); + f->glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST); + f->glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + f->glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + f->glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + scratch_layers = layers; + } +}; + +gl_engine::TextureCompressor::TextureCompressor(unsigned width, unsigned height, unsigned max_batch_size) + : m(std::make_unique(width, height, max_batch_size)) +{ +} + +gl_engine::TextureCompressor::~TextureCompressor() = default; + +size_t gl_engine::TextureCompressor::compressed_level_size(unsigned width, unsigned height) +{ + return size_t(std::max(1u, (width + 3) / 4)) * std::max(1u, (height + 3) / 4) * 8; +} + +unsigned gl_engine::TextureCompressor::mip_level_count(unsigned width, unsigned height) +{ + Q_ASSERT(width > 0 && height > 0); + return 1u + unsigned(std::floor(std::log2(std::max(width, height)))); +} + +bool gl_engine::TextureCompressor::is_supported() +{ +#if defined(__EMSCRIPTEN__) + const auto context = emscripten_webgl_get_current_context(); + return context && emscripten_webgl_enable_extension(context, "WEBGL_compressed_texture_etc"); +#else + return true; +#endif +} + +gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std::span> textures, + Texture& destination, + std::span destination_layers, + const Settings& settings) +{ + Q_ASSERT(is_supported()); + Q_ASSERT(!textures.empty()); + Q_ASSERT(textures.size() == destination_layers.size()); + Q_ASSERT(textures.size() <= m->max_batch_size); + Q_ASSERT(destination.m_target == Texture::Target::_2dArray); + Q_ASSERT(destination.m_format == Texture::Format::CompressedRGBA8); + Q_ASSERT(destination.m_width == m->width && destination.m_height == m->height); + Q_ASSERT(settings.algorithm == Texture::compression_algorithm()); + Q_ASSERT(settings.effort <= 10); + for (size_t i = 0; i < textures.size(); ++i) { + Q_ASSERT(unsigned(textures[i].width()) == m->width && unsigned(textures[i].height()) == m->height); + Q_ASSERT(destination_layers[i] < destination.m_n_layers); + } + + m->ensure_scratch_storage(unsigned(textures.size())); + auto* f = QOpenGLContext::currentContext()->extraFunctions(); + Result result; + result.mip_levels = settings.generate_mipmaps ? mip_level_count(m->width, m->height) : 1; + + result.timings.scratch_upload_ms = measure_finished_gl([&]() { + f->glBindTexture(GL_TEXTURE_2D_ARRAY, m->scratch_texture); + f->glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + for (size_t layer = 0; layer < textures.size(); ++layer) { + f->glTexSubImage3D(GL_TEXTURE_2D_ARRAY, + 0, + 0, + 0, + GLint(layer), + GLsizei(m->width), + GLsizei(m->height), + 1, + GL_RGBA, + GL_UNSIGNED_BYTE, + textures[layer].bytes().data()); + } + }); + + if (settings.generate_mipmaps) { + result.timings.mipmap_generation_ms = measure_finished_gl([&]() { + f->glBindTexture(GL_TEXTURE_2D_ARRAY, m->scratch_texture); + f->glGenerateMipmap(GL_TEXTURE_2D_ARRAY); + }); + } + + std::vector level_offsets; + level_offsets.reserve(result.mip_levels); + size_t total_encoded_size = 0; + for (unsigned level = 0; level < result.mip_levels; ++level) { + level_offsets.push_back(total_encoded_size); + const auto level_width = std::max(1u, m->width >> level); + const auto level_height = std::max(1u, m->height >> level); + total_encoded_size += compressed_level_size(level_width, level_height) * textures.size(); + } + result.encoded_bytes = total_encoded_size; + + result.timings.encoding_ms = measure_finished_gl([&]() { + auto* program = settings.algorithm == nucleus::utils::ColourTexture::Format::DXT1 ? m->dxt1_program.get() : m->etc1_program.get(); + program->bind(); + program->set_uniform("source_texture", 7); + program->set_uniform("effort", int(settings.effort)); + f->glActiveTexture(GL_TEXTURE7); + f->glBindTexture(GL_TEXTURE_2D_ARRAY, m->scratch_texture); + f->glBindVertexArray(m->vertex_array); + f->glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, m->transform_feedback); + f->glEnable(GL_RASTERIZER_DISCARD); + + for (unsigned level = 0; level < result.mip_levels; ++level) { + const auto level_width = std::max(1u, m->width >> level); + const auto level_height = std::max(1u, m->height >> level); + const auto blocks_x = std::max(1u, (level_width + 3) / 4); + const auto blocks_y = std::max(1u, (level_height + 3) / 4); + const auto level_size = compressed_level_size(level_width, level_height) * textures.size(); + program->set_uniform("texture_width", int(level_width)); + program->set_uniform("texture_height", int(level_height)); + program->set_uniform("blocks_x", int(blocks_x)); + program->set_uniform("blocks_y", int(blocks_y)); + program->set_uniform("mip_level", int(level)); + f->glBindBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, + 0, + m->encoded_buffer, + GLintptr(level_offsets[level]), + GLsizeiptr(level_size)); + f->glBeginTransformFeedback(GL_POINTS); + f->glDrawArrays(GL_POINTS, 0, GLsizei(blocks_x * blocks_y * textures.size())); + f->glEndTransformFeedback(); + } + + f->glDisable(GL_RASTERIZER_DISCARD); + f->glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, 0); + f->glBindVertexArray(0); + program->release(); + }); + + result.timings.compressed_upload_ms = measure_finished_gl([&]() { + f->glBindTexture(GL_TEXTURE_2D_ARRAY, destination.m_id); + f->glBindBuffer(GL_PIXEL_UNPACK_BUFFER, m->encoded_buffer); + const auto format = Texture::compressed_texture_format(); + for (unsigned level = 0; level < result.mip_levels; ++level) { + const auto level_width = std::max(1u, m->width >> level); + const auto level_height = std::max(1u, m->height >> level); + const auto layer_size = compressed_level_size(level_width, level_height); + for (size_t layer = 0; layer < textures.size(); ++layer) { + const auto offset = level_offsets[level] + layer_size * layer; + f->glCompressedTexSubImage3D(GL_TEXTURE_2D_ARRAY, + GLint(level), + 0, + 0, + GLint(destination_layers[layer]), + GLsizei(level_width), + GLsizei(level_height), + 1, + format, + GLsizei(layer_size), + reinterpret_cast(quintptr(offset))); + } + } + f->glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); + }); + + f->glActiveTexture(GL_TEXTURE0); + result.timings.total_ms = result.timings.scratch_upload_ms + result.timings.mipmap_generation_ms + result.timings.encoding_ms + + result.timings.compressed_upload_ms; + return result; +} diff --git a/gl_engine/Texture.h b/gl_engine/Texture.h index 41328a8e..af8b608e 100644 --- a/gl_engine/Texture.h +++ b/gl_engine/Texture.h @@ -20,7 +20,10 @@ #include #include +#include #include +#include +#include #ifdef ANDROID #include #endif @@ -28,6 +31,8 @@ #include namespace gl_engine { +class TextureCompressor; + class Texture { public: enum class Target : GLenum { _2d = GL_TEXTURE_2D, _2dArray = GL_TEXTURE_2D_ARRAY }; // no 1D textures in webgl @@ -72,6 +77,8 @@ class Texture { static float max_anisotropy(); private: + friend class TextureCompressor; + GLuint m_id = GLuint(-1); Target m_target = Target::_2d; Format m_format = Format::Invalid; @@ -82,6 +89,49 @@ class Texture { unsigned m_n_layers = unsigned(-1); }; +class TextureCompressor { +public: + struct Settings { + nucleus::utils::ColourTexture::Format algorithm = nucleus::utils::ColourTexture::Format::DXT1; + unsigned effort = 0; + bool generate_mipmaps = true; + }; + + struct Timings { + double scratch_upload_ms = 0.0; + double mipmap_generation_ms = 0.0; + double encoding_ms = 0.0; + double compressed_upload_ms = 0.0; + double total_ms = 0.0; + }; + + struct Result { + Timings timings; + size_t encoded_bytes = 0; + unsigned mip_levels = 0; + }; + + TextureCompressor(unsigned width, unsigned height, unsigned max_batch_size); + ~TextureCompressor(); + TextureCompressor(const TextureCompressor&) = delete; + TextureCompressor(TextureCompressor&&) = delete; + TextureCompressor& operator=(const TextureCompressor&) = delete; + TextureCompressor& operator=(TextureCompressor&&) = delete; + + [[nodiscard]] Result compress(std::span> textures, + Texture& destination, + std::span destination_layers, + const Settings& settings); + + [[nodiscard]] static size_t compressed_level_size(unsigned width, unsigned height); + [[nodiscard]] static unsigned mip_level_count(unsigned width, unsigned height); + [[nodiscard]] static bool is_supported(); + +private: + struct Impl; + std::unique_ptr m; +}; + extern template void gl_engine::Texture::upload(const radix::Raster&); extern template void gl_engine::Texture::upload(const radix::Raster&); extern template void gl_engine::Texture::upload>(const radix::Raster>&); diff --git a/gl_engine/shaders/texture_compress.frag b/gl_engine/shaders/texture_compress.frag new file mode 100644 index 00000000..91981030 --- /dev/null +++ b/gl_engine/shaders/texture_compress.frag @@ -0,0 +1,3 @@ +void main() +{ +} diff --git a/gl_engine/shaders/texture_compress.vert b/gl_engine/shaders/texture_compress.vert new file mode 100644 index 00000000..dc096cff --- /dev/null +++ b/gl_engine/shaders/texture_compress.vert @@ -0,0 +1,186 @@ +uniform highp sampler2DArray source_texture; +uniform highp int texture_width; +uniform highp int texture_height; +uniform highp int blocks_x; +uniform highp int blocks_y; +uniform highp int mip_level; +uniform highp int effort; + +flat out highp uvec2 encoded_block; + +highp uvec3 unpack_565(highp uint value) +{ + return uvec3(((value >> 11u) & 31u) * 255u / 31u, + ((value >> 5u) & 63u) * 255u / 63u, + (value & 31u) * 255u / 31u); +} + +highp uint pack_565(highp uvec3 value) +{ + return ((value.r * 31u + 127u) / 255u) << 11u | ((value.g * 63u + 127u) / 255u) << 5u | (value.b * 31u + 127u) / 255u; +} + +highp uint colour_error(highp uvec3 lhs, highp uvec3 rhs) +{ + highp ivec3 delta = ivec3(lhs) - ivec3(rhs); + return uint(dot(delta, delta)); +} + +highp uvec2 encode_dxt1(highp uvec3 pixels[16]) +{ + highp uvec3 minimum_colour = uvec3(255u); + highp uvec3 maximum_colour = uvec3(0u); + for (int i = 0; i < 16; ++i) { + minimum_colour = min(minimum_colour, pixels[i]); + maximum_colour = max(maximum_colour, pixels[i]); + } + + highp uint best_error = 0xffffffffu; + highp uint best_endpoints = 0u; + highp uint best_indices = 0u; + for (int candidate = 0; candidate <= 10; ++candidate) { + if (candidate > effort) + break; + highp uvec3 range = maximum_colour - minimum_colour; + highp uvec3 inset = range * uint(candidate) / 64u; + highp uint colour0 = pack_565(maximum_colour - inset); + highp uint colour1 = pack_565(minimum_colour + inset); + if (colour0 <= colour1) { + highp uint swap_value = colour0; + colour0 = colour1; + colour1 = swap_value; + } + if (colour0 == colour1) { + if (colour0 < 65535u) + ++colour0; + else + --colour1; + } + + highp uvec3 palette[4]; + palette[0] = unpack_565(colour0); + palette[1] = unpack_565(colour1); + palette[2] = (2u * palette[0] + palette[1]) / 3u; + palette[3] = (palette[0] + 2u * palette[1]) / 3u; + + highp uint total_error = 0u; + highp uint indices = 0u; + for (int i = 0; i < 16; ++i) { + highp uint selected = 0u; + highp uint selected_error = colour_error(pixels[i], palette[0]); + for (uint palette_index = 1u; palette_index < 4u; ++palette_index) { + highp uint error = colour_error(pixels[i], palette[palette_index]); + if (error < selected_error) { + selected = palette_index; + selected_error = error; + } + } + total_error += selected_error; + indices |= selected << uint(2 * i); + } + if (total_error < best_error) { + best_error = total_error; + best_endpoints = colour0 | colour1 << 16u; + best_indices = indices; + } + } + return uvec2(best_endpoints, best_indices); +} + +highp uint byte_swap(highp uint value) +{ + return value >> 24u | (value >> 8u & 0x0000ff00u) | (value << 8u & 0x00ff0000u) | value << 24u; +} + +highp int modifier(highp int table, highp int index) +{ + const highp ivec4 modifiers[8] = ivec4[8](ivec4(2, 8, -2, -8), + ivec4(5, 17, -5, -17), + ivec4(9, 29, -9, -29), + ivec4(13, 42, -13, -42), + ivec4(18, 60, -18, -60), + ivec4(24, 80, -24, -80), + ivec4(33, 106, -33, -106), + ivec4(47, 183, -47, -183)); + return modifiers[table][index]; +} + +highp uvec2 encode_etc1(highp uvec3 pixels[16]) +{ + highp uvec3 sum = uvec3(0u); + for (int i = 0; i < 16; ++i) + sum += pixels[i]; + highp ivec3 average = ivec3((sum + 8u) / 16u); + + highp uint best_error = 0xffffffffu; + highp uvec3 best_base = uvec3(0u); + highp uint best_table = 0u; + highp uint best_indices = 0u; + for (int candidate = 0; candidate <= 10; ++candidate) { + if (candidate > effort) + break; + highp int magnitude = ((candidate + 1) / 2) * 4; + highp int signed_offset = candidate == 0 ? 0 : ((candidate & 1) == 1 ? magnitude : -magnitude); + highp ivec3 adjusted = clamp(average + ivec3(signed_offset), ivec3(0), ivec3(255)); + highp uvec3 base5 = (uvec3(adjusted) * 31u + 127u) / 255u; + highp ivec3 decoded_base = ivec3((base5 << 3u) | (base5 >> 2u)); + + for (int table = 0; table < 8; ++table) { + highp uint total_error = 0u; + highp uint indices = 0u; + for (int y = 0; y < 4; ++y) { + for (int x = 0; x < 4; ++x) { + highp int pixel_index = y * 4 + x; + highp uint selected = 0u; + highp uint selected_error = 0xffffffffu; + for (int index = 0; index < 4; ++index) { + highp ivec3 reconstructed = clamp(decoded_base + ivec3(modifier(table, index)), ivec3(0), ivec3(255)); + highp ivec3 delta = ivec3(pixels[pixel_index]) - reconstructed; + highp uint error = uint(dot(delta, delta)); + if (error < selected_error) { + selected = uint(index); + selected_error = error; + } + } + total_error += selected_error; + highp uint bit_position = uint(x * 4 + y); + indices |= (selected & 1u) << bit_position; + indices |= (selected >> 1u) << (bit_position + 16u); + } + } + if (total_error < best_error) { + best_error = total_error; + best_base = base5; + best_table = uint(table); + best_indices = indices; + } + } + } + + highp uint control = best_table << 5u | best_table << 2u | 2u; + highp uint header = best_base.r << 3u | best_base.g << 11u | best_base.b << 19u | control << 24u; + return uvec2(header, byte_swap(best_indices)); +} + +void main() +{ + highp int blocks_per_layer = blocks_x * blocks_y; + highp int layer = gl_VertexID / blocks_per_layer; + highp int block_index = gl_VertexID - layer * blocks_per_layer; + highp ivec2 block = ivec2(block_index % blocks_x, block_index / blocks_x); + highp ivec2 origin = block * 4; + highp uvec3 pixels[16]; + for (int y = 0; y < 4; ++y) { + for (int x = 0; x < 4; ++x) { + highp ivec2 position = min(origin + ivec2(x, y), ivec2(texture_width - 1, texture_height - 1)); + pixels[y * 4 + x] = uvec3(round(texelFetch(source_texture, ivec3(position, layer), mip_level).rgb * 255.0)); + } + } + +#ifdef ALP_COMPRESS_ETC1 + encoded_block = encode_etc1(pixels); +#else + encoded_block = encode_dxt1(pixels); +#endif + gl_Position = vec4(0.0); +} diff --git a/unittests/gl_engine/texture.cpp b/unittests/gl_engine/texture.cpp index 89f14b6e..d032339f 100644 --- a/unittests/gl_engine/texture.cpp +++ b/unittests/gl_engine/texture.cpp @@ -18,6 +18,9 @@ #include #include +#include +#include +#include #include #include "UnittestGLContext.h" @@ -277,6 +280,35 @@ QImage create_test_rgba_qimage(unsigned width, unsigned height) } radix::Raster create_test_rgba_raster(unsigned width, unsigned height) { return nucleus::tile::conversion::to_rgba8raster(create_test_rgba_qimage(width, height)); } +double srgb_to_linear(uint8_t value) +{ + const auto normalised = double(value) / 255.0; + if (normalised <= 0.04045) + return normalised / 12.92; + return std::pow((normalised + 0.055) / 1.055, 2.4); +} + +double linear_psnr(const QImage& reconstructed, const radix::Raster& source) +{ + double squared_error = 0.0; + for (int y = 0; y < reconstructed.height(); ++y) { + for (int x = 0; x < reconstructed.width(); ++x) { + const auto actual = reconstructed.pixel(x, y); + const auto expected = source.pixel({ x, y }); + const std::array actual_channels { qRed(actual) / 255.0, qGreen(actual) / 255.0, qBlue(actual) / 255.0 }; + const std::array expected_channels { + srgb_to_linear(expected.x), srgb_to_linear(expected.y), srgb_to_linear(expected.z) + }; + for (size_t channel = 0; channel < actual_channels.size(); ++channel) { + const auto difference = actual_channels[channel] - expected_channels[channel]; + squared_error += difference * difference; + } + } + } + const auto mse = squared_error / double(reconstructed.width() * reconstructed.height() * 3); + return mse == 0.0 ? std::numeric_limits::infinity() : 10.0 * std::log10(1.0 / mse); +} + } // namespace TEST_CASE("gl texture") @@ -656,3 +688,67 @@ TEST_CASE("gl texture") } } } + +TEST_CASE("gl texture GPU compression quality") +{ + constexpr unsigned resolution = 64; + auto detailed = create_test_rgba_raster(resolution, resolution); + auto constant = radix::Raster(glm::uvec2(resolution), glm::u8vec4(42, 142, 242, 255)); + std::vector> sources; + sources.push_back(detailed); + sources.push_back(constant); + + gl_engine::Texture destination(gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); + destination.setParams(gl_engine::Texture::Filter::MipMapLinear, gl_engine::Texture::Filter::Nearest); + destination.allocate_array(resolution, resolution, unsigned(sources.size())); + + gl_engine::TextureCompressor compressor(resolution, resolution, unsigned(sources.size())); + const std::array destination_layers { 0, 1 }; + const auto result = compressor.compress(sources, + destination, + destination_layers, + { .algorithm = gl_engine::Texture::compression_algorithm(), .effort = 4, .generate_mipmaps = true }); + size_t expected_size = 0; + for (unsigned level = 0; level < gl_engine::TextureCompressor::mip_level_count(resolution, resolution); ++level) { + expected_size += gl_engine::TextureCompressor::compressed_level_size( + std::max(1u, resolution >> level), std::max(1u, resolution >> level)); + } + CHECK(result.encoded_bytes == expected_size * sources.size()); + CHECK(result.mip_levels == 7); + CHECK(result.timings.total_ms > 0.0); + + Framebuffer framebuffer(Framebuffer::DepthFormat::None, { Framebuffer::ColourFormat::RGBA8 }, { resolution, resolution }); + framebuffer.bind(); + ShaderProgram shader = create_debug_shader(R"( + uniform lowp sampler2DArray texture_sampler; + uniform highp int texture_layer; + uniform highp int mip_level; + in highp vec2 texcoords; + out lowp vec4 out_color; + void main() { + out_color = textureLod(texture_sampler, vec3(texcoords.x, 1.0 - texcoords.y, float(texture_layer)), float(mip_level)); + } + )"); + shader.bind(); + destination.bind(0); + shader.set_uniform("texture_sampler", 0); + shader.set_uniform("mip_level", 0); + for (int layer = 0; layer < int(sources.size()); ++layer) { + shader.set_uniform("texture_layer", layer); + gl_engine::helpers::create_screen_quad_geometry().draw(); + const auto reconstructed = framebuffer.read_colour_attachment(0); + const auto psnr = linear_psnr(reconstructed, sources[size_t(layer)]); + CAPTURE(layer, psnr); + CHECK(psnr > 12.0); + } + shader.set_uniform("texture_layer", 1); + for (int level = 1; level < int(result.mip_levels); ++level) { + shader.set_uniform("mip_level", level); + gl_engine::helpers::create_screen_quad_geometry().draw(); + const auto reconstructed = framebuffer.read_colour_attachment(0); + const auto psnr = linear_psnr(reconstructed, constant); + CAPTURE(level, psnr); + CHECK(psnr > 20.0); + } + Framebuffer::unbind(); +} From 97dc1d78db1020e90458d9a2a9275be220f20ea0 Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:19:26 +0200 Subject: [PATCH 03/38] Fix WebGL texture compression shader --- gl_engine/shaders/texture_compress.vert | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gl_engine/shaders/texture_compress.vert b/gl_engine/shaders/texture_compress.vert index dc096cff..0ce63218 100644 --- a/gl_engine/shaders/texture_compress.vert +++ b/gl_engine/shaders/texture_compress.vert @@ -23,7 +23,7 @@ highp uint pack_565(highp uvec3 value) highp uint colour_error(highp uvec3 lhs, highp uvec3 rhs) { highp ivec3 delta = ivec3(lhs) - ivec3(rhs); - return uint(dot(delta, delta)); + return uint(delta.x * delta.x + delta.y * delta.y + delta.z * delta.z); } highp uvec2 encode_dxt1(highp uvec3 pixels[16]) @@ -136,7 +136,7 @@ highp uvec2 encode_etc1(highp uvec3 pixels[16]) for (int index = 0; index < 4; ++index) { highp ivec3 reconstructed = clamp(decoded_base + ivec3(modifier(table, index)), ivec3(0), ivec3(255)); highp ivec3 delta = ivec3(pixels[pixel_index]) - reconstructed; - highp uint error = uint(dot(delta, delta)); + highp uint error = uint(delta.x * delta.x + delta.y * delta.y + delta.z * delta.z); if (error < selected_error) { selected = uint(index); selected_error = error; From c7f1dd56b6c12db48563afa353cdfbc7612f42e7 Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Mon, 17 Aug 2026 22:47:08 +0200 Subject: [PATCH 04/38] Report texture compression GL errors --- .../BenchmarkItem.cpp | 100 ++++++++++++++++-- apps/texture_compression_benchmark/Main.qml | 2 +- gl_engine/Texture.cpp | 18 +++- gl_engine/Texture.h | 8 ++ 4 files changed, 120 insertions(+), 8 deletions(-) diff --git a/apps/texture_compression_benchmark/BenchmarkItem.cpp b/apps/texture_compression_benchmark/BenchmarkItem.cpp index 43419f7a..6e4c9273 100644 --- a/apps/texture_compression_benchmark/BenchmarkItem.cpp +++ b/apps/texture_compression_benchmark/BenchmarkItem.cpp @@ -45,6 +45,12 @@ struct Statistics { double maximum = 0.0; }; +struct ObservedGlError { + QString stage; + int iteration = -1; + GLenum code = GL_NO_ERROR; +}; + Statistics statistics(std::vector values) { Q_ASSERT(!values.empty()); @@ -64,6 +70,74 @@ QJsonObject to_json(const Statistics& value) { QStringLiteral("max_ms"), value.maximum } }; } +QString gl_error_name(GLenum error) +{ + switch (error) { + case GL_INVALID_ENUM: + return QStringLiteral("GL_INVALID_ENUM"); + case GL_INVALID_VALUE: + return QStringLiteral("GL_INVALID_VALUE"); + case GL_INVALID_OPERATION: + return QStringLiteral("GL_INVALID_OPERATION"); + case GL_INVALID_FRAMEBUFFER_OPERATION: + return QStringLiteral("GL_INVALID_FRAMEBUFFER_OPERATION"); + case GL_OUT_OF_MEMORY: + return QStringLiteral("GL_OUT_OF_MEMORY"); + case 0x0507: + return QStringLiteral("GL_CONTEXT_LOST"); + default: + return QStringLiteral("UNKNOWN_GL_ERROR"); + } +} + +QJsonArray to_json(const std::vector& errors) +{ + QJsonArray result; + for (const auto& error : errors) { + QJsonObject object { + { QStringLiteral("stage"), error.stage }, + { QStringLiteral("name"), gl_error_name(error.code) }, + { QStringLiteral("code"), int(error.code) }, + { QStringLiteral("code_hex"), QStringLiteral("0x%1").arg(error.code, 4, 16, QLatin1Char('0')) }, + }; + object.insert(QStringLiteral("iteration"), error.iteration < 0 ? QJsonValue(QJsonValue::Null) : QJsonValue(error.iteration)); + result.append(object); + } + return result; +} + +QString compressor_stage_name(gl_engine::TextureCompressor::Stage stage) +{ + using Stage = gl_engine::TextureCompressor::Stage; + switch (stage) { + case Stage::ScratchUpload: + return QStringLiteral("gpu_scratch_upload"); + case Stage::MipmapGeneration: + return QStringLiteral("gpu_mipmap_generation"); + case Stage::Encoding: + return QStringLiteral("gpu_encoding"); + case Stage::CompressedUpload: + return QStringLiteral("gpu_compressed_upload"); + } + return QStringLiteral("gpu_unknown"); +} + +void collect_gl_errors(std::vector& errors, QString stage, int iteration = -1) +{ + auto* f = QOpenGLContext::currentContext()->extraFunctions(); + while (const auto error = f->glGetError()) + errors.push_back({ stage, iteration, error }); +} + +void collect_gl_errors(std::vector& errors, + const gl_engine::TextureCompressor::Result& result, + QString stage_prefix, + int iteration = -1) +{ + for (const auto& error : result.gl_errors) + errors.push_back({ stage_prefix + compressor_stage_name(error.stage), iteration, error.code }); +} + double elapsed_ms(Clock::time_point start) { return std::chrono::duration(Clock::now() - start).count(); @@ -211,14 +285,16 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { { QStringLiteral("vendor"), gl_string(GL_VENDOR) }, { QStringLiteral("version"), gl_string(GL_VERSION) }, { QStringLiteral("supported"), false }, - { QStringLiteral("error"), QStringLiteral("Compressed texture arrays require WEBGL_compressed_texture_etc") }, + { QStringLiteral("error"), QStringLiteral("No supported WebGL compressed texture format") }, }; const auto json = QString::fromUtf8(QJsonDocument(root).toJson(QJsonDocument::Indented)); qInfo().noquote() << json; - return { QStringLiteral("GPU compression is unavailable: this WebGL device does not expose WEBGL_compressed_texture_etc."), json }; + return { QStringLiteral("GPU compression is unavailable: this WebGL device exposes neither ETC nor sRGB S3TC."), json }; } const auto algorithm = gl_engine::Texture::compression_algorithm(); + while (QOpenGLContext::currentContext()->extraFunctions()->glGetError() != GL_NO_ERROR) { } + std::vector gl_errors; const auto filter = m_mipmaps ? gl_engine::Texture::Filter::MipMapLinear : gl_engine::Texture::Filter::Linear; gl_engine::Texture cpu_destination(gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); cpu_destination.setParams(filter, gl_engine::Texture::Filter::Linear); @@ -227,6 +303,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { gpu_destination.setParams(filter, gl_engine::Texture::Filter::Linear); gpu_destination.allocate_array(resolution, resolution, unsigned(m_batch_size)); gl_engine::TextureCompressor gpu_compressor(resolution, resolution, unsigned(m_batch_size)); + collect_gl_errors(gl_errors, QStringLiteral("setup")); auto upload_cpu = [&](const std::vector& compressed) { for (size_t layer = 0; layer < compressed.size(); ++layer) @@ -236,10 +313,12 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { auto warmup_cpu = cpu_compress(sources, algorithm, m_mipmaps); upload_cpu(warmup_cpu); - static_cast(gpu_compressor.compress(sources, + collect_gl_errors(gl_errors, QStringLiteral("warmup_cpu_upload")); + const auto warmup_gpu = gpu_compressor.compress(sources, gpu_destination, layers, - { .algorithm = algorithm, .effort = unsigned(m_effort), .generate_mipmaps = m_mipmaps })); + { .algorithm = algorithm, .effort = unsigned(m_effort), .generate_mipmaps = m_mipmaps }); + collect_gl_errors(gl_errors, warmup_gpu, QStringLiteral("warmup_")); std::vector cpu_compression_times; std::vector cpu_total_times; @@ -257,6 +336,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { auto compressed = cpu_compress(sources, algorithm, m_mipmaps); const auto cpu_compression_time = elapsed_ms(cpu_start); upload_cpu(compressed); + collect_gl_errors(gl_errors, QStringLiteral("cpu_upload"), iteration); cpu_compression_times.push_back(cpu_compression_time); cpu_total_times.push_back(elapsed_ms(cpu_start)); @@ -264,6 +344,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { gpu_destination, layers, { .algorithm = algorithm, .effort = unsigned(m_effort), .generate_mipmaps = m_mipmaps }); + collect_gl_errors(gl_errors, gpu, QString(), iteration); gpu_upload_times.push_back(gpu.timings.scratch_upload_ms); gpu_mipmap_times.push_back(gpu.timings.mipmap_generation_ms); gpu_encoding_times.push_back(gpu.timings.encoding_ms); @@ -278,8 +359,12 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { const auto gpu_encoding = statistics(gpu_encoding_times); const auto gpu_compressed_upload = statistics(gpu_compressed_upload_times); const auto gpu_total = statistics(gpu_total_times); - const auto cpu_psnr = linear_psnr(reconstruct(cpu_destination, resolution), source); - const auto gpu_psnr = linear_psnr(reconstruct(gpu_destination, resolution), source); + const auto cpu_reconstructed = reconstruct(cpu_destination, resolution); + collect_gl_errors(gl_errors, QStringLiteral("cpu_reconstruction")); + const auto cpu_psnr = linear_psnr(cpu_reconstructed, source); + const auto gpu_reconstructed = reconstruct(gpu_destination, resolution); + collect_gl_errors(gl_errors, QStringLiteral("gpu_reconstruction")); + const auto gpu_psnr = linear_psnr(gpu_reconstructed, source); const auto algorithm_name = algorithm == nucleus::utils::ColourTexture::Format::DXT1 ? QStringLiteral("DXT1 / BC1") : QStringLiteral("ETC1 in ETC2"); QJsonObject root { @@ -303,6 +388,8 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { { QStringLiteral("gpu_end_to_end"), to_json(gpu_total) }, { QStringLiteral("cpu_psnr_db"), cpu_psnr }, { QStringLiteral("gpu_psnr_db"), gpu_psnr }, + { QStringLiteral("gl_error_count"), int(gl_errors.size()) }, + { QStringLiteral("gl_errors"), to_json(gl_errors) }, { QStringLiteral("cpu_tiles_per_second"), 1000.0 * m_batch_size / cpu_total.median }, { QStringLiteral("gpu_tiles_per_second"), 1000.0 * m_batch_size / gpu_total.median }, }; @@ -333,6 +420,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { QStringLiteral("GPU completed throughput %1 tiles/s").arg(1000.0 * m_batch_size / gpu_total.median, 0, 'f', 1), QStringLiteral("CPU PSNR %1 dB").arg(cpu_psnr, 0, 'f', 2), QStringLiteral("GPU PSNR %1 dB").arg(gpu_psnr, 0, 'f', 2), + QStringLiteral("GL errors %1").arg(gl_errors.size()), }; qInfo().noquote() << json; return { summary.join('\n'), json }; diff --git a/apps/texture_compression_benchmark/Main.qml b/apps/texture_compression_benchmark/Main.qml index 956d9146..edbe882c 100644 --- a/apps/texture_compression_benchmark/Main.qml +++ b/apps/texture_compression_benchmark/Main.qml @@ -47,7 +47,7 @@ ApplicationWindow { Layout.fillWidth: true Layout.leftMargin: 20 Layout.rightMargin: 20 - text: qsTr("Measures DXT1 or ETC1 compression of 512×512 ortho textures, including optional mipmaps and final texture-array upload. WebGL requires WEBGL_compressed_texture_etc.") + text: qsTr("Measures DXT1 or ETC1 compression of 512×512 ortho textures, including optional mipmaps and final texture-array upload. WebGL requires ETC or sRGB S3TC compressed textures.") wrapMode: Text.Wrap } diff --git a/gl_engine/Texture.cpp b/gl_engine/Texture.cpp index 1a49e0dd..71d9b921 100644 --- a/gl_engine/Texture.cpp +++ b/gl_engine/Texture.cpp @@ -392,6 +392,13 @@ template double measure_finished_gl(Callable&& callable) const auto end = std::chrono::steady_clock::now(); return std::chrono::duration(end - start).count(); } + +void collect_gl_errors(std::vector& errors, gl_engine::TextureCompressor::Stage stage) +{ + auto* f = QOpenGLContext::currentContext()->extraFunctions(); + while (const auto error = f->glGetError()) + errors.push_back({ stage, error }); +} } struct gl_engine::TextureCompressor::Impl { @@ -495,7 +502,12 @@ bool gl_engine::TextureCompressor::is_supported() { #if defined(__EMSCRIPTEN__) const auto context = emscripten_webgl_get_current_context(); - return context && emscripten_webgl_enable_extension(context, "WEBGL_compressed_texture_etc"); + if (!context) + return false; + if (emscripten_webgl_enable_extension(context, "WEBGL_compressed_texture_etc")) + return true; + return emscripten_webgl_enable_extension(context, "WEBGL_compressed_texture_s3tc") + && emscripten_webgl_enable_extension(context, "WEBGL_compressed_texture_s3tc_srgb"); #else return true; #endif @@ -542,12 +554,14 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: textures[layer].bytes().data()); } }); + collect_gl_errors(result.gl_errors, Stage::ScratchUpload); if (settings.generate_mipmaps) { result.timings.mipmap_generation_ms = measure_finished_gl([&]() { f->glBindTexture(GL_TEXTURE_2D_ARRAY, m->scratch_texture); f->glGenerateMipmap(GL_TEXTURE_2D_ARRAY); }); + collect_gl_errors(result.gl_errors, Stage::MipmapGeneration); } std::vector level_offsets; @@ -598,6 +612,7 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: f->glBindVertexArray(0); program->release(); }); + collect_gl_errors(result.gl_errors, Stage::Encoding); result.timings.compressed_upload_ms = measure_finished_gl([&]() { f->glBindTexture(GL_TEXTURE_2D_ARRAY, destination.m_id); @@ -624,6 +639,7 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: } f->glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); }); + collect_gl_errors(result.gl_errors, Stage::CompressedUpload); f->glActiveTexture(GL_TEXTURE0); result.timings.total_ms = result.timings.scratch_upload_ms + result.timings.mipmap_generation_ms + result.timings.encoding_ms diff --git a/gl_engine/Texture.h b/gl_engine/Texture.h index af8b608e..1c9491b9 100644 --- a/gl_engine/Texture.h +++ b/gl_engine/Texture.h @@ -91,6 +91,8 @@ class Texture { class TextureCompressor { public: + enum class Stage { ScratchUpload, MipmapGeneration, Encoding, CompressedUpload }; + struct Settings { nucleus::utils::ColourTexture::Format algorithm = nucleus::utils::ColourTexture::Format::DXT1; unsigned effort = 0; @@ -106,7 +108,13 @@ class TextureCompressor { }; struct Result { + struct GlError { + Stage stage; + GLenum code = GL_NO_ERROR; + }; + Timings timings; + std::vector gl_errors; size_t encoded_bytes = 0; unsigned mip_levels = 0; }; From d9631536cfc83dfc9fb073bfa98794a1a6dea4ac Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Tue, 18 Aug 2026 10:03:34 +0200 Subject: [PATCH 05/38] Remove texture compression GL diagnostics --- .../BenchmarkItem.cpp | 97 +------------------ gl_engine/Texture.cpp | 13 --- gl_engine/Texture.h | 8 -- 3 files changed, 4 insertions(+), 114 deletions(-) diff --git a/apps/texture_compression_benchmark/BenchmarkItem.cpp b/apps/texture_compression_benchmark/BenchmarkItem.cpp index 6e4c9273..ef38db5e 100644 --- a/apps/texture_compression_benchmark/BenchmarkItem.cpp +++ b/apps/texture_compression_benchmark/BenchmarkItem.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -45,12 +44,6 @@ struct Statistics { double maximum = 0.0; }; -struct ObservedGlError { - QString stage; - int iteration = -1; - GLenum code = GL_NO_ERROR; -}; - Statistics statistics(std::vector values) { Q_ASSERT(!values.empty()); @@ -70,74 +63,6 @@ QJsonObject to_json(const Statistics& value) { QStringLiteral("max_ms"), value.maximum } }; } -QString gl_error_name(GLenum error) -{ - switch (error) { - case GL_INVALID_ENUM: - return QStringLiteral("GL_INVALID_ENUM"); - case GL_INVALID_VALUE: - return QStringLiteral("GL_INVALID_VALUE"); - case GL_INVALID_OPERATION: - return QStringLiteral("GL_INVALID_OPERATION"); - case GL_INVALID_FRAMEBUFFER_OPERATION: - return QStringLiteral("GL_INVALID_FRAMEBUFFER_OPERATION"); - case GL_OUT_OF_MEMORY: - return QStringLiteral("GL_OUT_OF_MEMORY"); - case 0x0507: - return QStringLiteral("GL_CONTEXT_LOST"); - default: - return QStringLiteral("UNKNOWN_GL_ERROR"); - } -} - -QJsonArray to_json(const std::vector& errors) -{ - QJsonArray result; - for (const auto& error : errors) { - QJsonObject object { - { QStringLiteral("stage"), error.stage }, - { QStringLiteral("name"), gl_error_name(error.code) }, - { QStringLiteral("code"), int(error.code) }, - { QStringLiteral("code_hex"), QStringLiteral("0x%1").arg(error.code, 4, 16, QLatin1Char('0')) }, - }; - object.insert(QStringLiteral("iteration"), error.iteration < 0 ? QJsonValue(QJsonValue::Null) : QJsonValue(error.iteration)); - result.append(object); - } - return result; -} - -QString compressor_stage_name(gl_engine::TextureCompressor::Stage stage) -{ - using Stage = gl_engine::TextureCompressor::Stage; - switch (stage) { - case Stage::ScratchUpload: - return QStringLiteral("gpu_scratch_upload"); - case Stage::MipmapGeneration: - return QStringLiteral("gpu_mipmap_generation"); - case Stage::Encoding: - return QStringLiteral("gpu_encoding"); - case Stage::CompressedUpload: - return QStringLiteral("gpu_compressed_upload"); - } - return QStringLiteral("gpu_unknown"); -} - -void collect_gl_errors(std::vector& errors, QString stage, int iteration = -1) -{ - auto* f = QOpenGLContext::currentContext()->extraFunctions(); - while (const auto error = f->glGetError()) - errors.push_back({ stage, iteration, error }); -} - -void collect_gl_errors(std::vector& errors, - const gl_engine::TextureCompressor::Result& result, - QString stage_prefix, - int iteration = -1) -{ - for (const auto& error : result.gl_errors) - errors.push_back({ stage_prefix + compressor_stage_name(error.stage), iteration, error.code }); -} - double elapsed_ms(Clock::time_point start) { return std::chrono::duration(Clock::now() - start).count(); @@ -293,8 +218,6 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { } const auto algorithm = gl_engine::Texture::compression_algorithm(); - while (QOpenGLContext::currentContext()->extraFunctions()->glGetError() != GL_NO_ERROR) { } - std::vector gl_errors; const auto filter = m_mipmaps ? gl_engine::Texture::Filter::MipMapLinear : gl_engine::Texture::Filter::Linear; gl_engine::Texture cpu_destination(gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); cpu_destination.setParams(filter, gl_engine::Texture::Filter::Linear); @@ -303,7 +226,6 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { gpu_destination.setParams(filter, gl_engine::Texture::Filter::Linear); gpu_destination.allocate_array(resolution, resolution, unsigned(m_batch_size)); gl_engine::TextureCompressor gpu_compressor(resolution, resolution, unsigned(m_batch_size)); - collect_gl_errors(gl_errors, QStringLiteral("setup")); auto upload_cpu = [&](const std::vector& compressed) { for (size_t layer = 0; layer < compressed.size(); ++layer) @@ -313,12 +235,10 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { auto warmup_cpu = cpu_compress(sources, algorithm, m_mipmaps); upload_cpu(warmup_cpu); - collect_gl_errors(gl_errors, QStringLiteral("warmup_cpu_upload")); - const auto warmup_gpu = gpu_compressor.compress(sources, + static_cast(gpu_compressor.compress(sources, gpu_destination, layers, - { .algorithm = algorithm, .effort = unsigned(m_effort), .generate_mipmaps = m_mipmaps }); - collect_gl_errors(gl_errors, warmup_gpu, QStringLiteral("warmup_")); + { .algorithm = algorithm, .effort = unsigned(m_effort), .generate_mipmaps = m_mipmaps })); std::vector cpu_compression_times; std::vector cpu_total_times; @@ -336,7 +256,6 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { auto compressed = cpu_compress(sources, algorithm, m_mipmaps); const auto cpu_compression_time = elapsed_ms(cpu_start); upload_cpu(compressed); - collect_gl_errors(gl_errors, QStringLiteral("cpu_upload"), iteration); cpu_compression_times.push_back(cpu_compression_time); cpu_total_times.push_back(elapsed_ms(cpu_start)); @@ -344,7 +263,6 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { gpu_destination, layers, { .algorithm = algorithm, .effort = unsigned(m_effort), .generate_mipmaps = m_mipmaps }); - collect_gl_errors(gl_errors, gpu, QString(), iteration); gpu_upload_times.push_back(gpu.timings.scratch_upload_ms); gpu_mipmap_times.push_back(gpu.timings.mipmap_generation_ms); gpu_encoding_times.push_back(gpu.timings.encoding_ms); @@ -359,12 +277,8 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { const auto gpu_encoding = statistics(gpu_encoding_times); const auto gpu_compressed_upload = statistics(gpu_compressed_upload_times); const auto gpu_total = statistics(gpu_total_times); - const auto cpu_reconstructed = reconstruct(cpu_destination, resolution); - collect_gl_errors(gl_errors, QStringLiteral("cpu_reconstruction")); - const auto cpu_psnr = linear_psnr(cpu_reconstructed, source); - const auto gpu_reconstructed = reconstruct(gpu_destination, resolution); - collect_gl_errors(gl_errors, QStringLiteral("gpu_reconstruction")); - const auto gpu_psnr = linear_psnr(gpu_reconstructed, source); + const auto cpu_psnr = linear_psnr(reconstruct(cpu_destination, resolution), source); + const auto gpu_psnr = linear_psnr(reconstruct(gpu_destination, resolution), source); const auto algorithm_name = algorithm == nucleus::utils::ColourTexture::Format::DXT1 ? QStringLiteral("DXT1 / BC1") : QStringLiteral("ETC1 in ETC2"); QJsonObject root { @@ -388,8 +302,6 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { { QStringLiteral("gpu_end_to_end"), to_json(gpu_total) }, { QStringLiteral("cpu_psnr_db"), cpu_psnr }, { QStringLiteral("gpu_psnr_db"), gpu_psnr }, - { QStringLiteral("gl_error_count"), int(gl_errors.size()) }, - { QStringLiteral("gl_errors"), to_json(gl_errors) }, { QStringLiteral("cpu_tiles_per_second"), 1000.0 * m_batch_size / cpu_total.median }, { QStringLiteral("gpu_tiles_per_second"), 1000.0 * m_batch_size / gpu_total.median }, }; @@ -420,7 +332,6 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { QStringLiteral("GPU completed throughput %1 tiles/s").arg(1000.0 * m_batch_size / gpu_total.median, 0, 'f', 1), QStringLiteral("CPU PSNR %1 dB").arg(cpu_psnr, 0, 'f', 2), QStringLiteral("GPU PSNR %1 dB").arg(gpu_psnr, 0, 'f', 2), - QStringLiteral("GL errors %1").arg(gl_errors.size()), }; qInfo().noquote() << json; return { summary.join('\n'), json }; diff --git a/gl_engine/Texture.cpp b/gl_engine/Texture.cpp index 71d9b921..e5132f40 100644 --- a/gl_engine/Texture.cpp +++ b/gl_engine/Texture.cpp @@ -392,13 +392,6 @@ template double measure_finished_gl(Callable&& callable) const auto end = std::chrono::steady_clock::now(); return std::chrono::duration(end - start).count(); } - -void collect_gl_errors(std::vector& errors, gl_engine::TextureCompressor::Stage stage) -{ - auto* f = QOpenGLContext::currentContext()->extraFunctions(); - while (const auto error = f->glGetError()) - errors.push_back({ stage, error }); -} } struct gl_engine::TextureCompressor::Impl { @@ -554,14 +547,11 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: textures[layer].bytes().data()); } }); - collect_gl_errors(result.gl_errors, Stage::ScratchUpload); - if (settings.generate_mipmaps) { result.timings.mipmap_generation_ms = measure_finished_gl([&]() { f->glBindTexture(GL_TEXTURE_2D_ARRAY, m->scratch_texture); f->glGenerateMipmap(GL_TEXTURE_2D_ARRAY); }); - collect_gl_errors(result.gl_errors, Stage::MipmapGeneration); } std::vector level_offsets; @@ -612,7 +602,6 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: f->glBindVertexArray(0); program->release(); }); - collect_gl_errors(result.gl_errors, Stage::Encoding); result.timings.compressed_upload_ms = measure_finished_gl([&]() { f->glBindTexture(GL_TEXTURE_2D_ARRAY, destination.m_id); @@ -639,8 +628,6 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: } f->glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); }); - collect_gl_errors(result.gl_errors, Stage::CompressedUpload); - f->glActiveTexture(GL_TEXTURE0); result.timings.total_ms = result.timings.scratch_upload_ms + result.timings.mipmap_generation_ms + result.timings.encoding_ms + result.timings.compressed_upload_ms; diff --git a/gl_engine/Texture.h b/gl_engine/Texture.h index 1c9491b9..af8b608e 100644 --- a/gl_engine/Texture.h +++ b/gl_engine/Texture.h @@ -91,8 +91,6 @@ class Texture { class TextureCompressor { public: - enum class Stage { ScratchUpload, MipmapGeneration, Encoding, CompressedUpload }; - struct Settings { nucleus::utils::ColourTexture::Format algorithm = nucleus::utils::ColourTexture::Format::DXT1; unsigned effort = 0; @@ -108,13 +106,7 @@ class TextureCompressor { }; struct Result { - struct GlError { - Stage stage; - GLenum code = GL_NO_ERROR; - }; - Timings timings; - std::vector gl_errors; size_t encoded_bytes = 0; unsigned mip_levels = 0; }; From fde70b429351e2487712d1b2be3ece67d5dca9a8 Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Tue, 18 Aug 2026 10:46:05 +0200 Subject: [PATCH 06/38] Add fragment shader texture compression backend --- .../BenchmarkItem.cpp | 34 ++- .../BenchmarkItem.h | 7 + apps/texture_compression_benchmark/Main.qml | 19 ++ gl_engine/CMakeLists.txt | 1 + gl_engine/ShaderProgram.cpp | 8 + gl_engine/ShaderProgram.h | 1 + gl_engine/Texture.cpp | 236 ++++++++++++++---- gl_engine/Texture.h | 5 + gl_engine/shaders/texture_compress.vert | 66 ++++- .../shaders/texture_compress_raster.vert | 5 + 10 files changed, 321 insertions(+), 61 deletions(-) create mode 100644 gl_engine/shaders/texture_compress_raster.vert diff --git a/apps/texture_compression_benchmark/BenchmarkItem.cpp b/apps/texture_compression_benchmark/BenchmarkItem.cpp index ef38db5e..8264ed3f 100644 --- a/apps/texture_compression_benchmark/BenchmarkItem.cpp +++ b/apps/texture_compression_benchmark/BenchmarkItem.cpp @@ -165,6 +165,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { m_batch_size = benchmark_item->m_batch_size; m_iterations = benchmark_item->m_iterations; m_mipmaps = benchmark_item->m_mipmaps; + m_backend = benchmark_item->m_backend; m_pending = true; } @@ -218,6 +219,11 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { } const auto algorithm = gl_engine::Texture::compression_algorithm(); + const auto backend = m_backend == 0 ? gl_engine::TextureCompressor::Backend::FragmentShader + : gl_engine::TextureCompressor::Backend::TransformFeedback; + const auto backend_name = backend == gl_engine::TextureCompressor::Backend::FragmentShader + ? QStringLiteral("Fragment shader + PBO") + : QStringLiteral("Transform feedback"); const auto filter = m_mipmaps ? gl_engine::Texture::Filter::MipMapLinear : gl_engine::Texture::Filter::Linear; gl_engine::Texture cpu_destination(gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); cpu_destination.setParams(filter, gl_engine::Texture::Filter::Linear); @@ -238,13 +244,14 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { static_cast(gpu_compressor.compress(sources, gpu_destination, layers, - { .algorithm = algorithm, .effort = unsigned(m_effort), .generate_mipmaps = m_mipmaps })); + { .algorithm = algorithm, .effort = unsigned(m_effort), .generate_mipmaps = m_mipmaps, .backend = backend })); std::vector cpu_compression_times; std::vector cpu_total_times; std::vector gpu_upload_times; std::vector gpu_mipmap_times; std::vector gpu_encoding_times; + std::vector gpu_output_transfer_times; std::vector gpu_compressed_upload_times; std::vector gpu_total_times; cpu_compression_times.reserve(size_t(m_iterations)); @@ -262,10 +269,11 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { const auto gpu = gpu_compressor.compress(sources, gpu_destination, layers, - { .algorithm = algorithm, .effort = unsigned(m_effort), .generate_mipmaps = m_mipmaps }); + { .algorithm = algorithm, .effort = unsigned(m_effort), .generate_mipmaps = m_mipmaps, .backend = backend }); gpu_upload_times.push_back(gpu.timings.scratch_upload_ms); gpu_mipmap_times.push_back(gpu.timings.mipmap_generation_ms); gpu_encoding_times.push_back(gpu.timings.encoding_ms); + gpu_output_transfer_times.push_back(gpu.timings.output_transfer_ms); gpu_compressed_upload_times.push_back(gpu.timings.compressed_upload_ms); gpu_total_times.push_back(gpu.timings.total_ms); } @@ -275,6 +283,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { const auto gpu_upload = statistics(gpu_upload_times); const auto gpu_mipmap = statistics(gpu_mipmap_times); const auto gpu_encoding = statistics(gpu_encoding_times); + const auto gpu_output_transfer = statistics(gpu_output_transfer_times); const auto gpu_compressed_upload = statistics(gpu_compressed_upload_times); const auto gpu_total = statistics(gpu_total_times); const auto cpu_psnr = linear_psnr(reconstruct(cpu_destination, resolution), source); @@ -287,6 +296,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { { QStringLiteral("version"), gl_string(GL_VERSION) }, { QStringLiteral("supported"), true }, { QStringLiteral("algorithm"), algorithm_name }, + { QStringLiteral("gpu_backend"), backend_name }, { QStringLiteral("timing_method"), QStringLiteral("glFinish-synchronised wall time") }, { QStringLiteral("resolution"), int(resolution) }, { QStringLiteral("batch_size"), m_batch_size }, @@ -298,6 +308,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { { QStringLiteral("gpu_scratch_upload"), to_json(gpu_upload) }, { QStringLiteral("gpu_mipmap_generation"), to_json(gpu_mipmap) }, { QStringLiteral("gpu_encoding"), to_json(gpu_encoding) }, + { QStringLiteral("gpu_output_transfer"), to_json(gpu_output_transfer) }, { QStringLiteral("gpu_compressed_upload"), to_json(gpu_compressed_upload) }, { QStringLiteral("gpu_end_to_end"), to_json(gpu_total) }, { QStringLiteral("cpu_psnr_db"), cpu_psnr }, @@ -317,6 +328,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { .arg(m_batch_size) .arg(m_effort) .arg(m_mipmaps ? QStringLiteral("on") : QStringLiteral("off")), + backend_name, gl_string(GL_RENDERER), QStringLiteral("Timing: completion-synchronised wall time"), QString(), @@ -325,6 +337,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { line(QStringLiteral("GPU scratch upload"), gpu_upload), line(QStringLiteral("GPU mip generation"), gpu_mipmap), line(QStringLiteral("GPU encoding"), gpu_encoding), + line(QStringLiteral("GPU output transfer"), gpu_output_transfer), line(QStringLiteral("GPU compressed upload"), gpu_compressed_upload), line(QStringLiteral("GPU end-to-end"), gpu_total), QString(), @@ -344,6 +357,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { int m_batch_size = 4; int m_iterations = 7; bool m_mipmaps = true; + int m_backend = 0; bool m_pending = false; }; @@ -394,6 +408,22 @@ void BenchmarkItem::setMipmaps(bool value) emit mipmapsChanged(); } +int BenchmarkItem::backend() const { return m_backend; } +void BenchmarkItem::setBackend(int value) +{ + if (value < 0 || value > 1 || (value == 1 && !transformFeedbackSupported())) + return; + if (m_backend == value) + return; + m_backend = value; + emit backendChanged(); +} + +bool BenchmarkItem::transformFeedbackSupported() const +{ + return gl_engine::TextureCompressor::is_backend_supported(gl_engine::TextureCompressor::Backend::TransformFeedback); +} + bool BenchmarkItem::running() const { return m_running; } QString BenchmarkItem::resultText() const { return m_result_text; } QString BenchmarkItem::resultJson() const { return m_result_json; } diff --git a/apps/texture_compression_benchmark/BenchmarkItem.h b/apps/texture_compression_benchmark/BenchmarkItem.h index 88ebd25a..84d17844 100644 --- a/apps/texture_compression_benchmark/BenchmarkItem.h +++ b/apps/texture_compression_benchmark/BenchmarkItem.h @@ -17,6 +17,8 @@ class BenchmarkItem : public QQuickFramebufferObject { Q_PROPERTY(int batchSize READ batchSize WRITE setBatchSize NOTIFY batchSizeChanged) Q_PROPERTY(int iterations READ iterations WRITE setIterations NOTIFY iterationsChanged) Q_PROPERTY(bool mipmaps READ mipmaps WRITE setMipmaps NOTIFY mipmapsChanged) + Q_PROPERTY(int backend READ backend WRITE setBackend NOTIFY backendChanged) + Q_PROPERTY(bool transformFeedbackSupported READ transformFeedbackSupported CONSTANT) Q_PROPERTY(bool running READ running NOTIFY runningChanged) Q_PROPERTY(QString resultText READ resultText NOTIFY resultTextChanged) Q_PROPERTY(QString resultJson READ resultJson NOTIFY resultJsonChanged) @@ -33,6 +35,9 @@ class BenchmarkItem : public QQuickFramebufferObject { void setIterations(int value); [[nodiscard]] bool mipmaps() const; void setMipmaps(bool value); + [[nodiscard]] int backend() const; + void setBackend(int value); + [[nodiscard]] bool transformFeedbackSupported() const; [[nodiscard]] bool running() const; [[nodiscard]] QString resultText() const; [[nodiscard]] QString resultJson() const; @@ -45,6 +50,7 @@ class BenchmarkItem : public QQuickFramebufferObject { void batchSizeChanged(); void iterationsChanged(); void mipmapsChanged(); + void backendChanged(); void runningChanged(); void resultTextChanged(); void resultJsonChanged(); @@ -57,6 +63,7 @@ class BenchmarkItem : public QQuickFramebufferObject { int m_batch_size = 4; int m_iterations = 7; bool m_mipmaps = true; + int m_backend = 0; bool m_running = false; unsigned m_request_serial = 0; QString m_result_text = QStringLiteral("Run the benchmark to collect results."); diff --git a/apps/texture_compression_benchmark/Main.qml b/apps/texture_compression_benchmark/Main.qml index edbe882c..dd42555f 100644 --- a/apps/texture_compression_benchmark/Main.qml +++ b/apps/texture_compression_benchmark/Main.qml @@ -95,6 +95,25 @@ ApplicationWindow { } } + RowLayout { + Layout.fillWidth: true + + Label { + Layout.fillWidth: true + text: qsTr("GPU encoder") + } + + ComboBox { + Layout.preferredWidth: 220 + model: benchmark.transformFeedbackSupported + ? [qsTr("Fragment shader + PBO"), qsTr("Transform feedback")] + : [qsTr("Fragment shader + PBO")] + currentIndex: benchmark.backend + enabled: !benchmark.running && benchmark.transformFeedbackSupported + onActivated: benchmark.backend = currentIndex + } + } + RowLayout { Layout.fillWidth: true diff --git a/gl_engine/CMakeLists.txt b/gl_engine/CMakeLists.txt index 09712cce..c4dd5194 100644 --- a/gl_engine/CMakeLists.txt +++ b/gl_engine/CMakeLists.txt @@ -88,6 +88,7 @@ qt_add_resources(gl_engine "shaders" shaders/track.frag shaders/track.vert shaders/texture_compress.frag + shaders/texture_compress_raster.vert shaders/texture_compress.vert shaders/turbo_colormap.glsl shaders/intersection.glsl diff --git a/gl_engine/ShaderProgram.cpp b/gl_engine/ShaderProgram.cpp index 49c17abb..059b5cbb 100644 --- a/gl_engine/ShaderProgram.cpp +++ b/gl_engine/ShaderProgram.cpp @@ -278,6 +278,14 @@ void ShaderProgram::set_uniform_array(const std::string& name, const std::vector m_q_shader_program->setUniformValueArray(uniform_location, reinterpret_cast(array.data()), int(array.size()), 3); } +void ShaderProgram::set_uniform_array(const std::string& name, const std::vector& array) +{ + if (!m_cached_uniforms.contains(name)) + m_cached_uniforms[name] = m_q_shader_program->uniformLocation(name.c_str()); + + QOpenGLContext::currentContext()->extraFunctions()->glUniform1iv(m_cached_uniforms.at(name), GLsizei(array.size()), array.data()); +} + // Helper function because i get frustrated with the shader compile errors... // I want the actual line that an error relates to also outputed... void outputMeaningfullErrors(const QString& qtLog, const QString& code, const QString& file) diff --git a/gl_engine/ShaderProgram.h b/gl_engine/ShaderProgram.h index 9f4a5260..cdd7cbdb 100644 --- a/gl_engine/ShaderProgram.h +++ b/gl_engine/ShaderProgram.h @@ -107,6 +107,7 @@ class ShaderProgram { void set_uniform_array(const std::string& name, const std::vector& array); void set_uniform_array(const std::string& name, const std::vector& array); + void set_uniform_array(const std::string& name, const std::vector& array); static void reset_shader_cache(); diff --git a/gl_engine/Texture.cpp b/gl_engine/Texture.cpp index e5132f40..abd9b6e3 100644 --- a/gl_engine/Texture.cpp +++ b/gl_engine/Texture.cpp @@ -395,16 +395,24 @@ template double measure_finished_gl(Callable&& callable) } struct gl_engine::TextureCompressor::Impl { + static constexpr unsigned max_shader_mip_levels = 16; + unsigned width = 0; unsigned height = 0; unsigned max_batch_size = 0; unsigned scratch_layers = 0; + GLsizei atlas_width = 0; + GLsizei atlas_height = 0; GLuint scratch_texture = 0; GLuint encoded_buffer = 0; GLuint vertex_array = 0; GLuint transform_feedback = 0; - std::unique_ptr dxt1_program; - std::unique_ptr etc1_program; + GLuint encoding_framebuffer = 0; + GLuint encoding_renderbuffer = 0; + std::unique_ptr dxt1_transform_program; + std::unique_ptr etc1_transform_program; + std::unique_ptr dxt1_fragment_program; + std::unique_ptr etc1_fragment_program; Impl(unsigned texture_width, unsigned texture_height, unsigned maximum_batch_size) : width(texture_width) @@ -412,38 +420,82 @@ struct gl_engine::TextureCompressor::Impl { , max_batch_size(maximum_batch_size) { Q_ASSERT(width > 0 && height > 0 && max_batch_size > 0); + Q_ASSERT(TextureCompressor::mip_level_count(width, height) <= max_shader_mip_levels); auto* f = QOpenGLContext::currentContext()->extraFunctions(); - f->glGenBuffers(1, &encoded_buffer); - f->glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, encoded_buffer); size_t maximum_size = 0; for (unsigned level = 0; level < TextureCompressor::mip_level_count(width, height); ++level) { maximum_size += TextureCompressor::compressed_level_size( std::max(1u, width >> level), std::max(1u, height >> level)); } maximum_size *= max_batch_size; - f->glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, GLsizeiptr(maximum_size), nullptr, GL_STREAM_DRAW); - f->glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0); + + GLint maximum_renderbuffer_size = 0; + f->glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &maximum_renderbuffer_size); + const auto maximum_pixels = maximum_size / 4; + atlas_width = GLsizei(std::min(maximum_pixels, size_t(maximum_renderbuffer_size))); + atlas_height = GLsizei((maximum_pixels + size_t(atlas_width) - 1) / size_t(atlas_width)); + Q_ASSERT(atlas_width > 0 && atlas_height > 0 && atlas_height <= maximum_renderbuffer_size); + + f->glGenBuffers(1, &encoded_buffer); + f->glBindBuffer(GL_PIXEL_PACK_BUFFER, encoded_buffer); + f->glBufferData(GL_PIXEL_PACK_BUFFER, GLsizeiptr(size_t(atlas_width) * size_t(atlas_height) * 4), nullptr, GL_STREAM_DRAW); + f->glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); f->glGenVertexArrays(1, &vertex_array); - f->glGenTransformFeedbacks(1, &transform_feedback); + GLint previous_draw_framebuffer = 0; + GLint previous_read_framebuffer = 0; + GLint previous_renderbuffer = 0; + f->glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previous_draw_framebuffer); + f->glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &previous_read_framebuffer); + f->glGetIntegerv(GL_RENDERBUFFER_BINDING, &previous_renderbuffer); + f->glGenFramebuffers(1, &encoding_framebuffer); + f->glGenRenderbuffers(1, &encoding_renderbuffer); + f->glBindRenderbuffer(GL_RENDERBUFFER, encoding_renderbuffer); + // RGBA8UI with RGBA_INTEGER/UNSIGNED_BYTE is the portable WebGL 2 integer readback path. + // Two pixels hold the two 32-bit words of each compressed block. + f->glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8UI, atlas_width, atlas_height); + f->glBindFramebuffer(GL_FRAMEBUFFER, encoding_framebuffer); + f->glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, encoding_renderbuffer); + Q_ASSERT(f->glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE); + f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, GLuint(previous_draw_framebuffer)); + f->glBindFramebuffer(GL_READ_FRAMEBUFFER, GLuint(previous_read_framebuffer)); + f->glBindRenderbuffer(GL_RENDERBUFFER, GLuint(previous_renderbuffer)); + + dxt1_fragment_program = std::make_unique("texture_compress_raster.vert", + "texture_compress.vert", + ShaderCodeSource::FILE, + std::vector { QStringLiteral("#define ALP_FRAGMENT_COMPRESSION") }); + etc1_fragment_program = std::make_unique("texture_compress_raster.vert", + "texture_compress.vert", + ShaderCodeSource::FILE, + std::vector { QStringLiteral("#define ALP_FRAGMENT_COMPRESSION"), QStringLiteral("#define ALP_COMPRESS_ETC1") }); + +#if !defined(__EMSCRIPTEN__) + f->glGenTransformFeedbacks(1, &transform_feedback); const std::vector varyings { QByteArrayLiteral("encoded_block") }; - dxt1_program = std::make_unique( + dxt1_transform_program = std::make_unique( "texture_compress.vert", "texture_compress.frag", ShaderCodeSource::FILE, std::vector {}, varyings); - etc1_program = std::make_unique("texture_compress.vert", + etc1_transform_program = std::make_unique("texture_compress.vert", "texture_compress.frag", ShaderCodeSource::FILE, std::vector { QStringLiteral("#define ALP_COMPRESS_ETC1") }, varyings); +#endif } ~Impl() { - dxt1_program.reset(); - etc1_program.reset(); + dxt1_transform_program.reset(); + etc1_transform_program.reset(); + dxt1_fragment_program.reset(); + etc1_fragment_program.reset(); if (!QOpenGLContext::currentContext()) return; auto* f = QOpenGLContext::currentContext()->extraFunctions(); - f->glDeleteTransformFeedbacks(1, &transform_feedback); + if (transform_feedback) + f->glDeleteTransformFeedbacks(1, &transform_feedback); + f->glDeleteFramebuffers(1, &encoding_framebuffer); + f->glDeleteRenderbuffers(1, &encoding_renderbuffer); f->glDeleteVertexArrays(1, &vertex_array); f->glDeleteBuffers(1, &encoded_buffer); if (scratch_texture) @@ -506,6 +558,15 @@ bool gl_engine::TextureCompressor::is_supported() #endif } +bool gl_engine::TextureCompressor::is_backend_supported(Backend backend) +{ +#if defined(__EMSCRIPTEN__) + return backend == Backend::FragmentShader; +#else + return backend == Backend::FragmentShader || backend == Backend::TransformFeedback; +#endif +} + gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std::span> textures, Texture& destination, std::span destination_layers, @@ -520,6 +581,7 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: Q_ASSERT(destination.m_width == m->width && destination.m_height == m->height); Q_ASSERT(settings.algorithm == Texture::compression_algorithm()); Q_ASSERT(settings.effort <= 10); + Q_ASSERT(is_backend_supported(settings.backend)); for (size_t i = 0; i < textures.size(); ++i) { Q_ASSERT(unsigned(textures[i].width()) == m->width && unsigned(textures[i].height()) == m->height); Q_ASSERT(destination_layers[i] < destination.m_n_layers); @@ -555,53 +617,131 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: } std::vector level_offsets; + std::vector level_offsets_blocks; + std::vector level_blocks_x; + std::vector level_blocks_y; level_offsets.reserve(result.mip_levels); + level_offsets_blocks.reserve(result.mip_levels); + level_blocks_x.reserve(result.mip_levels); + level_blocks_y.reserve(result.mip_levels); size_t total_encoded_size = 0; for (unsigned level = 0; level < result.mip_levels; ++level) { level_offsets.push_back(total_encoded_size); const auto level_width = std::max(1u, m->width >> level); const auto level_height = std::max(1u, m->height >> level); + level_offsets_blocks.push_back(int(total_encoded_size / 8)); + level_blocks_x.push_back(int(std::max(1u, (level_width + 3) / 4))); + level_blocks_y.push_back(int(std::max(1u, (level_height + 3) / 4))); total_encoded_size += compressed_level_size(level_width, level_height) * textures.size(); } result.encoded_bytes = total_encoded_size; - result.timings.encoding_ms = measure_finished_gl([&]() { - auto* program = settings.algorithm == nucleus::utils::ColourTexture::Format::DXT1 ? m->dxt1_program.get() : m->etc1_program.get(); - program->bind(); - program->set_uniform("source_texture", 7); - program->set_uniform("effort", int(settings.effort)); - f->glActiveTexture(GL_TEXTURE7); - f->glBindTexture(GL_TEXTURE_2D_ARRAY, m->scratch_texture); - f->glBindVertexArray(m->vertex_array); - f->glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, m->transform_feedback); - f->glEnable(GL_RASTERIZER_DISCARD); + if (settings.backend == Backend::TransformFeedback) { + result.timings.encoding_ms = measure_finished_gl([&]() { + auto* program = settings.algorithm == nucleus::utils::ColourTexture::Format::DXT1 ? m->dxt1_transform_program.get() : m->etc1_transform_program.get(); + program->bind(); + program->set_uniform("source_texture", 7); + program->set_uniform("effort", int(settings.effort)); + f->glActiveTexture(GL_TEXTURE7); + f->glBindTexture(GL_TEXTURE_2D_ARRAY, m->scratch_texture); + f->glBindVertexArray(m->vertex_array); + f->glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, m->transform_feedback); + f->glEnable(GL_RASTERIZER_DISCARD); + + for (unsigned level = 0; level < result.mip_levels; ++level) { + const auto level_width = std::max(1u, m->width >> level); + const auto level_height = std::max(1u, m->height >> level); + const auto level_size = compressed_level_size(level_width, level_height) * textures.size(); + program->set_uniform("texture_width", int(level_width)); + program->set_uniform("texture_height", int(level_height)); + program->set_uniform("blocks_x", level_blocks_x[level]); + program->set_uniform("blocks_y", level_blocks_y[level]); + program->set_uniform("mip_level", int(level)); + f->glBindBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, + 0, + m->encoded_buffer, + GLintptr(level_offsets[level]), + GLsizeiptr(level_size)); + f->glBeginTransformFeedback(GL_POINTS); + f->glDrawArrays(GL_POINTS, 0, GLsizei(level_blocks_x[level] * level_blocks_y[level] * int(textures.size()))); + f->glEndTransformFeedback(); + } - for (unsigned level = 0; level < result.mip_levels; ++level) { - const auto level_width = std::max(1u, m->width >> level); - const auto level_height = std::max(1u, m->height >> level); - const auto blocks_x = std::max(1u, (level_width + 3) / 4); - const auto blocks_y = std::max(1u, (level_height + 3) / 4); - const auto level_size = compressed_level_size(level_width, level_height) * textures.size(); - program->set_uniform("texture_width", int(level_width)); - program->set_uniform("texture_height", int(level_height)); - program->set_uniform("blocks_x", int(blocks_x)); - program->set_uniform("blocks_y", int(blocks_y)); - program->set_uniform("mip_level", int(level)); - f->glBindBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, - 0, - m->encoded_buffer, - GLintptr(level_offsets[level]), - GLsizeiptr(level_size)); - f->glBeginTransformFeedback(GL_POINTS); - f->glDrawArrays(GL_POINTS, 0, GLsizei(blocks_x * blocks_y * textures.size())); - f->glEndTransformFeedback(); - } + f->glDisable(GL_RASTERIZER_DISCARD); + f->glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, 0); + f->glBindVertexArray(0); + program->release(); + }); + } else { + result.timings.encoding_ms = measure_finished_gl([&]() { + auto* program = settings.algorithm == nucleus::utils::ColourTexture::Format::DXT1 ? m->dxt1_fragment_program.get() : m->etc1_fragment_program.get(); + GLint previous_draw_framebuffer = 0; + GLint previous_viewport[4] = {}; + GLboolean previous_colour_mask[4] = {}; + f->glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previous_draw_framebuffer); + f->glGetIntegerv(GL_VIEWPORT, previous_viewport); + f->glGetBooleanv(GL_COLOR_WRITEMASK, previous_colour_mask); + const auto blend_enabled = f->glIsEnabled(GL_BLEND); + const auto cull_enabled = f->glIsEnabled(GL_CULL_FACE); + const auto depth_enabled = f->glIsEnabled(GL_DEPTH_TEST); + const auto scissor_enabled = f->glIsEnabled(GL_SCISSOR_TEST); + + f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m->encoding_framebuffer); + f->glViewport(0, 0, m->atlas_width, m->atlas_height); + f->glDisable(GL_BLEND); + f->glDisable(GL_CULL_FACE); + f->glDisable(GL_DEPTH_TEST); + f->glDisable(GL_SCISSOR_TEST); + f->glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + program->bind(); + program->set_uniform("source_texture", 7); + program->set_uniform("texture_width", int(m->width)); + program->set_uniform("texture_height", int(m->height)); + program->set_uniform("effort", int(settings.effort)); + program->set_uniform("atlas_width", int(m->atlas_width)); + program->set_uniform("total_blocks", int(total_encoded_size / 8)); + program->set_uniform("mip_levels", int(result.mip_levels)); + program->set_uniform_array("level_offsets", level_offsets_blocks); + program->set_uniform_array("level_blocks_x", level_blocks_x); + program->set_uniform_array("level_blocks_y", level_blocks_y); + f->glActiveTexture(GL_TEXTURE7); + f->glBindTexture(GL_TEXTURE_2D_ARRAY, m->scratch_texture); + f->glBindVertexArray(m->vertex_array); + f->glDrawArrays(GL_TRIANGLES, 0, 3); + f->glBindVertexArray(0); + program->release(); + + if (blend_enabled) + f->glEnable(GL_BLEND); + if (cull_enabled) + f->glEnable(GL_CULL_FACE); + if (depth_enabled) + f->glEnable(GL_DEPTH_TEST); + if (scissor_enabled) + f->glEnable(GL_SCISSOR_TEST); + f->glColorMask(previous_colour_mask[0], previous_colour_mask[1], previous_colour_mask[2], previous_colour_mask[3]); + f->glViewport(previous_viewport[0], previous_viewport[1], previous_viewport[2], previous_viewport[3]); + f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, GLuint(previous_draw_framebuffer)); + }); - f->glDisable(GL_RASTERIZER_DISCARD); - f->glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, 0); - f->glBindVertexArray(0); - program->release(); - }); + result.timings.output_transfer_ms = measure_finished_gl([&]() { + GLint previous_read_framebuffer = 0; + GLint previous_read_buffer = 0; + GLint previous_pack_alignment = 0; + f->glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &previous_read_framebuffer); + f->glGetIntegerv(GL_READ_BUFFER, &previous_read_buffer); + f->glGetIntegerv(GL_PACK_ALIGNMENT, &previous_pack_alignment); + f->glBindFramebuffer(GL_READ_FRAMEBUFFER, m->encoding_framebuffer); + f->glReadBuffer(GL_COLOR_ATTACHMENT0); + f->glPixelStorei(GL_PACK_ALIGNMENT, 1); + f->glBindBuffer(GL_PIXEL_PACK_BUFFER, m->encoded_buffer); + f->glReadPixels(0, 0, m->atlas_width, m->atlas_height, GL_RGBA_INTEGER, GL_UNSIGNED_BYTE, nullptr); + f->glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + f->glPixelStorei(GL_PACK_ALIGNMENT, previous_pack_alignment); + f->glBindFramebuffer(GL_READ_FRAMEBUFFER, GLuint(previous_read_framebuffer)); + f->glReadBuffer(GLenum(previous_read_buffer)); + }); + } result.timings.compressed_upload_ms = measure_finished_gl([&]() { f->glBindTexture(GL_TEXTURE_2D_ARRAY, destination.m_id); @@ -630,6 +770,6 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: }); f->glActiveTexture(GL_TEXTURE0); result.timings.total_ms = result.timings.scratch_upload_ms + result.timings.mipmap_generation_ms + result.timings.encoding_ms - + result.timings.compressed_upload_ms; + + result.timings.output_transfer_ms + result.timings.compressed_upload_ms; return result; } diff --git a/gl_engine/Texture.h b/gl_engine/Texture.h index af8b608e..8fdf3373 100644 --- a/gl_engine/Texture.h +++ b/gl_engine/Texture.h @@ -91,16 +91,20 @@ class Texture { class TextureCompressor { public: + enum class Backend { FragmentShader, TransformFeedback }; + struct Settings { nucleus::utils::ColourTexture::Format algorithm = nucleus::utils::ColourTexture::Format::DXT1; unsigned effort = 0; bool generate_mipmaps = true; + Backend backend = Backend::FragmentShader; }; struct Timings { double scratch_upload_ms = 0.0; double mipmap_generation_ms = 0.0; double encoding_ms = 0.0; + double output_transfer_ms = 0.0; double compressed_upload_ms = 0.0; double total_ms = 0.0; }; @@ -126,6 +130,7 @@ class TextureCompressor { [[nodiscard]] static size_t compressed_level_size(unsigned width, unsigned height); [[nodiscard]] static unsigned mip_level_count(unsigned width, unsigned height); [[nodiscard]] static bool is_supported(); + [[nodiscard]] static bool is_backend_supported(Backend backend); private: struct Impl; diff --git a/gl_engine/shaders/texture_compress.vert b/gl_engine/shaders/texture_compress.vert index 0ce63218..0caaea20 100644 --- a/gl_engine/shaders/texture_compress.vert +++ b/gl_engine/shaders/texture_compress.vert @@ -1,12 +1,22 @@ uniform highp sampler2DArray source_texture; uniform highp int texture_width; uniform highp int texture_height; +#ifdef ALP_FRAGMENT_COMPRESSION +const highp int max_mip_levels = 16; +uniform highp int atlas_width; +uniform highp int total_blocks; +uniform highp int mip_levels; +uniform highp int level_offsets[max_mip_levels]; +uniform highp int level_blocks_x[max_mip_levels]; +uniform highp int level_blocks_y[max_mip_levels]; +layout(location = 0) out highp uvec4 encoded_pixel; +#else uniform highp int blocks_x; uniform highp int blocks_y; uniform highp int mip_level; -uniform highp int effort; - flat out highp uvec2 encoded_block; +#endif +uniform highp int effort; highp uvec3 unpack_565(highp uint value) { @@ -162,25 +172,59 @@ highp uvec2 encode_etc1(highp uvec3 pixels[16]) return uvec2(header, byte_swap(best_indices)); } -void main() +highp uvec2 compress_block(highp ivec2 block, + highp int layer, + highp int level, + highp int level_width, + highp int level_height) { - highp int blocks_per_layer = blocks_x * blocks_y; - highp int layer = gl_VertexID / blocks_per_layer; - highp int block_index = gl_VertexID - layer * blocks_per_layer; - highp ivec2 block = ivec2(block_index % blocks_x, block_index / blocks_x); highp ivec2 origin = block * 4; highp uvec3 pixels[16]; for (int y = 0; y < 4; ++y) { for (int x = 0; x < 4; ++x) { - highp ivec2 position = min(origin + ivec2(x, y), ivec2(texture_width - 1, texture_height - 1)); - pixels[y * 4 + x] = uvec3(round(texelFetch(source_texture, ivec3(position, layer), mip_level).rgb * 255.0)); + highp ivec2 position = min(origin + ivec2(x, y), ivec2(level_width - 1, level_height - 1)); + pixels[y * 4 + x] = uvec3(round(texelFetch(source_texture, ivec3(position, layer), level).rgb * 255.0)); } } #ifdef ALP_COMPRESS_ETC1 - encoded_block = encode_etc1(pixels); + return encode_etc1(pixels); #else - encoded_block = encode_dxt1(pixels); + return encode_dxt1(pixels); #endif +} + +void main() +{ +#ifdef ALP_FRAGMENT_COMPRESSION + highp int output_pixel = int(gl_FragCoord.y) * atlas_width + int(gl_FragCoord.x); + if (output_pixel >= total_blocks * 2) + discard; + highp int output_index = output_pixel / 2; + + highp int level = 0; + for (int candidate = 1; candidate < max_mip_levels; ++candidate) { + if (candidate >= mip_levels || output_index < level_offsets[candidate]) + break; + level = candidate; + } + + highp int blocks_x_at_level = level_blocks_x[level]; + highp int blocks_y_at_level = level_blocks_y[level]; + highp int blocks_per_layer = blocks_x_at_level * blocks_y_at_level; + highp int level_index = output_index - level_offsets[level]; + highp int layer = level_index / blocks_per_layer; + highp int block_index = level_index - layer * blocks_per_layer; + highp ivec2 block = ivec2(block_index % blocks_x_at_level, block_index / blocks_x_at_level); + highp uvec2 encoded = compress_block(block, layer, level, max(1, texture_width >> level), max(1, texture_height >> level)); + highp uint word = output_pixel % 2 == 0 ? encoded.x : encoded.y; + encoded_pixel = uvec4(word & 0xffu, (word >> 8u) & 0xffu, (word >> 16u) & 0xffu, word >> 24u); +#else + highp int blocks_per_layer = blocks_x * blocks_y; + highp int layer = gl_VertexID / blocks_per_layer; + highp int block_index = gl_VertexID - layer * blocks_per_layer; + highp ivec2 block = ivec2(block_index % blocks_x, block_index / blocks_x); + encoded_block = compress_block(block, layer, mip_level, texture_width, texture_height); gl_Position = vec4(0.0); +#endif } diff --git a/gl_engine/shaders/texture_compress_raster.vert b/gl_engine/shaders/texture_compress_raster.vert new file mode 100644 index 00000000..5d332762 --- /dev/null +++ b/gl_engine/shaders/texture_compress_raster.vert @@ -0,0 +1,5 @@ +void main() +{ + highp vec2 position = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2)); + gl_Position = vec4(position * 2.0 - 1.0, 0.0, 1.0); +} From 29d7f21456f4c3a7a65e32fd4590fd42bdcb5e74 Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Tue, 18 Aug 2026 19:40:56 +0200 Subject: [PATCH 07/38] Improve GPU texture compression profiling --- .../BenchmarkItem.cpp | 333 ++++++++++++-- .../BenchmarkItem.h | 2 +- .../android/AndroidManifest.xml | 3 +- gl_engine/CMakeLists.txt | 1 + gl_engine/Texture.cpp | 431 ++++++++++++++++-- gl_engine/Texture.h | 64 ++- gl_engine/shaders/texture_compress.vert | 11 +- gl_engine/shaders/texture_compress_pack.frag | 18 + unittests/gl_engine/texture.cpp | 20 + 9 files changed, 793 insertions(+), 90 deletions(-) create mode 100644 gl_engine/shaders/texture_compress_pack.frag diff --git a/apps/texture_compression_benchmark/BenchmarkItem.cpp b/apps/texture_compression_benchmark/BenchmarkItem.cpp index 8264ed3f..822155ca 100644 --- a/apps/texture_compression_benchmark/BenchmarkItem.cpp +++ b/apps/texture_compression_benchmark/BenchmarkItem.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -23,6 +24,7 @@ #include #include #include +#include #include #include @@ -63,6 +65,54 @@ QJsonObject to_json(const Statistics& value) { QStringLiteral("max_ms"), value.maximum } }; } +QJsonArray samples_to_json(const std::vector& values) +{ + QJsonArray result; + for (const auto value : values) + result.append(value); + return result; +} + +struct WallTimingSamples { + std::vector total; + std::vector submission; + std::vector completion_wait; + + void append(const gl_engine::TextureCompressor::StageTiming& timing) + { + total.push_back(timing.total_ms()); + submission.push_back(timing.submission_ms); + completion_wait.push_back(timing.completion_wait_ms); + } + + [[nodiscard]] QJsonObject statistics_json() const + { + return { + { QStringLiteral("total"), to_json(statistics(total)) }, + { QStringLiteral("submission"), to_json(statistics(submission)) }, + { QStringLiteral("completion_wait"), to_json(statistics(completion_wait)) }, + }; + } + + [[nodiscard]] QJsonObject raw_json() const + { + return { + { QStringLiteral("total"), samples_to_json(total) }, + { QStringLiteral("submission"), samples_to_json(submission) }, + { QStringLiteral("completion_wait"), samples_to_json(completion_wait) }, + }; + } +}; + +struct PendingGpuReport { + QJsonObject root; + QStringList summary; + std::vector tickets; + std::vector> query_results; + std::vector query_finished; + int disjoint_samples = 0; +}; + double elapsed_ms(Clock::time_point start) { return std::chrono::duration(Clock::now() - start).count(); @@ -171,17 +221,29 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { void render() override { - if (!m_pending) + if (!m_pending && !m_pending_gpu_report) return; - m_pending = false; m_window->beginExternalCommands(); - const auto [text, json] = run(); + std::optional> completed; + if (m_pending) { + m_pending = false; + const auto immediate = run(); + if (!m_pending_gpu_report) + completed = immediate; + } else { + completed = poll_gpu_report(); + } m_window->endExternalCommands(); - QPointer item = m_item; - QMetaObject::invokeMethod(m_item, [item, text, json]() { - if (item) - item->publishResults(text, json); - }); + if (completed) { + QPointer item = m_item; + const auto [text, json] = std::move(*completed); + QMetaObject::invokeMethod(m_item, [item, text, json]() { + if (item) + item->publishResults(text, json); + }); + } else { + request_another_frame(); + } } QOpenGLFramebufferObject* createFramebufferObject(const QSize&) override @@ -192,6 +254,102 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { } private: + void request_another_frame() + { + QPointer item = m_item; + QMetaObject::invokeMethod(m_item, [item]() { + if (item) + item->update(); + }); + } + + std::optional> poll_gpu_report() + { + Q_ASSERT(m_pending_gpu_report); + bool all_finished = true; + for (size_t i = 0; i < m_pending_gpu_report->tickets.size(); ++i) { + if (m_pending_gpu_report->query_finished[i]) + continue; + gl_engine::TextureCompressor::GpuTimings timings; + const auto status = m_gpu_timer->poll(m_pending_gpu_report->tickets[i], timings); + if (status == gl_engine::TextureCompressor::GpuTimer::PollStatus::Pending) { + all_finished = false; + continue; + } + m_pending_gpu_report->query_finished[i] = true; + if (status == gl_engine::TextureCompressor::GpuTimer::PollStatus::Ready) + m_pending_gpu_report->query_results[i] = timings; + else + ++m_pending_gpu_report->disjoint_samples; + } + if (!all_finished) + return std::nullopt; + + std::vector scratch_upload; + std::vector mipmap_generation; + std::vector compression_pass; + std::vector packing_pass; + std::vector output_transfer; + std::vector compressed_upload; + std::vector total; + for (const auto& result : m_pending_gpu_report->query_results) { + if (!result) + continue; + scratch_upload.push_back(result->scratch_upload_ms); + mipmap_generation.push_back(result->mipmap_generation_ms); + compression_pass.push_back(result->compression_pass_ms); + packing_pass.push_back(result->packing_pass_ms); + output_transfer.push_back(result->output_transfer_ms); + compressed_upload.push_back(result->compressed_upload_ms); + total.push_back(result->total_ms()); + } + + QJsonObject gpu_timer_json { + { QStringLiteral("supported"), true }, + { QStringLiteral("timing_method"), QStringLiteral("EXT_disjoint_timer_query; asynchronous GPU elapsed time") }, + { QStringLiteral("requested_samples"), int(m_pending_gpu_report->tickets.size()) }, + { QStringLiteral("valid_samples"), int(total.size()) }, + { QStringLiteral("disjoint_samples"), m_pending_gpu_report->disjoint_samples }, + }; + if (!total.empty()) { + gpu_timer_json.insert(QStringLiteral("status"), + m_pending_gpu_report->disjoint_samples ? QStringLiteral("partial") : QStringLiteral("valid")); + gpu_timer_json.insert(QStringLiteral("scratch_upload"), to_json(statistics(scratch_upload))); + gpu_timer_json.insert(QStringLiteral("mipmap_generation"), to_json(statistics(mipmap_generation))); + gpu_timer_json.insert(QStringLiteral("compression_pass"), to_json(statistics(compression_pass))); + gpu_timer_json.insert(QStringLiteral("packing_pass"), to_json(statistics(packing_pass))); + gpu_timer_json.insert(QStringLiteral("output_transfer"), to_json(statistics(output_transfer))); + gpu_timer_json.insert(QStringLiteral("compressed_upload"), to_json(statistics(compressed_upload))); + gpu_timer_json.insert(QStringLiteral("total_profiled_stages"), to_json(statistics(total))); + gpu_timer_json.insert(QStringLiteral("raw_samples_ms"), + QJsonObject { + { QStringLiteral("scratch_upload"), samples_to_json(scratch_upload) }, + { QStringLiteral("mipmap_generation"), samples_to_json(mipmap_generation) }, + { QStringLiteral("compression_pass"), samples_to_json(compression_pass) }, + { QStringLiteral("packing_pass"), samples_to_json(packing_pass) }, + { QStringLiteral("output_transfer"), samples_to_json(output_transfer) }, + { QStringLiteral("compressed_upload"), samples_to_json(compressed_upload) }, + { QStringLiteral("total_profiled_stages"), samples_to_json(total) }, + }); + m_pending_gpu_report->summary.push_back(QString()); + m_pending_gpu_report->summary.push_back(QStringLiteral("Actual GPU time (timer query)")); + m_pending_gpu_report->summary.push_back( + QStringLiteral("Compression pass median %1 ms").arg(statistics(compression_pass).median, 8, 'f', 3)); + m_pending_gpu_report->summary.push_back( + QStringLiteral("Packing pass median %1 ms").arg(statistics(packing_pass).median, 8, 'f', 3)); + m_pending_gpu_report->summary.push_back( + QStringLiteral("Profiled GPU stages total median %1 ms").arg(statistics(total).median, 8, 'f', 3)); + } else { + gpu_timer_json.insert(QStringLiteral("status"), QStringLiteral("disjoint")); + } + m_pending_gpu_report->root.insert(QStringLiteral("gpu_timer_query"), gpu_timer_json); + const auto json = QString::fromUtf8(QJsonDocument(m_pending_gpu_report->root).toJson(QJsonDocument::Indented)); + const auto text = m_pending_gpu_report->summary.join('\n'); + qInfo().noquote() << json; + m_pending_gpu_report.reset(); + return std::pair(text, json); + } + std::pair run() { constexpr unsigned resolution = 512; @@ -232,59 +390,97 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { gpu_destination.setParams(filter, gl_engine::Texture::Filter::Linear); gpu_destination.allocate_array(resolution, resolution, unsigned(m_batch_size)); gl_engine::TextureCompressor gpu_compressor(resolution, resolution, unsigned(m_batch_size)); + m_gpu_timer = std::make_unique(); auto upload_cpu = [&](const std::vector& compressed) { + const auto start = Clock::now(); for (size_t layer = 0; layer < compressed.size(); ++layer) cpu_destination.upload(compressed[layer], unsigned(layer)); QOpenGLContext::currentContext()->extraFunctions()->glFinish(); + return elapsed_ms(start); }; - auto warmup_cpu = cpu_compress(sources, algorithm, m_mipmaps); - upload_cpu(warmup_cpu); - static_cast(gpu_compressor.compress(sources, - gpu_destination, - layers, - { .algorithm = algorithm, .effort = unsigned(m_effort), .generate_mipmaps = m_mipmaps, .backend = backend })); + constexpr int warmup_iterations = 3; + const gl_engine::TextureCompressor::Settings gpu_settings { + .algorithm = algorithm, + .effort = unsigned(m_effort), + .generate_mipmaps = m_mipmaps, + .backend = backend, + .timing_mode = gl_engine::TextureCompressor::TimingMode::EndToEnd, + }; std::vector cpu_compression_times; + std::vector cpu_upload_times; std::vector cpu_total_times; - std::vector gpu_upload_times; - std::vector gpu_mipmap_times; - std::vector gpu_encoding_times; - std::vector gpu_output_transfer_times; - std::vector gpu_compressed_upload_times; + WallTimingSamples gpu_upload_times; + WallTimingSamples gpu_mipmap_times; + WallTimingSamples gpu_compression_pass_times; + WallTimingSamples gpu_packing_pass_times; + WallTimingSamples gpu_encoding_times; + WallTimingSamples gpu_output_transfer_times; + WallTimingSamples gpu_compressed_upload_times; std::vector gpu_total_times; + std::vector gpu_timing_tickets; cpu_compression_times.reserve(size_t(m_iterations)); + cpu_upload_times.reserve(size_t(m_iterations)); cpu_total_times.reserve(size_t(m_iterations)); gpu_total_times.reserve(size_t(m_iterations)); + // Keep CPU and GPU phases separate: mobile CPU frequency and thermal state are shared + // with the GPU, so interleaving them makes the CPU result backend-dependent. + for (int iteration = 0; iteration < warmup_iterations; ++iteration) { + auto compressed = cpu_compress(sources, algorithm, m_mipmaps); + static_cast(upload_cpu(compressed)); + } for (int iteration = 0; iteration < m_iterations; ++iteration) { const auto cpu_start = Clock::now(); auto compressed = cpu_compress(sources, algorithm, m_mipmaps); const auto cpu_compression_time = elapsed_ms(cpu_start); - upload_cpu(compressed); + const auto cpu_upload_time = upload_cpu(compressed); cpu_compression_times.push_back(cpu_compression_time); + cpu_upload_times.push_back(cpu_upload_time); cpu_total_times.push_back(elapsed_ms(cpu_start)); + } + for (int iteration = 0; iteration < warmup_iterations; ++iteration) + static_cast(gpu_compressor.compress(sources, gpu_destination, layers, gpu_settings)); + for (int iteration = 0; iteration < m_iterations; ++iteration) { const auto gpu = gpu_compressor.compress(sources, gpu_destination, layers, - { .algorithm = algorithm, .effort = unsigned(m_effort), .generate_mipmaps = m_mipmaps, .backend = backend }); - gpu_upload_times.push_back(gpu.timings.scratch_upload_ms); - gpu_mipmap_times.push_back(gpu.timings.mipmap_generation_ms); - gpu_encoding_times.push_back(gpu.timings.encoding_ms); - gpu_output_transfer_times.push_back(gpu.timings.output_transfer_ms); - gpu_compressed_upload_times.push_back(gpu.timings.compressed_upload_ms); + gpu_settings); gpu_total_times.push_back(gpu.timings.total_ms); } + // Stage timings are collected in a separate profiling phase. Each stage is completed + // independently, so these values diagnose where time is spent but are not summed to + // produce the end-to-end result above. + auto stage_settings = gpu_settings; + stage_settings.timing_mode = gl_engine::TextureCompressor::TimingMode::IndividualStages; + stage_settings.gpu_timer = m_gpu_timer.get(); + for (int iteration = 0; iteration < m_iterations; ++iteration) { + const auto gpu = gpu_compressor.compress(sources, gpu_destination, layers, stage_settings); + gpu_upload_times.append(gpu.timings.scratch_upload); + gpu_mipmap_times.append(gpu.timings.mipmap_generation); + gpu_compression_pass_times.append(gpu.timings.compression_pass); + gpu_packing_pass_times.append(gpu.timings.packing_pass); + gpu_encoding_times.append(gpu.timings.encoding); + gpu_output_transfer_times.append(gpu.timings.output_transfer); + gpu_compressed_upload_times.append(gpu.timings.compressed_upload); + if (gpu.gpu_timing_ticket) + gpu_timing_tickets.push_back(gpu.gpu_timing_ticket); + } + const auto cpu_compression = statistics(cpu_compression_times); + const auto cpu_upload = statistics(cpu_upload_times); const auto cpu_total = statistics(cpu_total_times); - const auto gpu_upload = statistics(gpu_upload_times); - const auto gpu_mipmap = statistics(gpu_mipmap_times); - const auto gpu_encoding = statistics(gpu_encoding_times); - const auto gpu_output_transfer = statistics(gpu_output_transfer_times); - const auto gpu_compressed_upload = statistics(gpu_compressed_upload_times); + const auto gpu_upload = statistics(gpu_upload_times.total); + const auto gpu_mipmap = statistics(gpu_mipmap_times.total); + const auto gpu_compression_pass = statistics(gpu_compression_pass_times.total); + const auto gpu_packing_pass = statistics(gpu_packing_pass_times.total); + const auto gpu_encoding = statistics(gpu_encoding_times.total); + const auto gpu_output_transfer = statistics(gpu_output_transfer_times.total); + const auto gpu_compressed_upload = statistics(gpu_compressed_upload_times.total); const auto gpu_total = statistics(gpu_total_times); const auto cpu_psnr = linear_psnr(reconstruct(cpu_destination, resolution), source); const auto gpu_psnr = linear_psnr(reconstruct(gpu_destination, resolution), source); @@ -297,16 +493,21 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { { QStringLiteral("supported"), true }, { QStringLiteral("algorithm"), algorithm_name }, { QStringLiteral("gpu_backend"), backend_name }, - { QStringLiteral("timing_method"), QStringLiteral("glFinish-synchronised wall time") }, + { QStringLiteral("timing_method"), QStringLiteral("wall time; one final glFinish per end-to-end sample") }, { QStringLiteral("resolution"), int(resolution) }, { QStringLiteral("batch_size"), m_batch_size }, { QStringLiteral("iterations"), m_iterations }, + { QStringLiteral("warmup_iterations"), warmup_iterations }, + { QStringLiteral("gpu_stage_profile_iterations"), m_iterations }, { QStringLiteral("effort"), m_effort }, { QStringLiteral("mipmaps"), m_mipmaps }, { QStringLiteral("cpu_compression"), to_json(cpu_compression) }, + { QStringLiteral("cpu_compressed_upload"), to_json(cpu_upload) }, { QStringLiteral("cpu_end_to_end"), to_json(cpu_total) }, { QStringLiteral("gpu_scratch_upload"), to_json(gpu_upload) }, { QStringLiteral("gpu_mipmap_generation"), to_json(gpu_mipmap) }, + { QStringLiteral("gpu_compression_pass"), to_json(gpu_compression_pass) }, + { QStringLiteral("gpu_packing_pass"), to_json(gpu_packing_pass) }, { QStringLiteral("gpu_encoding"), to_json(gpu_encoding) }, { QStringLiteral("gpu_output_transfer"), to_json(gpu_output_transfer) }, { QStringLiteral("gpu_compressed_upload"), to_json(gpu_compressed_upload) }, @@ -315,8 +516,43 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { { QStringLiteral("gpu_psnr_db"), gpu_psnr }, { QStringLiteral("cpu_tiles_per_second"), 1000.0 * m_batch_size / cpu_total.median }, { QStringLiteral("gpu_tiles_per_second"), 1000.0 * m_batch_size / gpu_total.median }, + { QStringLiteral("phase_order"), QStringLiteral("CPU warmup, CPU measurement, GPU warmup, GPU end-to-end measurement, GPU stage profiling") }, + { QStringLiteral("gpu_stage_timing_method"), QStringLiteral("separate profiling pass; each stage glFinish-synchronised") }, + { QStringLiteral("gpu_stage_wall_profile"), + QJsonObject { + { QStringLiteral("scratch_upload"), gpu_upload_times.statistics_json() }, + { QStringLiteral("mipmap_generation"), gpu_mipmap_times.statistics_json() }, + { QStringLiteral("compression_pass"), gpu_compression_pass_times.statistics_json() }, + { QStringLiteral("packing_pass"), gpu_packing_pass_times.statistics_json() }, + { QStringLiteral("encoding_total"), gpu_encoding_times.statistics_json() }, + { QStringLiteral("output_transfer"), gpu_output_transfer_times.statistics_json() }, + { QStringLiteral("compressed_upload"), gpu_compressed_upload_times.statistics_json() }, + { QStringLiteral("raw_samples_ms"), + QJsonObject { + { QStringLiteral("scratch_upload"), gpu_upload_times.raw_json() }, + { QStringLiteral("mipmap_generation"), gpu_mipmap_times.raw_json() }, + { QStringLiteral("compression_pass"), gpu_compression_pass_times.raw_json() }, + { QStringLiteral("packing_pass"), gpu_packing_pass_times.raw_json() }, + { QStringLiteral("encoding_total"), gpu_encoding_times.raw_json() }, + { QStringLiteral("output_transfer"), gpu_output_transfer_times.raw_json() }, + { QStringLiteral("compressed_upload"), gpu_compressed_upload_times.raw_json() }, + } }, + } }, + { QStringLiteral("raw_samples_ms"), + QJsonObject { + { QStringLiteral("cpu_compression"), samples_to_json(cpu_compression_times) }, + { QStringLiteral("cpu_compressed_upload"), samples_to_json(cpu_upload_times) }, + { QStringLiteral("cpu_end_to_end"), samples_to_json(cpu_total_times) }, + { QStringLiteral("gpu_end_to_end"), samples_to_json(gpu_total_times) }, + { QStringLiteral("gpu_scratch_upload_stage_profile"), samples_to_json(gpu_upload_times.total) }, + { QStringLiteral("gpu_mipmap_generation_stage_profile"), samples_to_json(gpu_mipmap_times.total) }, + { QStringLiteral("gpu_compression_pass_stage_profile"), samples_to_json(gpu_compression_pass_times.total) }, + { QStringLiteral("gpu_packing_pass_stage_profile"), samples_to_json(gpu_packing_pass_times.total) }, + { QStringLiteral("gpu_encoding_stage_profile"), samples_to_json(gpu_encoding_times.total) }, + { QStringLiteral("gpu_output_transfer_stage_profile"), samples_to_json(gpu_output_transfer_times.total) }, + { QStringLiteral("gpu_compressed_upload_stage_profile"), samples_to_json(gpu_compressed_upload_times.total) }, + } }, }; - const auto json = QString::fromUtf8(QJsonDocument(root).toJson(QJsonDocument::Indented)); const auto line = [](QString label, Statistics value) { return QStringLiteral("%1 median %2 ms p95 %3 ms").arg(label, -26).arg(value.median, 8, 'f', 3).arg(value.p95, 8, 'f', 3); }; @@ -330,12 +566,16 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { .arg(m_mipmaps ? QStringLiteral("on") : QStringLiteral("off")), backend_name, gl_string(GL_RENDERER), - QStringLiteral("Timing: completion-synchronised wall time"), + QStringLiteral("Timing: one final glFinish per end-to-end sample"), QString(), line(QStringLiteral("CPU compression"), cpu_compression), + line(QStringLiteral("CPU compressed upload"), cpu_upload), line(QStringLiteral("CPU end-to-end"), cpu_total), + QStringLiteral("GPU stages (separate serialised profiling pass)"), line(QStringLiteral("GPU scratch upload"), gpu_upload), line(QStringLiteral("GPU mip generation"), gpu_mipmap), + line(QStringLiteral("GPU compression pass"), gpu_compression_pass), + line(QStringLiteral("GPU packing pass"), gpu_packing_pass), line(QStringLiteral("GPU encoding"), gpu_encoding), line(QStringLiteral("GPU output transfer"), gpu_output_transfer), line(QStringLiteral("GPU compressed upload"), gpu_compressed_upload), @@ -346,8 +586,27 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { QStringLiteral("CPU PSNR %1 dB").arg(cpu_psnr, 0, 'f', 2), QStringLiteral("GPU PSNR %1 dB").arg(gpu_psnr, 0, 'f', 2), }; + if (m_gpu_timer->is_supported()) { + Q_ASSERT(gpu_timing_tickets.size() == size_t(m_iterations)); + m_pending_gpu_report = PendingGpuReport { + .root = std::move(root), + .summary = std::move(summary), + .tickets = std::move(gpu_timing_tickets), + .query_results = std::vector>(size_t(m_iterations)), + .query_finished = std::vector(size_t(m_iterations), false), + }; + return {}; + } + + root.insert(QStringLiteral("gpu_timer_query"), + QJsonObject { + { QStringLiteral("supported"), false }, + { QStringLiteral("status"), QStringLiteral("unsupported") }, + }); + const auto json = QString::fromUtf8(QJsonDocument(root).toJson(QJsonDocument::Indented)); + const auto text = summary.join('\n'); qInfo().noquote() << json; - return { summary.join('\n'), json }; + return { text, json }; } QPointer m_item; @@ -355,10 +614,12 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { unsigned m_seen_serial = 0; int m_effort = 4; int m_batch_size = 4; - int m_iterations = 7; + int m_iterations = 10; bool m_mipmaps = true; int m_backend = 0; bool m_pending = false; + std::unique_ptr m_gpu_timer; + std::optional m_pending_gpu_report; }; BenchmarkItem::BenchmarkItem(QQuickItem* parent) diff --git a/apps/texture_compression_benchmark/BenchmarkItem.h b/apps/texture_compression_benchmark/BenchmarkItem.h index 84d17844..431cddae 100644 --- a/apps/texture_compression_benchmark/BenchmarkItem.h +++ b/apps/texture_compression_benchmark/BenchmarkItem.h @@ -61,7 +61,7 @@ class BenchmarkItem : public QQuickFramebufferObject { int m_effort = 4; int m_batch_size = 4; - int m_iterations = 7; + int m_iterations = 10; bool m_mipmaps = true; int m_backend = 0; bool m_running = false; diff --git a/apps/texture_compression_benchmark/android/AndroidManifest.xml b/apps/texture_compression_benchmark/android/AndroidManifest.xml index 81518d7f..c0808204 100644 --- a/apps/texture_compression_benchmark/android/AndroidManifest.xml +++ b/apps/texture_compression_benchmark/android/AndroidManifest.xml @@ -3,7 +3,8 @@ - + + diff --git a/gl_engine/CMakeLists.txt b/gl_engine/CMakeLists.txt index c4dd5194..6cecfd77 100644 --- a/gl_engine/CMakeLists.txt +++ b/gl_engine/CMakeLists.txt @@ -88,6 +88,7 @@ qt_add_resources(gl_engine "shaders" shaders/track.frag shaders/track.vert shaders/texture_compress.frag + shaders/texture_compress_pack.frag shaders/texture_compress_raster.vert shaders/texture_compress.vert shaders/turbo_colormap.glsl diff --git a/gl_engine/Texture.cpp b/gl_engine/Texture.cpp index abd9b6e3..377edd68 100644 --- a/gl_engine/Texture.cpp +++ b/gl_engine/Texture.cpp @@ -24,8 +24,11 @@ #include #include #include +#include #include #include +#include +#include #include #ifdef __EMSCRIPTEN__ #include @@ -384,16 +387,287 @@ float gl_engine::Texture::max_anisotropy() } namespace { -template double measure_finished_gl(Callable&& callable) +template gl_engine::TextureCompressor::StageTiming measure_gl(Callable&& callable, bool finish) { const auto start = std::chrono::steady_clock::now(); std::forward(callable)(); - QOpenGLContext::currentContext()->extraFunctions()->glFinish(); - const auto end = std::chrono::steady_clock::now(); - return std::chrono::duration(end - start).count(); + const auto submitted = std::chrono::steady_clock::now(); + if (finish) + QOpenGLContext::currentContext()->extraFunctions()->glFinish(); + const auto completed = std::chrono::steady_clock::now(); + return { + std::chrono::duration(submitted - start).count(), + std::chrono::duration(completed - submitted).count(), + }; } } +double gl_engine::TextureCompressor::GpuTimings::total_ms() const +{ + return scratch_upload_ms + mipmap_generation_ms + compression_pass_ms + packing_pass_ms + output_transfer_ms + + compressed_upload_ms; +} + +struct gl_engine::TextureCompressor::GpuTimer::Impl { + static constexpr GLenum time_elapsed = 0x88BF; // GL_TIME_ELAPSED_EXT + static constexpr GLenum gpu_disjoint = 0x8FBB; // GL_GPU_DISJOINT_EXT + static constexpr size_t stage_count = 6; + + struct Sample { + std::array queries {}; + std::array used {}; + }; + + bool supported = false; + bool uses_extension_functions = false; + uint64_t next_ticket = 1; + uint64_t active_ticket = 0; + std::optional active_stage; + std::unordered_map samples; + +#if defined(__ANDROID__) + using GenQueries = void (*)(GLsizei, GLuint*); + using DeleteQueries = void (*)(GLsizei, const GLuint*); + using BeginQuery = void (*)(GLenum, GLuint); + using EndQuery = void (*)(GLenum); + using GetQueryObjectuiv = void (*)(GLuint, GLenum, GLuint*); + using GetQueryObjectui64v = void (*)(GLuint, GLenum, GLuint64*); + GenQueries gen_queries = nullptr; + DeleteQueries delete_queries = nullptr; + BeginQuery begin_query = nullptr; + EndQuery end_query = nullptr; + GetQueryObjectuiv get_query_object_uiv = nullptr; + GetQueryObjectui64v get_query_object_ui64v = nullptr; +#endif + + Impl() + { + auto* context = QOpenGLContext::currentContext(); + if (!context) + return; +#if defined(__EMSCRIPTEN__) + const auto webgl_context = emscripten_webgl_get_current_context(); + supported = webgl_context && emscripten_webgl_enable_extension(webgl_context, "EXT_disjoint_timer_query_webgl2"); +#elif defined(__ANDROID__) + supported = context->hasExtension(QByteArrayLiteral("GL_EXT_disjoint_timer_query")); + if (!supported) + return; + gen_queries = reinterpret_cast(context->getProcAddress("glGenQueriesEXT")); + delete_queries = reinterpret_cast(context->getProcAddress("glDeleteQueriesEXT")); + begin_query = reinterpret_cast(context->getProcAddress("glBeginQueryEXT")); + end_query = reinterpret_cast(context->getProcAddress("glEndQueryEXT")); + get_query_object_uiv = reinterpret_cast(context->getProcAddress("glGetQueryObjectuivEXT")); + get_query_object_ui64v = reinterpret_cast(context->getProcAddress("glGetQueryObjectui64vEXT")); + supported = gen_queries && delete_queries && begin_query && end_query && get_query_object_uiv && get_query_object_ui64v; + uses_extension_functions = supported; +#else + const auto format = context->format(); + supported = format.majorVersion() > 3 || (format.majorVersion() == 3 && format.minorVersion() >= 3) + || context->hasExtension(QByteArrayLiteral("GL_ARB_timer_query")); +#endif + } + + void gen_query(GLuint* query) + { +#if defined(__EMSCRIPTEN__) + glGenQueries(1, query); + return; +#endif +#if defined(__ANDROID__) + if (uses_extension_functions) { + gen_queries(1, query); + return; + } +#endif + QOpenGLContext::currentContext()->extraFunctions()->glGenQueries(1, query); + } + + void delete_query(GLuint query) + { + if (!query) + return; +#if defined(__EMSCRIPTEN__) + glDeleteQueries(1, &query); + return; +#endif +#if defined(__ANDROID__) + if (uses_extension_functions) { + delete_queries(1, &query); + return; + } +#endif + QOpenGLContext::currentContext()->extraFunctions()->glDeleteQueries(1, &query); + } + + void begin(GLuint query) + { +#if defined(__EMSCRIPTEN__) + glBeginQuery(time_elapsed, query); + return; +#endif +#if defined(__ANDROID__) + if (uses_extension_functions) { + begin_query(time_elapsed, query); + return; + } +#endif + QOpenGLContext::currentContext()->extraFunctions()->glBeginQuery(time_elapsed, query); + } + + void end() + { +#if defined(__EMSCRIPTEN__) + glEndQuery(time_elapsed); + return; +#endif +#if defined(__ANDROID__) + if (uses_extension_functions) { + end_query(time_elapsed); + return; + } +#endif + QOpenGLContext::currentContext()->extraFunctions()->glEndQuery(time_elapsed); + } + + void get_query_uiv(GLuint query, GLenum parameter, GLuint* value) + { +#if defined(__EMSCRIPTEN__) + glGetQueryObjectuiv(query, parameter, value); + return; +#endif +#if defined(__ANDROID__) + if (uses_extension_functions) { + get_query_object_uiv(query, parameter, value); + return; + } +#endif + QOpenGLContext::currentContext()->extraFunctions()->glGetQueryObjectuiv(query, parameter, value); + } + + void get_query_result(GLuint query, GLuint64* value) + { +#if defined(__EMSCRIPTEN__) + GLuint result = 0; + glGetQueryObjectuiv(query, GL_QUERY_RESULT, &result); + *value = result; +#elif defined(__ANDROID__) + get_query_object_ui64v(query, GL_QUERY_RESULT, value); +#else + GLuint result = 0; + QOpenGLContext::currentContext()->extraFunctions()->glGetQueryObjectuiv(query, GL_QUERY_RESULT, &result); + *value = result; +#endif + } + + void delete_sample(Sample& sample) + { + for (const auto query : sample.queries) + delete_query(query); + } +}; + +gl_engine::TextureCompressor::GpuTimer::GpuTimer() + : m(std::make_unique()) +{ +} + +gl_engine::TextureCompressor::GpuTimer::~GpuTimer() +{ + if (!QOpenGLContext::currentContext()) + return; + for (auto& [ticket, sample] : m->samples) { + static_cast(ticket); + m->delete_sample(sample); + } +} + +bool gl_engine::TextureCompressor::GpuTimer::is_supported() const { return m->supported; } + +uint64_t gl_engine::TextureCompressor::GpuTimer::begin_sample() +{ + if (!m->supported) + return 0; + Q_ASSERT(m->active_ticket == 0); + m->active_ticket = m->next_ticket++; + m->samples.emplace(m->active_ticket, Impl::Sample {}); + return m->active_ticket; +} + +void gl_engine::TextureCompressor::GpuTimer::begin_stage(Stage stage) +{ + if (!m->active_ticket) + return; + Q_ASSERT(!m->active_stage.has_value()); + auto& sample = m->samples.at(m->active_ticket); + const auto index = size_t(stage); + Q_ASSERT(!sample.used[index]); + m->gen_query(&sample.queries[index]); + sample.used[index] = true; + m->begin(sample.queries[index]); + m->active_stage = stage; +} + +void gl_engine::TextureCompressor::GpuTimer::end_stage() +{ + if (!m->active_ticket) + return; + Q_ASSERT(m->active_stage.has_value()); + m->end(); + m->active_stage.reset(); +} + +void gl_engine::TextureCompressor::GpuTimer::end_sample() +{ + if (!m->active_ticket) + return; + Q_ASSERT(!m->active_stage.has_value()); + m->active_ticket = 0; +} + +gl_engine::TextureCompressor::GpuTimer::PollStatus gl_engine::TextureCompressor::GpuTimer::poll( + uint64_t ticket, GpuTimings& timings) +{ + Q_ASSERT(ticket != 0); + const auto iterator = m->samples.find(ticket); + Q_ASSERT(iterator != m->samples.end()); + GLint disjoint = GL_FALSE; +#if defined(__EMSCRIPTEN__) || defined(__ANDROID__) + QOpenGLContext::currentContext()->extraFunctions()->glGetIntegerv(Impl::gpu_disjoint, &disjoint); +#endif + if (disjoint) { + m->delete_sample(iterator->second); + m->samples.erase(iterator); + return PollStatus::Disjoint; + } + + for (size_t index = 0; index < Impl::stage_count; ++index) { + if (!iterator->second.used[index]) + continue; + GLuint available = GL_FALSE; + m->get_query_uiv(iterator->second.queries[index], GL_QUERY_RESULT_AVAILABLE, &available); + if (!available) + return PollStatus::Pending; + } + + std::array milliseconds {}; + for (size_t index = 0; index < Impl::stage_count; ++index) { + if (!iterator->second.used[index]) + continue; + GLuint64 nanoseconds = 0; + m->get_query_result(iterator->second.queries[index], &nanoseconds); + milliseconds[index] = double(nanoseconds) / 1'000'000.0; + } + timings.scratch_upload_ms = milliseconds[size_t(Stage::ScratchUpload)]; + timings.mipmap_generation_ms = milliseconds[size_t(Stage::MipmapGeneration)]; + timings.compression_pass_ms = milliseconds[size_t(Stage::CompressionPass)]; + timings.packing_pass_ms = milliseconds[size_t(Stage::PackingPass)]; + timings.output_transfer_ms = milliseconds[size_t(Stage::OutputTransfer)]; + timings.compressed_upload_ms = milliseconds[size_t(Stage::CompressedUpload)]; + m->delete_sample(iterator->second); + m->samples.erase(iterator); + return PollStatus::Ready; +} + struct gl_engine::TextureCompressor::Impl { static constexpr unsigned max_shader_mip_levels = 16; @@ -401,18 +675,23 @@ struct gl_engine::TextureCompressor::Impl { unsigned height = 0; unsigned max_batch_size = 0; unsigned scratch_layers = 0; - GLsizei atlas_width = 0; - GLsizei atlas_height = 0; + GLsizei block_atlas_width = 0; + GLsizei block_atlas_height = 0; + GLsizei output_atlas_width = 0; + GLsizei output_atlas_height = 0; GLuint scratch_texture = 0; + GLuint encoded_texture = 0; GLuint encoded_buffer = 0; GLuint vertex_array = 0; GLuint transform_feedback = 0; GLuint encoding_framebuffer = 0; - GLuint encoding_renderbuffer = 0; + GLuint packing_framebuffer = 0; + GLuint packing_renderbuffer = 0; std::unique_ptr dxt1_transform_program; std::unique_ptr etc1_transform_program; std::unique_ptr dxt1_fragment_program; std::unique_ptr etc1_fragment_program; + std::unique_ptr packing_program; Impl(unsigned texture_width, unsigned texture_height, unsigned maximum_batch_size) : width(texture_width) @@ -430,36 +709,62 @@ struct gl_engine::TextureCompressor::Impl { maximum_size *= max_batch_size; GLint maximum_renderbuffer_size = 0; + GLint maximum_texture_size = 0; f->glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &maximum_renderbuffer_size); - const auto maximum_pixels = maximum_size / 4; - atlas_width = GLsizei(std::min(maximum_pixels, size_t(maximum_renderbuffer_size))); - atlas_height = GLsizei((maximum_pixels + size_t(atlas_width) - 1) / size_t(atlas_width)); - Q_ASSERT(atlas_width > 0 && atlas_height > 0 && atlas_height <= maximum_renderbuffer_size); + f->glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maximum_texture_size); + const auto atlas_size = [](size_t pixels, GLint maximum_dimension) { + const auto atlas_width = GLsizei(std::min(pixels, size_t(maximum_dimension))); + const auto atlas_height = GLsizei((pixels + size_t(atlas_width) - 1) / size_t(atlas_width)); + Q_ASSERT(atlas_width > 0 && atlas_height > 0 && atlas_height <= maximum_dimension); + return std::pair(atlas_width, atlas_height); + }; + std::tie(block_atlas_width, block_atlas_height) = atlas_size(maximum_size / 8, maximum_texture_size); + std::tie(output_atlas_width, output_atlas_height) = atlas_size(maximum_size / 4, maximum_renderbuffer_size); f->glGenBuffers(1, &encoded_buffer); f->glBindBuffer(GL_PIXEL_PACK_BUFFER, encoded_buffer); - f->glBufferData(GL_PIXEL_PACK_BUFFER, GLsizeiptr(size_t(atlas_width) * size_t(atlas_height) * 4), nullptr, GL_STREAM_DRAW); + f->glBufferData(GL_PIXEL_PACK_BUFFER, + GLsizeiptr(size_t(output_atlas_width) * size_t(output_atlas_height) * 4), + nullptr, + GL_STREAM_DRAW); f->glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); f->glGenVertexArrays(1, &vertex_array); GLint previous_draw_framebuffer = 0; GLint previous_read_framebuffer = 0; GLint previous_renderbuffer = 0; + GLint previous_texture = 0; f->glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previous_draw_framebuffer); f->glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &previous_read_framebuffer); f->glGetIntegerv(GL_RENDERBUFFER_BINDING, &previous_renderbuffer); + f->glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture); + + f->glGenTextures(1, &encoded_texture); + f->glBindTexture(GL_TEXTURE_2D, encoded_texture); + f->glTexStorage2D(GL_TEXTURE_2D, 1, GL_RG32UI, block_atlas_width, block_atlas_height); + f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + f->glGenFramebuffers(1, &encoding_framebuffer); - f->glGenRenderbuffers(1, &encoding_renderbuffer); - f->glBindRenderbuffer(GL_RENDERBUFFER, encoding_renderbuffer); + f->glBindFramebuffer(GL_FRAMEBUFFER, encoding_framebuffer); + f->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, encoded_texture, 0); + Q_ASSERT(f->glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE); + + f->glGenFramebuffers(1, &packing_framebuffer); + f->glGenRenderbuffers(1, &packing_renderbuffer); + f->glBindRenderbuffer(GL_RENDERBUFFER, packing_renderbuffer); // RGBA8UI with RGBA_INTEGER/UNSIGNED_BYTE is the portable WebGL 2 integer readback path. // Two pixels hold the two 32-bit words of each compressed block. - f->glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8UI, atlas_width, atlas_height); - f->glBindFramebuffer(GL_FRAMEBUFFER, encoding_framebuffer); - f->glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, encoding_renderbuffer); + f->glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8UI, output_atlas_width, output_atlas_height); + f->glBindFramebuffer(GL_FRAMEBUFFER, packing_framebuffer); + f->glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, packing_renderbuffer); Q_ASSERT(f->glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE); f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, GLuint(previous_draw_framebuffer)); f->glBindFramebuffer(GL_READ_FRAMEBUFFER, GLuint(previous_read_framebuffer)); f->glBindRenderbuffer(GL_RENDERBUFFER, GLuint(previous_renderbuffer)); + f->glBindTexture(GL_TEXTURE_2D, GLuint(previous_texture)); dxt1_fragment_program = std::make_unique("texture_compress_raster.vert", "texture_compress.vert", @@ -469,6 +774,8 @@ struct gl_engine::TextureCompressor::Impl { "texture_compress.vert", ShaderCodeSource::FILE, std::vector { QStringLiteral("#define ALP_FRAGMENT_COMPRESSION"), QStringLiteral("#define ALP_COMPRESS_ETC1") }); + packing_program = std::make_unique( + "texture_compress_raster.vert", "texture_compress_pack.frag", ShaderCodeSource::FILE); #if !defined(__EMSCRIPTEN__) f->glGenTransformFeedbacks(1, &transform_feedback); @@ -489,13 +796,16 @@ struct gl_engine::TextureCompressor::Impl { etc1_transform_program.reset(); dxt1_fragment_program.reset(); etc1_fragment_program.reset(); + packing_program.reset(); if (!QOpenGLContext::currentContext()) return; auto* f = QOpenGLContext::currentContext()->extraFunctions(); if (transform_feedback) f->glDeleteTransformFeedbacks(1, &transform_feedback); f->glDeleteFramebuffers(1, &encoding_framebuffer); - f->glDeleteRenderbuffers(1, &encoding_renderbuffer); + f->glDeleteFramebuffers(1, &packing_framebuffer); + f->glDeleteRenderbuffers(1, &packing_renderbuffer); + f->glDeleteTextures(1, &encoded_texture); f->glDeleteVertexArrays(1, &vertex_array); f->glDeleteBuffers(1, &encoded_buffer); if (scratch_texture) @@ -591,8 +901,22 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: auto* f = QOpenGLContext::currentContext()->extraFunctions(); Result result; result.mip_levels = settings.generate_mipmaps ? mip_level_count(m->width, m->height) : 1; - - result.timings.scratch_upload_ms = measure_finished_gl([&]() { + const bool finish_stages = settings.timing_mode == TimingMode::IndividualStages; + const auto total_start = std::chrono::steady_clock::now(); + auto* gpu_timer = settings.gpu_timer && settings.gpu_timer->is_supported() ? settings.gpu_timer : nullptr; + if (gpu_timer) + result.gpu_timing_ticket = gpu_timer->begin_sample(); + const auto measure_stage = [&](GpuTimer::Stage stage, auto&& callable) { + return measure_gl([&]() { + if (gpu_timer) + gpu_timer->begin_stage(stage); + std::forward(callable)(); + if (gpu_timer) + gpu_timer->end_stage(); + }, finish_stages); + }; + + result.timings.scratch_upload = measure_stage(GpuTimer::Stage::ScratchUpload, [&]() { f->glBindTexture(GL_TEXTURE_2D_ARRAY, m->scratch_texture); f->glPixelStorei(GL_UNPACK_ALIGNMENT, 1); for (size_t layer = 0; layer < textures.size(); ++layer) { @@ -610,7 +934,7 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: } }); if (settings.generate_mipmaps) { - result.timings.mipmap_generation_ms = measure_finished_gl([&]() { + result.timings.mipmap_generation = measure_stage(GpuTimer::Stage::MipmapGeneration, [&]() { f->glBindTexture(GL_TEXTURE_2D_ARRAY, m->scratch_texture); f->glGenerateMipmap(GL_TEXTURE_2D_ARRAY); }); @@ -637,7 +961,7 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: result.encoded_bytes = total_encoded_size; if (settings.backend == Backend::TransformFeedback) { - result.timings.encoding_ms = measure_finished_gl([&]() { + result.timings.compression_pass = measure_stage(GpuTimer::Stage::CompressionPass, [&]() { auto* program = settings.algorithm == nucleus::utils::ColourTexture::Format::DXT1 ? m->dxt1_transform_program.get() : m->etc1_transform_program.get(); program->bind(); program->set_uniform("source_texture", 7); @@ -672,33 +996,40 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: f->glBindVertexArray(0); program->release(); }); + result.timings.encoding = result.timings.compression_pass; } else { - result.timings.encoding_ms = measure_finished_gl([&]() { + GLint previous_draw_framebuffer = 0; + GLint previous_viewport[4] = {}; + GLboolean previous_colour_mask[4] = {}; + GLboolean blend_enabled = GL_FALSE; + GLboolean cull_enabled = GL_FALSE; + GLboolean depth_enabled = GL_FALSE; + GLboolean scissor_enabled = GL_FALSE; + + result.timings.compression_pass = measure_stage(GpuTimer::Stage::CompressionPass, [&]() { auto* program = settings.algorithm == nucleus::utils::ColourTexture::Format::DXT1 ? m->dxt1_fragment_program.get() : m->etc1_fragment_program.get(); - GLint previous_draw_framebuffer = 0; - GLint previous_viewport[4] = {}; - GLboolean previous_colour_mask[4] = {}; f->glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previous_draw_framebuffer); f->glGetIntegerv(GL_VIEWPORT, previous_viewport); f->glGetBooleanv(GL_COLOR_WRITEMASK, previous_colour_mask); - const auto blend_enabled = f->glIsEnabled(GL_BLEND); - const auto cull_enabled = f->glIsEnabled(GL_CULL_FACE); - const auto depth_enabled = f->glIsEnabled(GL_DEPTH_TEST); - const auto scissor_enabled = f->glIsEnabled(GL_SCISSOR_TEST); + blend_enabled = f->glIsEnabled(GL_BLEND); + cull_enabled = f->glIsEnabled(GL_CULL_FACE); + depth_enabled = f->glIsEnabled(GL_DEPTH_TEST); + scissor_enabled = f->glIsEnabled(GL_SCISSOR_TEST); - f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m->encoding_framebuffer); - f->glViewport(0, 0, m->atlas_width, m->atlas_height); f->glDisable(GL_BLEND); f->glDisable(GL_CULL_FACE); f->glDisable(GL_DEPTH_TEST); f->glDisable(GL_SCISSOR_TEST); f->glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + + f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m->encoding_framebuffer); + f->glViewport(0, 0, m->block_atlas_width, m->block_atlas_height); program->bind(); program->set_uniform("source_texture", 7); program->set_uniform("texture_width", int(m->width)); program->set_uniform("texture_height", int(m->height)); program->set_uniform("effort", int(settings.effort)); - program->set_uniform("atlas_width", int(m->atlas_width)); + program->set_uniform("atlas_width", int(m->block_atlas_width)); program->set_uniform("total_blocks", int(total_encoded_size / 8)); program->set_uniform("mip_levels", int(result.mip_levels)); program->set_uniform_array("level_offsets", level_offsets_blocks); @@ -708,8 +1039,21 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: f->glBindTexture(GL_TEXTURE_2D_ARRAY, m->scratch_texture); f->glBindVertexArray(m->vertex_array); f->glDrawArrays(GL_TRIANGLES, 0, 3); + }); + + result.timings.packing_pass = measure_stage(GpuTimer::Stage::PackingPass, [&]() { + f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m->packing_framebuffer); + f->glViewport(0, 0, m->output_atlas_width, m->output_atlas_height); + m->packing_program->bind(); + m->packing_program->set_uniform("encoded_blocks", 6); + m->packing_program->set_uniform("block_atlas_width", int(m->block_atlas_width)); + m->packing_program->set_uniform("output_atlas_width", int(m->output_atlas_width)); + m->packing_program->set_uniform("total_blocks", int(total_encoded_size / 8)); + f->glActiveTexture(GL_TEXTURE6); + f->glBindTexture(GL_TEXTURE_2D, m->encoded_texture); + f->glDrawArrays(GL_TRIANGLES, 0, 3); f->glBindVertexArray(0); - program->release(); + m->packing_program->release(); if (blend_enabled) f->glEnable(GL_BLEND); @@ -723,19 +1067,23 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: f->glViewport(previous_viewport[0], previous_viewport[1], previous_viewport[2], previous_viewport[3]); f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, GLuint(previous_draw_framebuffer)); }); + result.timings.encoding.submission_ms + = result.timings.compression_pass.submission_ms + result.timings.packing_pass.submission_ms; + result.timings.encoding.completion_wait_ms + = result.timings.compression_pass.completion_wait_ms + result.timings.packing_pass.completion_wait_ms; - result.timings.output_transfer_ms = measure_finished_gl([&]() { + result.timings.output_transfer = measure_stage(GpuTimer::Stage::OutputTransfer, [&]() { GLint previous_read_framebuffer = 0; GLint previous_read_buffer = 0; GLint previous_pack_alignment = 0; f->glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &previous_read_framebuffer); f->glGetIntegerv(GL_READ_BUFFER, &previous_read_buffer); f->glGetIntegerv(GL_PACK_ALIGNMENT, &previous_pack_alignment); - f->glBindFramebuffer(GL_READ_FRAMEBUFFER, m->encoding_framebuffer); + f->glBindFramebuffer(GL_READ_FRAMEBUFFER, m->packing_framebuffer); f->glReadBuffer(GL_COLOR_ATTACHMENT0); f->glPixelStorei(GL_PACK_ALIGNMENT, 1); f->glBindBuffer(GL_PIXEL_PACK_BUFFER, m->encoded_buffer); - f->glReadPixels(0, 0, m->atlas_width, m->atlas_height, GL_RGBA_INTEGER, GL_UNSIGNED_BYTE, nullptr); + f->glReadPixels(0, 0, m->output_atlas_width, m->output_atlas_height, GL_RGBA_INTEGER, GL_UNSIGNED_BYTE, nullptr); f->glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); f->glPixelStorei(GL_PACK_ALIGNMENT, previous_pack_alignment); f->glBindFramebuffer(GL_READ_FRAMEBUFFER, GLuint(previous_read_framebuffer)); @@ -743,7 +1091,7 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: }); } - result.timings.compressed_upload_ms = measure_finished_gl([&]() { + result.timings.compressed_upload = measure_stage(GpuTimer::Stage::CompressedUpload, [&]() { f->glBindTexture(GL_TEXTURE_2D_ARRAY, destination.m_id); f->glBindBuffer(GL_PIXEL_UNPACK_BUFFER, m->encoded_buffer); const auto format = Texture::compressed_texture_format(); @@ -768,8 +1116,11 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: } f->glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); }); + if (gpu_timer) + gpu_timer->end_sample(); f->glActiveTexture(GL_TEXTURE0); - result.timings.total_ms = result.timings.scratch_upload_ms + result.timings.mipmap_generation_ms + result.timings.encoding_ms - + result.timings.output_transfer_ms + result.timings.compressed_upload_ms; + if (!finish_stages) + f->glFinish(); + result.timings.total_ms = std::chrono::duration(std::chrono::steady_clock::now() - total_start).count(); return result; } diff --git a/gl_engine/Texture.h b/gl_engine/Texture.h index 8fdf3373..116fb0f6 100644 --- a/gl_engine/Texture.h +++ b/gl_engine/Texture.h @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -92,20 +93,72 @@ class Texture { class TextureCompressor { public: enum class Backend { FragmentShader, TransformFeedback }; + enum class TimingMode { EndToEnd, IndividualStages }; + + struct GpuTimings { + double scratch_upload_ms = 0.0; + double mipmap_generation_ms = 0.0; + double compression_pass_ms = 0.0; + double packing_pass_ms = 0.0; + double output_transfer_ms = 0.0; + double compressed_upload_ms = 0.0; + + [[nodiscard]] double total_ms() const; + }; + + class GpuTimer { + public: + enum class PollStatus { Pending, Ready, Disjoint }; + + GpuTimer(); + ~GpuTimer(); + GpuTimer(const GpuTimer&) = delete; + GpuTimer(GpuTimer&&) = delete; + GpuTimer& operator=(const GpuTimer&) = delete; + GpuTimer& operator=(GpuTimer&&) = delete; + + [[nodiscard]] bool is_supported() const; + [[nodiscard]] PollStatus poll(uint64_t ticket, GpuTimings& timings); + + private: + friend class TextureCompressor; + enum class Stage { ScratchUpload, MipmapGeneration, CompressionPass, PackingPass, OutputTransfer, CompressedUpload }; + + [[nodiscard]] uint64_t begin_sample(); + void begin_stage(Stage stage); + void end_stage(); + void end_sample(); + + struct Impl; + std::unique_ptr m; + }; struct Settings { nucleus::utils::ColourTexture::Format algorithm = nucleus::utils::ColourTexture::Format::DXT1; unsigned effort = 0; bool generate_mipmaps = true; Backend backend = Backend::FragmentShader; + TimingMode timing_mode = TimingMode::EndToEnd; + GpuTimer* gpu_timer = nullptr; + }; + + struct StageTiming { + double submission_ms = 0.0; + double completion_wait_ms = 0.0; + + [[nodiscard]] double total_ms() const { return submission_ms + completion_wait_ms; } }; struct Timings { - double scratch_upload_ms = 0.0; - double mipmap_generation_ms = 0.0; - double encoding_ms = 0.0; - double output_transfer_ms = 0.0; - double compressed_upload_ms = 0.0; + // total_ms always includes one completion wait. Individual stage values only include + // completion waits when Settings::timing_mode is IndividualStages. + StageTiming scratch_upload; + StageTiming mipmap_generation; + StageTiming compression_pass; + StageTiming packing_pass; + StageTiming encoding; + StageTiming output_transfer; + StageTiming compressed_upload; double total_ms = 0.0; }; @@ -113,6 +166,7 @@ class TextureCompressor { Timings timings; size_t encoded_bytes = 0; unsigned mip_levels = 0; + uint64_t gpu_timing_ticket = 0; }; TextureCompressor(unsigned width, unsigned height, unsigned max_batch_size); diff --git a/gl_engine/shaders/texture_compress.vert b/gl_engine/shaders/texture_compress.vert index 0caaea20..2643f3a6 100644 --- a/gl_engine/shaders/texture_compress.vert +++ b/gl_engine/shaders/texture_compress.vert @@ -9,7 +9,7 @@ uniform highp int mip_levels; uniform highp int level_offsets[max_mip_levels]; uniform highp int level_blocks_x[max_mip_levels]; uniform highp int level_blocks_y[max_mip_levels]; -layout(location = 0) out highp uvec4 encoded_pixel; +layout(location = 0) out highp uvec2 encoded_block; #else uniform highp int blocks_x; uniform highp int blocks_y; @@ -197,10 +197,9 @@ highp uvec2 compress_block(highp ivec2 block, void main() { #ifdef ALP_FRAGMENT_COMPRESSION - highp int output_pixel = int(gl_FragCoord.y) * atlas_width + int(gl_FragCoord.x); - if (output_pixel >= total_blocks * 2) + highp int output_index = int(gl_FragCoord.y) * atlas_width + int(gl_FragCoord.x); + if (output_index >= total_blocks) discard; - highp int output_index = output_pixel / 2; highp int level = 0; for (int candidate = 1; candidate < max_mip_levels; ++candidate) { @@ -216,9 +215,7 @@ void main() highp int layer = level_index / blocks_per_layer; highp int block_index = level_index - layer * blocks_per_layer; highp ivec2 block = ivec2(block_index % blocks_x_at_level, block_index / blocks_x_at_level); - highp uvec2 encoded = compress_block(block, layer, level, max(1, texture_width >> level), max(1, texture_height >> level)); - highp uint word = output_pixel % 2 == 0 ? encoded.x : encoded.y; - encoded_pixel = uvec4(word & 0xffu, (word >> 8u) & 0xffu, (word >> 16u) & 0xffu, word >> 24u); + encoded_block = compress_block(block, layer, level, max(1, texture_width >> level), max(1, texture_height >> level)); #else highp int blocks_per_layer = blocks_x * blocks_y; highp int layer = gl_VertexID / blocks_per_layer; diff --git a/gl_engine/shaders/texture_compress_pack.frag b/gl_engine/shaders/texture_compress_pack.frag new file mode 100644 index 00000000..d759b153 --- /dev/null +++ b/gl_engine/shaders/texture_compress_pack.frag @@ -0,0 +1,18 @@ +uniform highp usampler2D encoded_blocks; +uniform highp int block_atlas_width; +uniform highp int output_atlas_width; +uniform highp int total_blocks; +layout(location = 0) out highp uvec4 encoded_pixel; + +void main() +{ + highp int output_pixel = int(gl_FragCoord.y) * output_atlas_width + int(gl_FragCoord.x); + if (output_pixel >= total_blocks * 2) + discard; + + highp int block_index = output_pixel / 2; + highp ivec2 block_position = ivec2(block_index % block_atlas_width, block_index / block_atlas_width); + highp uvec2 encoded = texelFetch(encoded_blocks, block_position, 0).rg; + highp uint word = output_pixel % 2 == 0 ? encoded.x : encoded.y; + encoded_pixel = uvec4(word & 0xffu, (word >> 8u) & 0xffu, (word >> 16u) & 0xffu, word >> 24u); +} diff --git a/unittests/gl_engine/texture.cpp b/unittests/gl_engine/texture.cpp index d032339f..55e3f2f6 100644 --- a/unittests/gl_engine/texture.cpp +++ b/unittests/gl_engine/texture.cpp @@ -702,12 +702,15 @@ TEST_CASE("gl texture GPU compression quality") destination.setParams(gl_engine::Texture::Filter::MipMapLinear, gl_engine::Texture::Filter::Nearest); destination.allocate_array(resolution, resolution, unsigned(sources.size())); + auto* f = QOpenGLContext::currentContext()->extraFunctions(); + while (f->glGetError() != GL_NO_ERROR) { } gl_engine::TextureCompressor compressor(resolution, resolution, unsigned(sources.size())); const std::array destination_layers { 0, 1 }; const auto result = compressor.compress(sources, destination, destination_layers, { .algorithm = gl_engine::Texture::compression_algorithm(), .effort = 4, .generate_mipmaps = true }); + CHECK(f->glGetError() == GL_NO_ERROR); size_t expected_size = 0; for (unsigned level = 0; level < gl_engine::TextureCompressor::mip_level_count(resolution, resolution); ++level) { expected_size += gl_engine::TextureCompressor::compressed_level_size( @@ -717,6 +720,23 @@ TEST_CASE("gl texture GPU compression quality") CHECK(result.mip_levels == 7); CHECK(result.timings.total_ms > 0.0); + const auto profiled_result = compressor.compress(sources, + destination, + destination_layers, + { .algorithm = gl_engine::Texture::compression_algorithm(), + .effort = 4, + .generate_mipmaps = true, + .timing_mode = gl_engine::TextureCompressor::TimingMode::IndividualStages }); + CHECK(profiled_result.timings.scratch_upload.total_ms() > 0.0); + CHECK(profiled_result.timings.mipmap_generation.total_ms() > 0.0); + CHECK(profiled_result.timings.compression_pass.total_ms() > 0.0); + CHECK(profiled_result.timings.packing_pass.total_ms() > 0.0); + CHECK(profiled_result.timings.encoding.total_ms() > 0.0); + CHECK(profiled_result.timings.output_transfer.total_ms() > 0.0); + CHECK(profiled_result.timings.compressed_upload.total_ms() > 0.0); + CHECK(profiled_result.timings.total_ms > 0.0); + CHECK(f->glGetError() == GL_NO_ERROR); + Framebuffer framebuffer(Framebuffer::DepthFormat::None, { Framebuffer::ColourFormat::RGBA8 }, { resolution, resolution }); framebuffer.bind(); ShaderProgram shader = create_debug_shader(R"( From a8d21fdb919a9cad1aa96f9047bad7f8be56921f Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Tue, 18 Aug 2026 22:51:22 +0200 Subject: [PATCH 08/38] Add asynchronous GPU compression verification --- .../BenchmarkItem.cpp | 348 ++++++++++++++++-- gl_engine/Texture.cpp | 12 +- gl_engine/Texture.h | 6 +- unittests/gl_engine/texture.cpp | 16 + 4 files changed, 338 insertions(+), 44 deletions(-) diff --git a/apps/texture_compression_benchmark/BenchmarkItem.cpp b/apps/texture_compression_benchmark/BenchmarkItem.cpp index 822155ca..a8252dbf 100644 --- a/apps/texture_compression_benchmark/BenchmarkItem.cpp +++ b/apps/texture_compression_benchmark/BenchmarkItem.cpp @@ -34,6 +34,9 @@ #include #include #include +#if defined(__EMSCRIPTEN__) +#include +#endif namespace { using Raster = radix::Raster; @@ -104,6 +107,33 @@ struct WallTimingSamples { } }; +struct PendingFenceDiagnostic { + std::unique_ptr compressor; + std::unique_ptr destination; + std::unique_ptr probe_framebuffer; + std::unique_ptr probe_shader; + gl_engine::helpers::ScreenQuadGeometry probe_geometry; + std::vector sources; + std::vector layers; + gl_engine::TextureCompressor::Settings settings; + GLsync fence = nullptr; + Clock::time_point started_at; + int iteration = 0; + int iteration_count = 0; + int current_poll_count = 0; + GLenum wait_error = GL_NO_ERROR; + double failure_elapsed_ms = 0.0; + QString status = QStringLiteral("pending"); + std::vector submission_ms; + std::vector fence_completion_ms; + std::vector verification_readback_ms; + std::vector verified_end_to_end_ms; + std::vector poll_counts; + std::vector sample_checksums; + std::vector last_source_markers; + std::vector last_sampled_pixels; +}; + struct PendingGpuReport { QJsonObject root; QStringList summary; @@ -111,6 +141,7 @@ struct PendingGpuReport { std::vector> query_results; std::vector query_finished; int disjoint_samples = 0; + std::optional fence_diagnostic; }; double elapsed_ms(Clock::time_point start) @@ -199,6 +230,51 @@ QString gl_string(GLenum name) return value ? QString::fromLatin1(reinterpret_cast(value)) : QStringLiteral("unavailable"); } +QJsonArray pixels_to_json(const std::vector& pixels) +{ + QJsonArray result; + for (const auto& pixel : pixels) { + result.append(QJsonArray { int(pixel.x), int(pixel.y), int(pixel.z), int(pixel.w) }); + } + return result; +} + +GLsync create_gpu_fence(QOpenGLExtraFunctions* f) +{ +#if defined(__EMSCRIPTEN__) + static_cast(f); + return emscripten_glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); +#else + return f->glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); +#endif +} + +void flush_gpu_commands(QOpenGLExtraFunctions* f) +{ + f->glFlush(); +} + +GLenum poll_gpu_fence(QOpenGLExtraFunctions* f, GLsync fence) +{ +#if defined(__EMSCRIPTEN__) + GLint status = GL_UNSIGNALED; + f->glGetSynciv(fence, GL_SYNC_STATUS, 1, nullptr, &status); + return status == GL_SIGNALED ? GL_ALREADY_SIGNALED : GL_TIMEOUT_EXPIRED; +#else + return f->glClientWaitSync(fence, 0, 0); +#endif +} + +void delete_gpu_fence(QOpenGLExtraFunctions* f, GLsync fence) +{ +#if defined(__EMSCRIPTEN__) + static_cast(f); + emscripten_glDeleteSync(fence); +#else + f->glDeleteSync(fence); +#endif +} + } // namespace class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { @@ -256,16 +332,189 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { private: void request_another_frame() { - QPointer item = m_item; - QMetaObject::invokeMethod(m_item, [item]() { - if (item) - item->update(); + update(); + QPointer window = m_window; + QMetaObject::invokeMethod(m_window, [window]() { + if (window) + window->update(); }); } + void begin_fence_sample(PendingFenceDiagnostic& diagnostic) + { + diagnostic.last_source_markers.clear(); + diagnostic.last_source_markers.reserve(diagnostic.sources.size()); + for (size_t layer = 0; layer < diagnostic.sources.size(); ++layer) { + const auto marker = glm::u8vec4(uint8_t((37 + 53 * layer + 29 * size_t(diagnostic.iteration)) % 256), + uint8_t((83 + 97 * layer + 47 * size_t(diagnostic.iteration)) % 256), + uint8_t((149 + 31 * layer + 71 * size_t(diagnostic.iteration)) % 256), + 255); + diagnostic.last_source_markers.push_back(marker); + const auto centre = diagnostic.sources[layer].size() / 2u; + for (unsigned y = centre.y - 8; y < centre.y + 8; ++y) { + for (unsigned x = centre.x - 8; x < centre.x + 8; ++x) + diagnostic.sources[layer].pixel({ x, y }) = marker; + } + } + + diagnostic.started_at = Clock::now(); + static_cast(diagnostic.compressor->compress( + diagnostic.sources, *diagnostic.destination, diagnostic.layers, diagnostic.settings)); + auto* f = QOpenGLContext::currentContext()->extraFunctions(); + diagnostic.fence = create_gpu_fence(f); + flush_gpu_commands(f); + diagnostic.submission_ms.push_back(elapsed_ms(diagnostic.started_at)); + diagnostic.current_poll_count = 0; + if (!diagnostic.fence) { + diagnostic.status = QStringLiteral("fence_creation_failed"); + diagnostic.wait_error = f->glGetError(); + } + } + + QImage read_fence_probe(PendingFenceDiagnostic& diagnostic) + { + diagnostic.probe_framebuffer->bind(); + diagnostic.probe_shader->bind(); + diagnostic.destination->bind(0); + diagnostic.probe_shader->set_uniform("texture_sampler", 0); + diagnostic.probe_geometry.draw(); + auto image = diagnostic.probe_framebuffer->read_colour_attachment(0); + gl_engine::Framebuffer::unbind(); + return image; + } + + bool poll_fence_diagnostic(PendingFenceDiagnostic& diagnostic) + { + if (diagnostic.status != QStringLiteral("pending")) + return true; + + ++diagnostic.current_poll_count; + auto* f = QOpenGLContext::currentContext()->extraFunctions(); + const auto wait_status = poll_gpu_fence(f, diagnostic.fence); + if (wait_status == GL_TIMEOUT_EXPIRED && elapsed_ms(diagnostic.started_at) < 5000.0) + return false; + if (wait_status == GL_TIMEOUT_EXPIRED) { + diagnostic.status = QStringLiteral("fence_timeout"); + diagnostic.failure_elapsed_ms = elapsed_ms(diagnostic.started_at); + delete_gpu_fence(f, diagnostic.fence); + diagnostic.fence = nullptr; + return true; + } + if (wait_status == GL_WAIT_FAILED) { + diagnostic.status = QStringLiteral("client_wait_failed"); + diagnostic.wait_error = f->glGetError(); + delete_gpu_fence(f, diagnostic.fence); + diagnostic.fence = nullptr; + return true; + } + if (wait_status != GL_ALREADY_SIGNALED && wait_status != GL_CONDITION_SATISFIED) { + diagnostic.status = QStringLiteral("unexpected_wait_status"); + diagnostic.wait_error = wait_status; + delete_gpu_fence(f, diagnostic.fence); + diagnostic.fence = nullptr; + return true; + } + + diagnostic.fence_completion_ms.push_back(elapsed_ms(diagnostic.started_at)); + diagnostic.poll_counts.push_back(diagnostic.current_poll_count); + delete_gpu_fence(f, diagnostic.fence); + diagnostic.fence = nullptr; + + const auto readback_start = Clock::now(); + const auto sampled = read_fence_probe(diagnostic); + diagnostic.verification_readback_ms.push_back(elapsed_ms(readback_start)); + diagnostic.verified_end_to_end_ms.push_back(elapsed_ms(diagnostic.started_at)); + + uint64_t checksum = 14695981039346656037ull; + diagnostic.last_sampled_pixels.clear(); + diagnostic.last_sampled_pixels.reserve(size_t(sampled.width())); + for (int x = 0; x < sampled.width(); ++x) { + const auto pixel = sampled.pixel(x, 0); + const auto rgba = glm::u8vec4(qRed(pixel), qGreen(pixel), qBlue(pixel), qAlpha(pixel)); + diagnostic.last_sampled_pixels.push_back(rgba); + for (const auto channel : { rgba.x, rgba.y, rgba.z, rgba.w }) { + checksum ^= channel; + checksum *= 1099511628211ull; + } + } + diagnostic.sample_checksums.push_back(QStringLiteral("0x%1").arg(checksum, 16, 16, QLatin1Char('0'))); + + ++diagnostic.iteration; + if (diagnostic.iteration < diagnostic.iteration_count) { + begin_fence_sample(diagnostic); + return false; + } + diagnostic.status = QStringLiteral("valid"); + return true; + } + + void append_fence_report(PendingGpuReport& report, const PendingFenceDiagnostic& diagnostic) + { + QJsonArray poll_counts; + for (const auto count : diagnostic.poll_counts) + poll_counts.append(count); + QJsonArray checksums; + for (const auto& checksum : diagnostic.sample_checksums) + checksums.append(checksum); + + QJsonObject json { + { QStringLiteral("supported"), true }, + { QStringLiteral("status"), diagnostic.status }, + { QStringLiteral("requested_samples"), diagnostic.iteration_count }, + { QStringLiteral("completed_samples"), int(diagnostic.verified_end_to_end_ms.size()) }, + { QStringLiteral("timing_method"), + QStringLiteral("glFenceSync + later-frame nonblocking status polling; dependent sampled-texture CPU readback") }, + { QStringLiteral("wait_error"), int(diagnostic.wait_error) }, + { QStringLiteral("watchdog_ms"), 5000 }, + { QStringLiteral("failure_elapsed_ms"), diagnostic.failure_elapsed_ms }, + { QStringLiteral("poll_counts"), poll_counts }, + { QStringLiteral("sample_checksums_fnv1a64"), checksums }, + { QStringLiteral("last_source_markers_srgb8"), pixels_to_json(diagnostic.last_source_markers) }, + { QStringLiteral("last_sampled_layers_linear_rgba8"), pixels_to_json(diagnostic.last_sampled_pixels) }, + }; + if (!diagnostic.verified_end_to_end_ms.empty()) { + json.insert(QStringLiteral("submission"), to_json(statistics(diagnostic.submission_ms))); + json.insert(QStringLiteral("fence_completion"), to_json(statistics(diagnostic.fence_completion_ms))); + json.insert(QStringLiteral("verification_readback"), to_json(statistics(diagnostic.verification_readback_ms))); + json.insert(QStringLiteral("verified_end_to_end"), to_json(statistics(diagnostic.verified_end_to_end_ms))); + json.insert(QStringLiteral("raw_samples_ms"), + QJsonObject { + { QStringLiteral("submission"), samples_to_json(diagnostic.submission_ms) }, + { QStringLiteral("fence_completion"), samples_to_json(diagnostic.fence_completion_ms) }, + { QStringLiteral("verification_readback"), samples_to_json(diagnostic.verification_readback_ms) }, + { QStringLiteral("verified_end_to_end"), samples_to_json(diagnostic.verified_end_to_end_ms) }, + }); + report.summary.push_back(QString()); + report.summary.push_back(QStringLiteral("Fence + dependent readback verification")); + report.summary.push_back(QStringLiteral("Fence completion median %1 ms") + .arg(statistics(diagnostic.fence_completion_ms).median, 8, 'f', 3)); + report.summary.push_back(QStringLiteral("Verified end-to-end median %1 ms") + .arg(statistics(diagnostic.verified_end_to_end_ms).median, 8, 'f', 3)); + } + report.root.insert(QStringLiteral("gpu_fence_verification"), json); + } + + std::pair finish_report() + { + const auto json = QString::fromUtf8(QJsonDocument(m_pending_gpu_report->root).toJson(QJsonDocument::Indented)); + const auto text = m_pending_gpu_report->summary.join('\n'); + qInfo().noquote() << json; + m_pending_gpu_report.reset(); + return { text, json }; + } + std::optional> poll_gpu_report() { Q_ASSERT(m_pending_gpu_report); + if (m_pending_gpu_report->fence_diagnostic) { + if (!poll_fence_diagnostic(*m_pending_gpu_report->fence_diagnostic)) + return std::nullopt; + append_fence_report(*m_pending_gpu_report, *m_pending_gpu_report->fence_diagnostic); + m_pending_gpu_report->fence_diagnostic.reset(); + } + if (!m_gpu_timer->is_supported()) + return finish_report(); + bool all_finished = true; for (size_t i = 0; i < m_pending_gpu_report->tickets.size(); ++i) { if (m_pending_gpu_report->query_finished[i]) @@ -343,11 +592,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { gpu_timer_json.insert(QStringLiteral("status"), QStringLiteral("disjoint")); } m_pending_gpu_report->root.insert(QStringLiteral("gpu_timer_query"), gpu_timer_json); - const auto json = QString::fromUtf8(QJsonDocument(m_pending_gpu_report->root).toJson(QJsonDocument::Indented)); - const auto text = m_pending_gpu_report->summary.join('\n'); - qInfo().noquote() << json; - m_pending_gpu_report.reset(); - return std::pair(text, json); + return finish_report(); } std::pair run() @@ -386,10 +631,11 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { gl_engine::Texture cpu_destination(gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); cpu_destination.setParams(filter, gl_engine::Texture::Filter::Linear); cpu_destination.allocate_array(resolution, resolution, unsigned(m_batch_size)); - gl_engine::Texture gpu_destination(gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); - gpu_destination.setParams(filter, gl_engine::Texture::Filter::Linear); - gpu_destination.allocate_array(resolution, resolution, unsigned(m_batch_size)); - gl_engine::TextureCompressor gpu_compressor(resolution, resolution, unsigned(m_batch_size)); + auto gpu_destination = std::make_unique( + gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); + gpu_destination->setParams(filter, gl_engine::Texture::Filter::Linear); + gpu_destination->allocate_array(resolution, resolution, unsigned(m_batch_size)); + auto gpu_compressor = std::make_unique(resolution, resolution, unsigned(m_batch_size)); m_gpu_timer = std::make_unique(); auto upload_cpu = [&](const std::vector& compressed) { @@ -443,10 +689,10 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { } for (int iteration = 0; iteration < warmup_iterations; ++iteration) - static_cast(gpu_compressor.compress(sources, gpu_destination, layers, gpu_settings)); + static_cast(gpu_compressor->compress(sources, *gpu_destination, layers, gpu_settings)); for (int iteration = 0; iteration < m_iterations; ++iteration) { - const auto gpu = gpu_compressor.compress(sources, - gpu_destination, + const auto gpu = gpu_compressor->compress(sources, + *gpu_destination, layers, gpu_settings); gpu_total_times.push_back(gpu.timings.total_ms); @@ -459,7 +705,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { stage_settings.timing_mode = gl_engine::TextureCompressor::TimingMode::IndividualStages; stage_settings.gpu_timer = m_gpu_timer.get(); for (int iteration = 0; iteration < m_iterations; ++iteration) { - const auto gpu = gpu_compressor.compress(sources, gpu_destination, layers, stage_settings); + const auto gpu = gpu_compressor->compress(sources, *gpu_destination, layers, stage_settings); gpu_upload_times.append(gpu.timings.scratch_upload); gpu_mipmap_times.append(gpu.timings.mipmap_generation); gpu_compression_pass_times.append(gpu.timings.compression_pass); @@ -483,7 +729,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { const auto gpu_compressed_upload = statistics(gpu_compressed_upload_times.total); const auto gpu_total = statistics(gpu_total_times); const auto cpu_psnr = linear_psnr(reconstruct(cpu_destination, resolution), source); - const auto gpu_psnr = linear_psnr(reconstruct(gpu_destination, resolution), source); + const auto gpu_psnr = linear_psnr(reconstruct(*gpu_destination, resolution), source); const auto algorithm_name = algorithm == nucleus::utils::ColourTexture::Format::DXT1 ? QStringLiteral("DXT1 / BC1") : QStringLiteral("ETC1 in ETC2"); QJsonObject root { @@ -516,7 +762,8 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { { QStringLiteral("gpu_psnr_db"), gpu_psnr }, { QStringLiteral("cpu_tiles_per_second"), 1000.0 * m_batch_size / cpu_total.median }, { QStringLiteral("gpu_tiles_per_second"), 1000.0 * m_batch_size / gpu_total.median }, - { QStringLiteral("phase_order"), QStringLiteral("CPU warmup, CPU measurement, GPU warmup, GPU end-to-end measurement, GPU stage profiling") }, + { QStringLiteral("phase_order"), + QStringLiteral("CPU warmup, CPU measurement, GPU warmup, GPU end-to-end measurement, GPU stage profiling, asynchronous fence verification") }, { QStringLiteral("gpu_stage_timing_method"), QStringLiteral("separate profiling pass; each stage glFinish-synchronised") }, { QStringLiteral("gpu_stage_wall_profile"), QJsonObject { @@ -586,27 +833,52 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { QStringLiteral("CPU PSNR %1 dB").arg(cpu_psnr, 0, 'f', 2), QStringLiteral("GPU PSNR %1 dB").arg(gpu_psnr, 0, 'f', 2), }; - if (m_gpu_timer->is_supported()) { - Q_ASSERT(gpu_timing_tickets.size() == size_t(m_iterations)); - m_pending_gpu_report = PendingGpuReport { - .root = std::move(root), - .summary = std::move(summary), - .tickets = std::move(gpu_timing_tickets), - .query_results = std::vector>(size_t(m_iterations)), - .query_finished = std::vector(size_t(m_iterations), false), - }; - return {}; + if (!m_gpu_timer->is_supported()) { + root.insert(QStringLiteral("gpu_timer_query"), + QJsonObject { + { QStringLiteral("supported"), false }, + { QStringLiteral("status"), QStringLiteral("unsupported") }, + }); } - root.insert(QStringLiteral("gpu_timer_query"), - QJsonObject { - { QStringLiteral("supported"), false }, - { QStringLiteral("status"), QStringLiteral("unsupported") }, - }); - const auto json = QString::fromUtf8(QJsonDocument(root).toJson(QJsonDocument::Indented)); - const auto text = summary.join('\n'); - qInfo().noquote() << json; - return { text, json }; + m_pending_gpu_report = PendingGpuReport { + .root = std::move(root), + .summary = std::move(summary), + .tickets = std::move(gpu_timing_tickets), + .query_results = std::vector>(size_t(m_iterations)), + .query_finished = std::vector(size_t(m_iterations), false), + }; + if (m_gpu_timer->is_supported()) + Q_ASSERT(m_pending_gpu_report->tickets.size() == size_t(m_iterations)); + + PendingFenceDiagnostic fence_diagnostic; + fence_diagnostic.compressor = std::move(gpu_compressor); + fence_diagnostic.destination = std::move(gpu_destination); + fence_diagnostic.probe_framebuffer = std::make_unique(gl_engine::Framebuffer::DepthFormat::None, + std::vector { gl_engine::Framebuffer::ColourFormat::RGBA8 }, + glm::uvec2(unsigned(m_batch_size), 1u)); + fence_diagnostic.probe_shader = std::make_unique(R"( + void main() { + highp vec2 vertices[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); + gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0); + })", + R"( + uniform lowp sampler2DArray texture_sampler; + out lowp vec4 out_color; + void main() { + highp int layer = int(gl_FragCoord.x); + out_color = textureLod(texture_sampler, vec3(0.5, 0.5, float(layer)), 0.0); + })", + gl_engine::ShaderCodeSource::PLAINTEXT); + fence_diagnostic.probe_geometry = gl_engine::helpers::create_screen_quad_geometry(); + fence_diagnostic.sources = std::move(sources); + fence_diagnostic.layers = std::move(layers); + fence_diagnostic.settings = gpu_settings; + fence_diagnostic.settings.timing_mode = gl_engine::TextureCompressor::TimingMode::SubmissionOnly; + fence_diagnostic.iteration_count = m_iterations; + m_pending_gpu_report->fence_diagnostic.emplace(std::move(fence_diagnostic)); + begin_fence_sample(*m_pending_gpu_report->fence_diagnostic); + return {}; } QPointer m_item; diff --git a/gl_engine/Texture.cpp b/gl_engine/Texture.cpp index 377edd68..564c40f5 100644 --- a/gl_engine/Texture.cpp +++ b/gl_engine/Texture.cpp @@ -392,8 +392,13 @@ template gl_engine::TextureCompressor::StageTiming measure_g const auto start = std::chrono::steady_clock::now(); std::forward(callable)(); const auto submitted = std::chrono::steady_clock::now(); - if (finish) - QOpenGLContext::currentContext()->extraFunctions()->glFinish(); + if (!finish) { + return { + std::chrono::duration(submitted - start).count(), + 0.0, + }; + } + QOpenGLContext::currentContext()->extraFunctions()->glFinish(); const auto completed = std::chrono::steady_clock::now(); return { std::chrono::duration(submitted - start).count(), @@ -902,6 +907,7 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: Result result; result.mip_levels = settings.generate_mipmaps ? mip_level_count(m->width, m->height) : 1; const bool finish_stages = settings.timing_mode == TimingMode::IndividualStages; + const bool finish_total = settings.timing_mode == TimingMode::EndToEnd; const auto total_start = std::chrono::steady_clock::now(); auto* gpu_timer = settings.gpu_timer && settings.gpu_timer->is_supported() ? settings.gpu_timer : nullptr; if (gpu_timer) @@ -1119,7 +1125,7 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: if (gpu_timer) gpu_timer->end_sample(); f->glActiveTexture(GL_TEXTURE0); - if (!finish_stages) + if (finish_total) f->glFinish(); result.timings.total_ms = std::chrono::duration(std::chrono::steady_clock::now() - total_start).count(); return result; diff --git a/gl_engine/Texture.h b/gl_engine/Texture.h index 116fb0f6..f7007ebc 100644 --- a/gl_engine/Texture.h +++ b/gl_engine/Texture.h @@ -93,7 +93,7 @@ class Texture { class TextureCompressor { public: enum class Backend { FragmentShader, TransformFeedback }; - enum class TimingMode { EndToEnd, IndividualStages }; + enum class TimingMode { EndToEnd, IndividualStages, SubmissionOnly }; struct GpuTimings { double scratch_upload_ms = 0.0; @@ -150,8 +150,8 @@ class TextureCompressor { }; struct Timings { - // total_ms always includes one completion wait. Individual stage values only include - // completion waits when Settings::timing_mode is IndividualStages. + // EndToEnd includes one final completion wait. IndividualStages includes a completion + // wait per stage. SubmissionOnly does not wait for GPU completion. StageTiming scratch_upload; StageTiming mipmap_generation; StageTiming compression_pass; diff --git a/unittests/gl_engine/texture.cpp b/unittests/gl_engine/texture.cpp index 55e3f2f6..0d444e0e 100644 --- a/unittests/gl_engine/texture.cpp +++ b/unittests/gl_engine/texture.cpp @@ -737,6 +737,22 @@ TEST_CASE("gl texture GPU compression quality") CHECK(profiled_result.timings.total_ms > 0.0); CHECK(f->glGetError() == GL_NO_ERROR); + const auto submitted_result = compressor.compress(sources, + destination, + destination_layers, + { .algorithm = gl_engine::Texture::compression_algorithm(), + .effort = 4, + .generate_mipmaps = true, + .timing_mode = gl_engine::TextureCompressor::TimingMode::SubmissionOnly }); + CHECK(submitted_result.timings.scratch_upload.completion_wait_ms == 0.0); + CHECK(submitted_result.timings.mipmap_generation.completion_wait_ms == 0.0); + CHECK(submitted_result.timings.compression_pass.completion_wait_ms == 0.0); + CHECK(submitted_result.timings.packing_pass.completion_wait_ms == 0.0); + CHECK(submitted_result.timings.output_transfer.completion_wait_ms == 0.0); + CHECK(submitted_result.timings.compressed_upload.completion_wait_ms == 0.0); + f->glFinish(); + CHECK(f->glGetError() == GL_NO_ERROR); + Framebuffer framebuffer(Framebuffer::DepthFormat::None, { Framebuffer::ColourFormat::RGBA8 }, { resolution, resolution }); framebuffer.bind(); ShaderProgram shader = create_debug_shader(R"( From e780dc40ac5f4eaeb6a4e5e180d1f4c5db5c59be Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:33:44 +0200 Subject: [PATCH 09/38] Remove transform feedback texture compression --- .../BenchmarkItem.cpp | 27 +- .../BenchmarkItem.h | 7 - apps/texture_compression_benchmark/Main.qml | 9 +- gl_engine/CMakeLists.txt | 1 - gl_engine/ShaderProgram.cpp | 12 +- gl_engine/ShaderProgram.h | 4 +- gl_engine/Texture.cpp | 250 +++++++----------- gl_engine/Texture.h | 3 - gl_engine/shaders/texture_compress.frag | 3 - gl_engine/shaders/texture_compress.vert | 16 -- 10 files changed, 97 insertions(+), 235 deletions(-) delete mode 100644 gl_engine/shaders/texture_compress.frag diff --git a/apps/texture_compression_benchmark/BenchmarkItem.cpp b/apps/texture_compression_benchmark/BenchmarkItem.cpp index a8252dbf..16585e13 100644 --- a/apps/texture_compression_benchmark/BenchmarkItem.cpp +++ b/apps/texture_compression_benchmark/BenchmarkItem.cpp @@ -291,7 +291,6 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { m_batch_size = benchmark_item->m_batch_size; m_iterations = benchmark_item->m_iterations; m_mipmaps = benchmark_item->m_mipmaps; - m_backend = benchmark_item->m_backend; m_pending = true; } @@ -622,11 +621,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { } const auto algorithm = gl_engine::Texture::compression_algorithm(); - const auto backend = m_backend == 0 ? gl_engine::TextureCompressor::Backend::FragmentShader - : gl_engine::TextureCompressor::Backend::TransformFeedback; - const auto backend_name = backend == gl_engine::TextureCompressor::Backend::FragmentShader - ? QStringLiteral("Fragment shader + PBO") - : QStringLiteral("Transform feedback"); + const auto backend_name = QStringLiteral("Fragment shader + PBO"); const auto filter = m_mipmaps ? gl_engine::Texture::Filter::MipMapLinear : gl_engine::Texture::Filter::Linear; gl_engine::Texture cpu_destination(gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); cpu_destination.setParams(filter, gl_engine::Texture::Filter::Linear); @@ -651,7 +646,6 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { .algorithm = algorithm, .effort = unsigned(m_effort), .generate_mipmaps = m_mipmaps, - .backend = backend, .timing_mode = gl_engine::TextureCompressor::TimingMode::EndToEnd, }; @@ -673,7 +667,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { gpu_total_times.reserve(size_t(m_iterations)); // Keep CPU and GPU phases separate: mobile CPU frequency and thermal state are shared - // with the GPU, so interleaving them makes the CPU result backend-dependent. + // with the GPU, so interleaving them makes the CPU result workload-dependent. for (int iteration = 0; iteration < warmup_iterations; ++iteration) { auto compressed = cpu_compress(sources, algorithm, m_mipmaps); static_cast(upload_cpu(compressed)); @@ -888,7 +882,6 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { int m_batch_size = 4; int m_iterations = 10; bool m_mipmaps = true; - int m_backend = 0; bool m_pending = false; std::unique_ptr m_gpu_timer; std::optional m_pending_gpu_report; @@ -941,22 +934,6 @@ void BenchmarkItem::setMipmaps(bool value) emit mipmapsChanged(); } -int BenchmarkItem::backend() const { return m_backend; } -void BenchmarkItem::setBackend(int value) -{ - if (value < 0 || value > 1 || (value == 1 && !transformFeedbackSupported())) - return; - if (m_backend == value) - return; - m_backend = value; - emit backendChanged(); -} - -bool BenchmarkItem::transformFeedbackSupported() const -{ - return gl_engine::TextureCompressor::is_backend_supported(gl_engine::TextureCompressor::Backend::TransformFeedback); -} - bool BenchmarkItem::running() const { return m_running; } QString BenchmarkItem::resultText() const { return m_result_text; } QString BenchmarkItem::resultJson() const { return m_result_json; } diff --git a/apps/texture_compression_benchmark/BenchmarkItem.h b/apps/texture_compression_benchmark/BenchmarkItem.h index 431cddae..1608074c 100644 --- a/apps/texture_compression_benchmark/BenchmarkItem.h +++ b/apps/texture_compression_benchmark/BenchmarkItem.h @@ -17,8 +17,6 @@ class BenchmarkItem : public QQuickFramebufferObject { Q_PROPERTY(int batchSize READ batchSize WRITE setBatchSize NOTIFY batchSizeChanged) Q_PROPERTY(int iterations READ iterations WRITE setIterations NOTIFY iterationsChanged) Q_PROPERTY(bool mipmaps READ mipmaps WRITE setMipmaps NOTIFY mipmapsChanged) - Q_PROPERTY(int backend READ backend WRITE setBackend NOTIFY backendChanged) - Q_PROPERTY(bool transformFeedbackSupported READ transformFeedbackSupported CONSTANT) Q_PROPERTY(bool running READ running NOTIFY runningChanged) Q_PROPERTY(QString resultText READ resultText NOTIFY resultTextChanged) Q_PROPERTY(QString resultJson READ resultJson NOTIFY resultJsonChanged) @@ -35,9 +33,6 @@ class BenchmarkItem : public QQuickFramebufferObject { void setIterations(int value); [[nodiscard]] bool mipmaps() const; void setMipmaps(bool value); - [[nodiscard]] int backend() const; - void setBackend(int value); - [[nodiscard]] bool transformFeedbackSupported() const; [[nodiscard]] bool running() const; [[nodiscard]] QString resultText() const; [[nodiscard]] QString resultJson() const; @@ -50,7 +45,6 @@ class BenchmarkItem : public QQuickFramebufferObject { void batchSizeChanged(); void iterationsChanged(); void mipmapsChanged(); - void backendChanged(); void runningChanged(); void resultTextChanged(); void resultJsonChanged(); @@ -63,7 +57,6 @@ class BenchmarkItem : public QQuickFramebufferObject { int m_batch_size = 4; int m_iterations = 10; bool m_mipmaps = true; - int m_backend = 0; bool m_running = false; unsigned m_request_serial = 0; QString m_result_text = QStringLiteral("Run the benchmark to collect results."); diff --git a/apps/texture_compression_benchmark/Main.qml b/apps/texture_compression_benchmark/Main.qml index dd42555f..a79e67d4 100644 --- a/apps/texture_compression_benchmark/Main.qml +++ b/apps/texture_compression_benchmark/Main.qml @@ -103,14 +103,9 @@ ApplicationWindow { text: qsTr("GPU encoder") } - ComboBox { + Label { Layout.preferredWidth: 220 - model: benchmark.transformFeedbackSupported - ? [qsTr("Fragment shader + PBO"), qsTr("Transform feedback")] - : [qsTr("Fragment shader + PBO")] - currentIndex: benchmark.backend - enabled: !benchmark.running && benchmark.transformFeedbackSupported - onActivated: benchmark.backend = currentIndex + text: qsTr("Fragment shader + PBO") } } diff --git a/gl_engine/CMakeLists.txt b/gl_engine/CMakeLists.txt index 6cecfd77..d595e26a 100644 --- a/gl_engine/CMakeLists.txt +++ b/gl_engine/CMakeLists.txt @@ -87,7 +87,6 @@ qt_add_resources(gl_engine "shaders" shaders/tile_id.glsl shaders/track.frag shaders/track.vert - shaders/texture_compress.frag shaders/texture_compress_pack.frag shaders/texture_compress_raster.vert shaders/texture_compress.vert diff --git a/gl_engine/ShaderProgram.cpp b/gl_engine/ShaderProgram.cpp index 059b5cbb..ec4e792d 100644 --- a/gl_engine/ShaderProgram.cpp +++ b/gl_engine/ShaderProgram.cpp @@ -180,13 +180,11 @@ QString ShaderProgram::read_file_content_local(const QString& name) { ShaderProgram::ShaderProgram(QString vertex_shader, QString fragment_shader, ShaderCodeSource code_source, - const std::vector& defines, - const std::vector& transform_feedback_varyings) + const std::vector& defines) : m_vertex_shader(vertex_shader) , m_fragment_shader(fragment_shader) , m_code_source(code_source) , m_defines(defines) - , m_transform_feedback_varyings(transform_feedback_varyings) { reload(); Q_ASSERT(m_q_shader_program); @@ -334,14 +332,6 @@ void ShaderProgram::reload() } else if (!program->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentCode)) { outputMeaningfullErrors(program->log(), fragmentCode, m_fragment_shader); } else { - if (!m_transform_feedback_varyings.empty()) { - std::vector varyings; - varyings.reserve(m_transform_feedback_varyings.size()); - for (const auto& varying : m_transform_feedback_varyings) - varyings.push_back(varying.constData()); - QOpenGLContext::currentContext()->extraFunctions()->glTransformFeedbackVaryings( - program->programId(), GLsizei(varyings.size()), varyings.data(), GL_INTERLEAVED_ATTRIBS); - } if (!program->link()) { #ifdef _MSC_VER // when using msvc in github ci qDebug/Critical don't print when an assert fails diff --git a/gl_engine/ShaderProgram.h b/gl_engine/ShaderProgram.h index cdd7cbdb..5f812a78 100644 --- a/gl_engine/ShaderProgram.h +++ b/gl_engine/ShaderProgram.h @@ -56,7 +56,6 @@ class ShaderProgram { QString m_fragment_shader; // either filename or native shader code ShaderCodeSource m_code_source; std::vector m_defines; - std::vector m_transform_feedback_varyings; #if ALP_ENABLE_SHADER_NETWORK_HOTRELOAD // A temporary cache for the downloaded shader files. @@ -88,8 +87,7 @@ class ShaderProgram { ShaderProgram(QString vertex_shader, QString fragment_shader, ShaderCodeSource code_source = ShaderCodeSource::FILE, - const std::vector& defines = {}, - const std::vector& transform_feedback_varyings = {}); + const std::vector& defines = {}); int attribute_location(const std::string& name); void bind(); diff --git a/gl_engine/Texture.cpp b/gl_engine/Texture.cpp index 564c40f5..20ed088a 100644 --- a/gl_engine/Texture.cpp +++ b/gl_engine/Texture.cpp @@ -688,12 +688,9 @@ struct gl_engine::TextureCompressor::Impl { GLuint encoded_texture = 0; GLuint encoded_buffer = 0; GLuint vertex_array = 0; - GLuint transform_feedback = 0; GLuint encoding_framebuffer = 0; GLuint packing_framebuffer = 0; GLuint packing_renderbuffer = 0; - std::unique_ptr dxt1_transform_program; - std::unique_ptr etc1_transform_program; std::unique_ptr dxt1_fragment_program; std::unique_ptr etc1_fragment_program; std::unique_ptr packing_program; @@ -773,40 +770,24 @@ struct gl_engine::TextureCompressor::Impl { dxt1_fragment_program = std::make_unique("texture_compress_raster.vert", "texture_compress.vert", - ShaderCodeSource::FILE, - std::vector { QStringLiteral("#define ALP_FRAGMENT_COMPRESSION") }); + ShaderCodeSource::FILE); etc1_fragment_program = std::make_unique("texture_compress_raster.vert", "texture_compress.vert", ShaderCodeSource::FILE, - std::vector { QStringLiteral("#define ALP_FRAGMENT_COMPRESSION"), QStringLiteral("#define ALP_COMPRESS_ETC1") }); + std::vector { QStringLiteral("#define ALP_COMPRESS_ETC1") }); packing_program = std::make_unique( "texture_compress_raster.vert", "texture_compress_pack.frag", ShaderCodeSource::FILE); -#if !defined(__EMSCRIPTEN__) - f->glGenTransformFeedbacks(1, &transform_feedback); - const std::vector varyings { QByteArrayLiteral("encoded_block") }; - dxt1_transform_program = std::make_unique( - "texture_compress.vert", "texture_compress.frag", ShaderCodeSource::FILE, std::vector {}, varyings); - etc1_transform_program = std::make_unique("texture_compress.vert", - "texture_compress.frag", - ShaderCodeSource::FILE, - std::vector { QStringLiteral("#define ALP_COMPRESS_ETC1") }, - varyings); -#endif } ~Impl() { - dxt1_transform_program.reset(); - etc1_transform_program.reset(); dxt1_fragment_program.reset(); etc1_fragment_program.reset(); packing_program.reset(); if (!QOpenGLContext::currentContext()) return; auto* f = QOpenGLContext::currentContext()->extraFunctions(); - if (transform_feedback) - f->glDeleteTransformFeedbacks(1, &transform_feedback); f->glDeleteFramebuffers(1, &encoding_framebuffer); f->glDeleteFramebuffers(1, &packing_framebuffer); f->glDeleteRenderbuffers(1, &packing_renderbuffer); @@ -873,15 +854,6 @@ bool gl_engine::TextureCompressor::is_supported() #endif } -bool gl_engine::TextureCompressor::is_backend_supported(Backend backend) -{ -#if defined(__EMSCRIPTEN__) - return backend == Backend::FragmentShader; -#else - return backend == Backend::FragmentShader || backend == Backend::TransformFeedback; -#endif -} - gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std::span> textures, Texture& destination, std::span destination_layers, @@ -896,7 +868,6 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: Q_ASSERT(destination.m_width == m->width && destination.m_height == m->height); Q_ASSERT(settings.algorithm == Texture::compression_algorithm()); Q_ASSERT(settings.effort <= 10); - Q_ASSERT(is_backend_supported(settings.backend)); for (size_t i = 0; i < textures.size(); ++i) { Q_ASSERT(unsigned(textures[i].width()) == m->width && unsigned(textures[i].height()) == m->height); Q_ASSERT(destination_layers[i] < destination.m_n_layers); @@ -966,136 +937,97 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: } result.encoded_bytes = total_encoded_size; - if (settings.backend == Backend::TransformFeedback) { - result.timings.compression_pass = measure_stage(GpuTimer::Stage::CompressionPass, [&]() { - auto* program = settings.algorithm == nucleus::utils::ColourTexture::Format::DXT1 ? m->dxt1_transform_program.get() : m->etc1_transform_program.get(); - program->bind(); - program->set_uniform("source_texture", 7); - program->set_uniform("effort", int(settings.effort)); - f->glActiveTexture(GL_TEXTURE7); - f->glBindTexture(GL_TEXTURE_2D_ARRAY, m->scratch_texture); - f->glBindVertexArray(m->vertex_array); - f->glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, m->transform_feedback); - f->glEnable(GL_RASTERIZER_DISCARD); - - for (unsigned level = 0; level < result.mip_levels; ++level) { - const auto level_width = std::max(1u, m->width >> level); - const auto level_height = std::max(1u, m->height >> level); - const auto level_size = compressed_level_size(level_width, level_height) * textures.size(); - program->set_uniform("texture_width", int(level_width)); - program->set_uniform("texture_height", int(level_height)); - program->set_uniform("blocks_x", level_blocks_x[level]); - program->set_uniform("blocks_y", level_blocks_y[level]); - program->set_uniform("mip_level", int(level)); - f->glBindBufferRange(GL_TRANSFORM_FEEDBACK_BUFFER, - 0, - m->encoded_buffer, - GLintptr(level_offsets[level]), - GLsizeiptr(level_size)); - f->glBeginTransformFeedback(GL_POINTS); - f->glDrawArrays(GL_POINTS, 0, GLsizei(level_blocks_x[level] * level_blocks_y[level] * int(textures.size()))); - f->glEndTransformFeedback(); - } + GLint previous_draw_framebuffer = 0; + GLint previous_viewport[4] = {}; + GLboolean previous_colour_mask[4] = {}; + GLboolean blend_enabled = GL_FALSE; + GLboolean cull_enabled = GL_FALSE; + GLboolean depth_enabled = GL_FALSE; + GLboolean scissor_enabled = GL_FALSE; - f->glDisable(GL_RASTERIZER_DISCARD); - f->glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, 0); - f->glBindVertexArray(0); - program->release(); - }); - result.timings.encoding = result.timings.compression_pass; - } else { - GLint previous_draw_framebuffer = 0; - GLint previous_viewport[4] = {}; - GLboolean previous_colour_mask[4] = {}; - GLboolean blend_enabled = GL_FALSE; - GLboolean cull_enabled = GL_FALSE; - GLboolean depth_enabled = GL_FALSE; - GLboolean scissor_enabled = GL_FALSE; - - result.timings.compression_pass = measure_stage(GpuTimer::Stage::CompressionPass, [&]() { - auto* program = settings.algorithm == nucleus::utils::ColourTexture::Format::DXT1 ? m->dxt1_fragment_program.get() : m->etc1_fragment_program.get(); - f->glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previous_draw_framebuffer); - f->glGetIntegerv(GL_VIEWPORT, previous_viewport); - f->glGetBooleanv(GL_COLOR_WRITEMASK, previous_colour_mask); - blend_enabled = f->glIsEnabled(GL_BLEND); - cull_enabled = f->glIsEnabled(GL_CULL_FACE); - depth_enabled = f->glIsEnabled(GL_DEPTH_TEST); - scissor_enabled = f->glIsEnabled(GL_SCISSOR_TEST); - - f->glDisable(GL_BLEND); - f->glDisable(GL_CULL_FACE); - f->glDisable(GL_DEPTH_TEST); - f->glDisable(GL_SCISSOR_TEST); - f->glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); - - f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m->encoding_framebuffer); - f->glViewport(0, 0, m->block_atlas_width, m->block_atlas_height); - program->bind(); - program->set_uniform("source_texture", 7); - program->set_uniform("texture_width", int(m->width)); - program->set_uniform("texture_height", int(m->height)); - program->set_uniform("effort", int(settings.effort)); - program->set_uniform("atlas_width", int(m->block_atlas_width)); - program->set_uniform("total_blocks", int(total_encoded_size / 8)); - program->set_uniform("mip_levels", int(result.mip_levels)); - program->set_uniform_array("level_offsets", level_offsets_blocks); - program->set_uniform_array("level_blocks_x", level_blocks_x); - program->set_uniform_array("level_blocks_y", level_blocks_y); - f->glActiveTexture(GL_TEXTURE7); - f->glBindTexture(GL_TEXTURE_2D_ARRAY, m->scratch_texture); - f->glBindVertexArray(m->vertex_array); - f->glDrawArrays(GL_TRIANGLES, 0, 3); - }); + result.timings.compression_pass = measure_stage(GpuTimer::Stage::CompressionPass, [&]() { + auto* program = settings.algorithm == nucleus::utils::ColourTexture::Format::DXT1 ? m->dxt1_fragment_program.get() : m->etc1_fragment_program.get(); + f->glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previous_draw_framebuffer); + f->glGetIntegerv(GL_VIEWPORT, previous_viewport); + f->glGetBooleanv(GL_COLOR_WRITEMASK, previous_colour_mask); + blend_enabled = f->glIsEnabled(GL_BLEND); + cull_enabled = f->glIsEnabled(GL_CULL_FACE); + depth_enabled = f->glIsEnabled(GL_DEPTH_TEST); + scissor_enabled = f->glIsEnabled(GL_SCISSOR_TEST); + + f->glDisable(GL_BLEND); + f->glDisable(GL_CULL_FACE); + f->glDisable(GL_DEPTH_TEST); + f->glDisable(GL_SCISSOR_TEST); + f->glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + + f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m->encoding_framebuffer); + f->glViewport(0, 0, m->block_atlas_width, m->block_atlas_height); + program->bind(); + program->set_uniform("source_texture", 7); + program->set_uniform("texture_width", int(m->width)); + program->set_uniform("texture_height", int(m->height)); + program->set_uniform("effort", int(settings.effort)); + program->set_uniform("atlas_width", int(m->block_atlas_width)); + program->set_uniform("total_blocks", int(total_encoded_size / 8)); + program->set_uniform("mip_levels", int(result.mip_levels)); + program->set_uniform_array("level_offsets", level_offsets_blocks); + program->set_uniform_array("level_blocks_x", level_blocks_x); + program->set_uniform_array("level_blocks_y", level_blocks_y); + f->glActiveTexture(GL_TEXTURE7); + f->glBindTexture(GL_TEXTURE_2D_ARRAY, m->scratch_texture); + f->glBindVertexArray(m->vertex_array); + f->glDrawArrays(GL_TRIANGLES, 0, 3); + }); - result.timings.packing_pass = measure_stage(GpuTimer::Stage::PackingPass, [&]() { - f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m->packing_framebuffer); - f->glViewport(0, 0, m->output_atlas_width, m->output_atlas_height); - m->packing_program->bind(); - m->packing_program->set_uniform("encoded_blocks", 6); - m->packing_program->set_uniform("block_atlas_width", int(m->block_atlas_width)); - m->packing_program->set_uniform("output_atlas_width", int(m->output_atlas_width)); - m->packing_program->set_uniform("total_blocks", int(total_encoded_size / 8)); - f->glActiveTexture(GL_TEXTURE6); - f->glBindTexture(GL_TEXTURE_2D, m->encoded_texture); - f->glDrawArrays(GL_TRIANGLES, 0, 3); - f->glBindVertexArray(0); - m->packing_program->release(); - - if (blend_enabled) - f->glEnable(GL_BLEND); - if (cull_enabled) - f->glEnable(GL_CULL_FACE); - if (depth_enabled) - f->glEnable(GL_DEPTH_TEST); - if (scissor_enabled) - f->glEnable(GL_SCISSOR_TEST); - f->glColorMask(previous_colour_mask[0], previous_colour_mask[1], previous_colour_mask[2], previous_colour_mask[3]); - f->glViewport(previous_viewport[0], previous_viewport[1], previous_viewport[2], previous_viewport[3]); - f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, GLuint(previous_draw_framebuffer)); - }); - result.timings.encoding.submission_ms - = result.timings.compression_pass.submission_ms + result.timings.packing_pass.submission_ms; - result.timings.encoding.completion_wait_ms - = result.timings.compression_pass.completion_wait_ms + result.timings.packing_pass.completion_wait_ms; - - result.timings.output_transfer = measure_stage(GpuTimer::Stage::OutputTransfer, [&]() { - GLint previous_read_framebuffer = 0; - GLint previous_read_buffer = 0; - GLint previous_pack_alignment = 0; - f->glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &previous_read_framebuffer); - f->glGetIntegerv(GL_READ_BUFFER, &previous_read_buffer); - f->glGetIntegerv(GL_PACK_ALIGNMENT, &previous_pack_alignment); - f->glBindFramebuffer(GL_READ_FRAMEBUFFER, m->packing_framebuffer); - f->glReadBuffer(GL_COLOR_ATTACHMENT0); - f->glPixelStorei(GL_PACK_ALIGNMENT, 1); - f->glBindBuffer(GL_PIXEL_PACK_BUFFER, m->encoded_buffer); - f->glReadPixels(0, 0, m->output_atlas_width, m->output_atlas_height, GL_RGBA_INTEGER, GL_UNSIGNED_BYTE, nullptr); - f->glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); - f->glPixelStorei(GL_PACK_ALIGNMENT, previous_pack_alignment); - f->glBindFramebuffer(GL_READ_FRAMEBUFFER, GLuint(previous_read_framebuffer)); - f->glReadBuffer(GLenum(previous_read_buffer)); - }); - } + result.timings.packing_pass = measure_stage(GpuTimer::Stage::PackingPass, [&]() { + f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m->packing_framebuffer); + f->glViewport(0, 0, m->output_atlas_width, m->output_atlas_height); + m->packing_program->bind(); + m->packing_program->set_uniform("encoded_blocks", 6); + m->packing_program->set_uniform("block_atlas_width", int(m->block_atlas_width)); + m->packing_program->set_uniform("output_atlas_width", int(m->output_atlas_width)); + m->packing_program->set_uniform("total_blocks", int(total_encoded_size / 8)); + f->glActiveTexture(GL_TEXTURE6); + f->glBindTexture(GL_TEXTURE_2D, m->encoded_texture); + f->glDrawArrays(GL_TRIANGLES, 0, 3); + f->glBindVertexArray(0); + m->packing_program->release(); + + if (blend_enabled) + f->glEnable(GL_BLEND); + if (cull_enabled) + f->glEnable(GL_CULL_FACE); + if (depth_enabled) + f->glEnable(GL_DEPTH_TEST); + if (scissor_enabled) + f->glEnable(GL_SCISSOR_TEST); + f->glColorMask(previous_colour_mask[0], previous_colour_mask[1], previous_colour_mask[2], previous_colour_mask[3]); + f->glViewport(previous_viewport[0], previous_viewport[1], previous_viewport[2], previous_viewport[3]); + f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, GLuint(previous_draw_framebuffer)); + }); + result.timings.encoding.submission_ms + = result.timings.compression_pass.submission_ms + result.timings.packing_pass.submission_ms; + result.timings.encoding.completion_wait_ms + = result.timings.compression_pass.completion_wait_ms + result.timings.packing_pass.completion_wait_ms; + + result.timings.output_transfer = measure_stage(GpuTimer::Stage::OutputTransfer, [&]() { + GLint previous_read_framebuffer = 0; + GLint previous_read_buffer = 0; + GLint previous_pack_alignment = 0; + f->glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &previous_read_framebuffer); + f->glGetIntegerv(GL_READ_BUFFER, &previous_read_buffer); + f->glGetIntegerv(GL_PACK_ALIGNMENT, &previous_pack_alignment); + f->glBindFramebuffer(GL_READ_FRAMEBUFFER, m->packing_framebuffer); + f->glReadBuffer(GL_COLOR_ATTACHMENT0); + f->glPixelStorei(GL_PACK_ALIGNMENT, 1); + f->glBindBuffer(GL_PIXEL_PACK_BUFFER, m->encoded_buffer); + f->glReadPixels(0, 0, m->output_atlas_width, m->output_atlas_height, GL_RGBA_INTEGER, GL_UNSIGNED_BYTE, nullptr); + f->glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + f->glPixelStorei(GL_PACK_ALIGNMENT, previous_pack_alignment); + f->glBindFramebuffer(GL_READ_FRAMEBUFFER, GLuint(previous_read_framebuffer)); + f->glReadBuffer(GLenum(previous_read_buffer)); + }); result.timings.compressed_upload = measure_stage(GpuTimer::Stage::CompressedUpload, [&]() { f->glBindTexture(GL_TEXTURE_2D_ARRAY, destination.m_id); diff --git a/gl_engine/Texture.h b/gl_engine/Texture.h index f7007ebc..c90d5d1b 100644 --- a/gl_engine/Texture.h +++ b/gl_engine/Texture.h @@ -92,7 +92,6 @@ class Texture { class TextureCompressor { public: - enum class Backend { FragmentShader, TransformFeedback }; enum class TimingMode { EndToEnd, IndividualStages, SubmissionOnly }; struct GpuTimings { @@ -137,7 +136,6 @@ class TextureCompressor { nucleus::utils::ColourTexture::Format algorithm = nucleus::utils::ColourTexture::Format::DXT1; unsigned effort = 0; bool generate_mipmaps = true; - Backend backend = Backend::FragmentShader; TimingMode timing_mode = TimingMode::EndToEnd; GpuTimer* gpu_timer = nullptr; }; @@ -184,7 +182,6 @@ class TextureCompressor { [[nodiscard]] static size_t compressed_level_size(unsigned width, unsigned height); [[nodiscard]] static unsigned mip_level_count(unsigned width, unsigned height); [[nodiscard]] static bool is_supported(); - [[nodiscard]] static bool is_backend_supported(Backend backend); private: struct Impl; diff --git a/gl_engine/shaders/texture_compress.frag b/gl_engine/shaders/texture_compress.frag deleted file mode 100644 index 91981030..00000000 --- a/gl_engine/shaders/texture_compress.frag +++ /dev/null @@ -1,3 +0,0 @@ -void main() -{ -} diff --git a/gl_engine/shaders/texture_compress.vert b/gl_engine/shaders/texture_compress.vert index 2643f3a6..ef6c1a6d 100644 --- a/gl_engine/shaders/texture_compress.vert +++ b/gl_engine/shaders/texture_compress.vert @@ -1,7 +1,6 @@ uniform highp sampler2DArray source_texture; uniform highp int texture_width; uniform highp int texture_height; -#ifdef ALP_FRAGMENT_COMPRESSION const highp int max_mip_levels = 16; uniform highp int atlas_width; uniform highp int total_blocks; @@ -10,12 +9,6 @@ uniform highp int level_offsets[max_mip_levels]; uniform highp int level_blocks_x[max_mip_levels]; uniform highp int level_blocks_y[max_mip_levels]; layout(location = 0) out highp uvec2 encoded_block; -#else -uniform highp int blocks_x; -uniform highp int blocks_y; -uniform highp int mip_level; -flat out highp uvec2 encoded_block; -#endif uniform highp int effort; highp uvec3 unpack_565(highp uint value) @@ -196,7 +189,6 @@ highp uvec2 compress_block(highp ivec2 block, void main() { -#ifdef ALP_FRAGMENT_COMPRESSION highp int output_index = int(gl_FragCoord.y) * atlas_width + int(gl_FragCoord.x); if (output_index >= total_blocks) discard; @@ -216,12 +208,4 @@ void main() highp int block_index = level_index - layer * blocks_per_layer; highp ivec2 block = ivec2(block_index % blocks_x_at_level, block_index / blocks_x_at_level); encoded_block = compress_block(block, layer, level, max(1, texture_width >> level), max(1, texture_height >> level)); -#else - highp int blocks_per_layer = blocks_x * blocks_y; - highp int layer = gl_VertexID / blocks_per_layer; - highp int block_index = gl_VertexID - layer * blocks_per_layer; - highp ivec2 block = ivec2(block_index % blocks_x, block_index / blocks_x); - encoded_block = compress_block(block, layer, mip_level, texture_width, texture_height); - gl_Position = vec4(0.0); -#endif } From e74ed623305e768d68c2b4eb3c6ab391421fbed1 Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Wed, 19 Aug 2026 19:08:15 +0200 Subject: [PATCH 10/38] Use compact texture compression atlases --- gl_engine/Texture.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gl_engine/Texture.cpp b/gl_engine/Texture.cpp index 20ed088a..b4959368 100644 --- a/gl_engine/Texture.cpp +++ b/gl_engine/Texture.cpp @@ -715,7 +715,7 @@ struct gl_engine::TextureCompressor::Impl { f->glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &maximum_renderbuffer_size); f->glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maximum_texture_size); const auto atlas_size = [](size_t pixels, GLint maximum_dimension) { - const auto atlas_width = GLsizei(std::min(pixels, size_t(maximum_dimension))); + const auto atlas_width = GLsizei(std::min({ pixels, size_t(maximum_dimension), size_t(256) })); const auto atlas_height = GLsizei((pixels + size_t(atlas_width) - 1) / size_t(atlas_width)); Q_ASSERT(atlas_width > 0 && atlas_height > 0 && atlas_height <= maximum_dimension); return std::pair(atlas_width, atlas_height); From dccd4a7bb43e5b93a8b04eb7fffd54acd8808649 Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Wed, 19 Aug 2026 23:32:26 +0200 Subject: [PATCH 11/38] Revise texture compression benchmark dataset --- .../BenchmarkItem.cpp | 441 +++++++++++++----- .../BenchmarkItem.h | 36 +- .../CMakeLists.txt | 9 +- apps/texture_compression_benchmark/Main.qml | 96 ++-- 4 files changed, 402 insertions(+), 180 deletions(-) diff --git a/apps/texture_compression_benchmark/BenchmarkItem.cpp b/apps/texture_compression_benchmark/BenchmarkItem.cpp index 16585e13..d5d431fa 100644 --- a/apps/texture_compression_benchmark/BenchmarkItem.cpp +++ b/apps/texture_compression_benchmark/BenchmarkItem.cpp @@ -6,18 +6,24 @@ #include "BenchmarkItem.h" +#include #include #include #include #include #include #include +#include +#include +#include #include #include #include #include #include +#include #include +#include #include #include #include @@ -42,6 +48,62 @@ namespace { using Raster = radix::Raster; using Clock = std::chrono::steady_clock; +struct TileGroup { + const char* name; + const char* category; + int zoom; + int y; + int x; +}; + +constexpr std::array tile_groups { { + { "Vienna", "city", 17, 45448, 71496 }, + { "Salzburg", "city", 16, 22832, 35144 }, + { "Graz", "city", 16, 23030, 35578 }, + { "Neusiedler See", "lake", 13, 2852, 4476 }, + { "Attersee", "lake", 14, 5702, 8808 }, + { "Wörthersee", "lake", 15, 11574, 17670 }, + { "Grossglockner", "mountain", 16, 23030, 35078 }, + { "Dachstein", "mountain", 15, 11460, 17622 }, + { "Arlberg", "mountain", 14, 5752, 8656 }, + { "Ötztal", "mountain", 16, 23084, 34746 }, + { "Wienerwald", "forest", 14, 5684, 8926 }, + { "Kalkalpen", "forest", 15, 11418, 17692 }, + { "Bregenzerwald", "forest", 16, 22956, 34570 }, + { "Marchfeld", "fields", 15, 11358, 17904 }, + { "Burgenland", "fields", 16, 22910, 35770 }, + { "Weinviertel", "fields", 14, 5656, 8938 }, +} }; + +QString tile_url(const TileGroup& group, int x_offset, int y_offset) +{ + return QStringLiteral("https://gataki.cg.tuwien.ac.at/raw/basemap/tiles/%1/%2/%3.jpeg") + .arg(group.zoom) + .arg(group.y + y_offset) + .arg(group.x + x_offset); +} + +QJsonArray dataset_json() +{ + QJsonArray result; + for (const auto& group : tile_groups) { + QJsonArray urls; + for (int y = 0; y < 2; ++y) { + for (int x = 0; x < 2; ++x) + urls.append(tile_url(group, x, y)); + } + result.append(QJsonObject { + { QStringLiteral("name"), QString::fromUtf8(group.name) }, + { QStringLiteral("category"), QString::fromLatin1(group.category) }, + { QStringLiteral("zoom"), group.zoom }, + { QStringLiteral("top_left_x"), group.x }, + { QStringLiteral("top_left_y"), group.y }, + { QStringLiteral("urls"), urls }, + }); + } + return result; +} + struct Statistics { double median = 0.0; double p95 = 0.0; @@ -113,6 +175,7 @@ struct PendingFenceDiagnostic { std::unique_ptr probe_framebuffer; std::unique_ptr probe_shader; gl_engine::helpers::ScreenQuadGeometry probe_geometry; + std::vector source_pool; std::vector sources; std::vector layers; gl_engine::TextureCompressor::Settings settings; @@ -157,7 +220,7 @@ double srgb_to_linear(uint8_t value) return std::pow((normalised + 0.055) / 1.055, 2.4); } -double linear_psnr(const QImage& reconstructed, const Raster& source) +double linear_squared_error(const QImage& reconstructed, const Raster& source) { double squared_error = 0.0; for (int y = 0; y < reconstructed.height(); ++y) { @@ -174,11 +237,23 @@ double linear_psnr(const QImage& reconstructed, const Raster& source) } } } - const auto mse = squared_error / double(reconstructed.width() * reconstructed.height() * 3); + return squared_error; +} + +double linear_psnr(std::span reconstructed, std::span sources) +{ + Q_ASSERT(reconstructed.size() == sources.size()); + double squared_error = 0.0; + uint64_t channel_count = 0; + for (size_t i = 0; i < sources.size(); ++i) { + squared_error += linear_squared_error(reconstructed[i], sources[i]); + channel_count += uint64_t(reconstructed[i].width()) * uint64_t(reconstructed[i].height()) * 3; + } + const auto mse = squared_error / double(channel_count); return mse == 0.0 ? std::numeric_limits::infinity() : 10.0 * std::log10(1.0 / mse); } -QImage reconstruct(gl_engine::Texture& texture, unsigned resolution) +QImage reconstruct(gl_engine::Texture& texture, unsigned resolution, unsigned layer) { gl_engine::Framebuffer framebuffer( gl_engine::Framebuffer::DepthFormat::None, { gl_engine::Framebuffer::ColourFormat::RGBA8 }, { resolution, resolution }); @@ -192,21 +267,41 @@ QImage reconstruct(gl_engine::Texture& texture, unsigned resolution) })", R"( uniform lowp sampler2DArray texture_sampler; + uniform highp float texture_layer; in highp vec2 texcoords; out lowp vec4 out_color; void main() { - out_color = textureLod(texture_sampler, vec3(texcoords.x, 1.0 - texcoords.y, 0.0), 0.0); + out_color = textureLod(texture_sampler, vec3(texcoords.x, 1.0 - texcoords.y, texture_layer), 0.0); })", gl_engine::ShaderCodeSource::PLAINTEXT); shader.bind(); texture.bind(0); shader.set_uniform("texture_sampler", 0); + shader.set_uniform("texture_layer", float(layer)); gl_engine::helpers::create_screen_quad_geometry().draw(); auto result = framebuffer.read_colour_attachment(0); gl_engine::Framebuffer::unbind(); return result; } +QString preview_data_url(std::span images) +{ + constexpr int columns = 4; + constexpr int tile_size = 512; + QImage preview(columns * tile_size, columns * tile_size, QImage::Format_RGBA8888); + preview.fill(Qt::black); + QPainter painter(&preview); + for (size_t i = 0; i < images.size(); ++i) + painter.drawImage(QPoint(int(i % columns) * tile_size, int(i / columns) * tile_size), images[i]); + painter.end(); + + QByteArray png; + QBuffer buffer(&png); + buffer.open(QIODevice::WriteOnly); + preview.save(&buffer, "PNG"); + return QStringLiteral("data:image/png;base64,") + QString::fromLatin1(png.toBase64()); +} + std::vector cpu_compress( std::span sources, nucleus::utils::ColourTexture::Format algorithm, bool mipmaps) { @@ -288,9 +383,9 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { return; m_seen_serial = benchmark_item->m_request_serial; m_effort = benchmark_item->m_effort; - m_batch_size = benchmark_item->m_batch_size; - m_iterations = benchmark_item->m_iterations; m_mipmaps = benchmark_item->m_mipmaps; + m_source_images = benchmark_item->m_source_images; + m_preview_source.clear(); m_pending = true; } @@ -312,9 +407,10 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { if (completed) { QPointer item = m_item; const auto [text, json] = std::move(*completed); - QMetaObject::invokeMethod(m_item, [item, text, json]() { + const auto preview_source = m_preview_source; + QMetaObject::invokeMethod(m_item, [item, text, json, preview_source]() { if (item) - item->publishResults(text, json); + item->publishResults(text, json, preview_source); }); } else { request_another_frame(); @@ -341,6 +437,10 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { void begin_fence_sample(PendingFenceDiagnostic& diagnostic) { + constexpr size_t batch_size = 4; + const auto group_offset = size_t(diagnostic.iteration % 4) * batch_size; + diagnostic.sources.assign( + diagnostic.source_pool.begin() + ptrdiff_t(group_offset), diagnostic.source_pool.begin() + ptrdiff_t(group_offset + batch_size)); diagnostic.last_source_markers.clear(); diagnostic.last_source_markers.reserve(diagnostic.sources.size()); for (size_t layer = 0; layer < diagnostic.sources.size(); ++layer) { @@ -597,15 +697,29 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { std::pair run() { constexpr unsigned resolution = 512; - QImage input(QStringLiteral(":/benchmark/merged.jpg")); - if (input.isNull()) - return { QStringLiteral("Unable to load the benchmark image."), QStringLiteral("{}") }; - input = input.convertToFormat(QImage::Format_RGBA8888).scaled( - int(resolution), int(resolution), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - const auto source = nucleus::tile::conversion::to_rgba8raster(input); - std::vector sources(size_t(m_batch_size), source); - std::vector layers(size_t(m_batch_size), 0u); + constexpr int batch_size = 4; + constexpr int batches_per_round = 4; + constexpr int measurement_rounds = 3; + constexpr int sample_count = batches_per_round * measurement_rounds; + if (m_source_images.size() < tile_groups.size()) { + const QJsonObject error { + { QStringLiteral("supported"), false }, + { QStringLiteral("error"), QStringLiteral("Benchmark imagery is incomplete") }, + }; + return { QStringLiteral("Unable to load the complete benchmark dataset."), + QString::fromUtf8(QJsonDocument(error).toJson(QJsonDocument::Indented)) }; + } + std::vector all_sources; + all_sources.reserve(tile_groups.size()); + for (const auto& image : m_source_images) + all_sources.push_back(nucleus::tile::conversion::to_rgba8raster(image)); + std::vector layers(size_t(batch_size), 0u); std::iota(layers.begin(), layers.end(), 0u); + std::vector quality_layers(tile_groups.size(), 0u); + std::iota(quality_layers.begin(), quality_layers.end(), 0u); + const auto sources_for_group = [&](int group) { + return std::span(all_sources).subspan(size_t(group * batch_size), size_t(batch_size)); + }; if (!gl_engine::TextureCompressor::is_supported()) { QJsonObject root { @@ -623,31 +737,58 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { const auto algorithm = gl_engine::Texture::compression_algorithm(); const auto backend_name = QStringLiteral("Fragment shader + PBO"); const auto filter = m_mipmaps ? gl_engine::Texture::Filter::MipMapLinear : gl_engine::Texture::Filter::Linear; - gl_engine::Texture cpu_destination(gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); - cpu_destination.setParams(filter, gl_engine::Texture::Filter::Linear); - cpu_destination.allocate_array(resolution, resolution, unsigned(m_batch_size)); - auto gpu_destination = std::make_unique( - gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); - gpu_destination->setParams(filter, gl_engine::Texture::Filter::Linear); - gpu_destination->allocate_array(resolution, resolution, unsigned(m_batch_size)); - auto gpu_compressor = std::make_unique(resolution, resolution, unsigned(m_batch_size)); - m_gpu_timer = std::make_unique(); - - auto upload_cpu = [&](const std::vector& compressed) { - const auto start = Clock::now(); - for (size_t layer = 0; layer < compressed.size(); ++layer) - cpu_destination.upload(compressed[layer], unsigned(layer)); - QOpenGLContext::currentContext()->extraFunctions()->glFinish(); - return elapsed_ms(start); - }; - - constexpr int warmup_iterations = 3; const gl_engine::TextureCompressor::Settings gpu_settings { .algorithm = algorithm, .effort = unsigned(m_effort), .generate_mipmaps = m_mipmaps, .timing_mode = gl_engine::TextureCompressor::TimingMode::EndToEnd, }; + auto upload_cpu = [&](gl_engine::Texture& destination, const std::vector& compressed) { + const auto start = Clock::now(); + for (size_t layer = 0; layer < compressed.size(); ++layer) + destination.upload(compressed[layer], unsigned(layer)); + QOpenGLContext::currentContext()->extraFunctions()->glFinish(); + return elapsed_ms(start); + }; + + // Quality is evaluated first with one untimed compression of all 16 images. + double cpu_psnr = 0.0; + double gpu_psnr = 0.0; + { + gl_engine::Texture cpu_quality_destination(gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); + cpu_quality_destination.setParams(filter, gl_engine::Texture::Filter::Linear); + cpu_quality_destination.allocate_array(resolution, resolution, unsigned(tile_groups.size())); + auto cpu_quality = cpu_compress(all_sources, algorithm, m_mipmaps); + static_cast(upload_cpu(cpu_quality_destination, cpu_quality)); + + gl_engine::Texture gpu_quality_destination(gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); + gpu_quality_destination.setParams(filter, gl_engine::Texture::Filter::Linear); + gpu_quality_destination.allocate_array(resolution, resolution, unsigned(tile_groups.size())); + gl_engine::TextureCompressor gpu_quality_compressor(resolution, resolution, unsigned(tile_groups.size())); + static_cast(gpu_quality_compressor.compress(all_sources, gpu_quality_destination, quality_layers, gpu_settings)); + + std::vector cpu_reconstructed; + std::vector gpu_reconstructed; + cpu_reconstructed.reserve(all_sources.size()); + gpu_reconstructed.reserve(all_sources.size()); + for (unsigned layer = 0; layer < all_sources.size(); ++layer) { + cpu_reconstructed.push_back(reconstruct(cpu_quality_destination, resolution, layer)); + gpu_reconstructed.push_back(reconstruct(gpu_quality_destination, resolution, layer)); + } + cpu_psnr = linear_psnr(cpu_reconstructed, all_sources); + gpu_psnr = linear_psnr(gpu_reconstructed, all_sources); + m_preview_source = preview_data_url(gpu_reconstructed); + } + + gl_engine::Texture cpu_destination(gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); + cpu_destination.setParams(filter, gl_engine::Texture::Filter::Linear); + cpu_destination.allocate_array(resolution, resolution, batch_size); + auto gpu_destination = std::make_unique( + gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); + gpu_destination->setParams(filter, gl_engine::Texture::Filter::Linear); + gpu_destination->allocate_array(resolution, resolution, batch_size); + auto gpu_compressor = std::make_unique(resolution, resolution, batch_size); + m_gpu_timer = std::make_unique(); std::vector cpu_compression_times; std::vector cpu_upload_times; @@ -661,35 +802,38 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { WallTimingSamples gpu_compressed_upload_times; std::vector gpu_total_times; std::vector gpu_timing_tickets; - cpu_compression_times.reserve(size_t(m_iterations)); - cpu_upload_times.reserve(size_t(m_iterations)); - cpu_total_times.reserve(size_t(m_iterations)); - gpu_total_times.reserve(size_t(m_iterations)); + cpu_compression_times.reserve(sample_count); + cpu_upload_times.reserve(sample_count); + cpu_total_times.reserve(sample_count); + gpu_total_times.reserve(sample_count); // Keep CPU and GPU phases separate: mobile CPU frequency and thermal state are shared // with the GPU, so interleaving them makes the CPU result workload-dependent. - for (int iteration = 0; iteration < warmup_iterations; ++iteration) { - auto compressed = cpu_compress(sources, algorithm, m_mipmaps); - static_cast(upload_cpu(compressed)); + // Each backend warms up once on all four distinct batches. + for (int group = 0; group < batches_per_round; ++group) { + auto compressed = cpu_compress(sources_for_group(group), algorithm, m_mipmaps); + static_cast(upload_cpu(cpu_destination, compressed)); } - for (int iteration = 0; iteration < m_iterations; ++iteration) { - const auto cpu_start = Clock::now(); - auto compressed = cpu_compress(sources, algorithm, m_mipmaps); - const auto cpu_compression_time = elapsed_ms(cpu_start); - const auto cpu_upload_time = upload_cpu(compressed); - cpu_compression_times.push_back(cpu_compression_time); - cpu_upload_times.push_back(cpu_upload_time); - cpu_total_times.push_back(elapsed_ms(cpu_start)); + for (int group = 0; group < batches_per_round; ++group) + static_cast(gpu_compressor->compress(sources_for_group(group), *gpu_destination, layers, gpu_settings)); + + for (int round = 0; round < measurement_rounds; ++round) { + for (int group = 0; group < batches_per_round; ++group) { + const auto cpu_start = Clock::now(); + auto compressed = cpu_compress(sources_for_group(group), algorithm, m_mipmaps); + const auto cpu_compression_time = elapsed_ms(cpu_start); + const auto cpu_upload_time = upload_cpu(cpu_destination, compressed); + cpu_compression_times.push_back(cpu_compression_time); + cpu_upload_times.push_back(cpu_upload_time); + cpu_total_times.push_back(elapsed_ms(cpu_start)); + } } - for (int iteration = 0; iteration < warmup_iterations; ++iteration) - static_cast(gpu_compressor->compress(sources, *gpu_destination, layers, gpu_settings)); - for (int iteration = 0; iteration < m_iterations; ++iteration) { - const auto gpu = gpu_compressor->compress(sources, - *gpu_destination, - layers, - gpu_settings); - gpu_total_times.push_back(gpu.timings.total_ms); + for (int round = 0; round < measurement_rounds; ++round) { + for (int group = 0; group < batches_per_round; ++group) { + const auto gpu = gpu_compressor->compress(sources_for_group(group), *gpu_destination, layers, gpu_settings); + gpu_total_times.push_back(gpu.timings.total_ms); + } } // Stage timings are collected in a separate profiling phase. Each stage is completed @@ -698,17 +842,19 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { auto stage_settings = gpu_settings; stage_settings.timing_mode = gl_engine::TextureCompressor::TimingMode::IndividualStages; stage_settings.gpu_timer = m_gpu_timer.get(); - for (int iteration = 0; iteration < m_iterations; ++iteration) { - const auto gpu = gpu_compressor->compress(sources, *gpu_destination, layers, stage_settings); - gpu_upload_times.append(gpu.timings.scratch_upload); - gpu_mipmap_times.append(gpu.timings.mipmap_generation); - gpu_compression_pass_times.append(gpu.timings.compression_pass); - gpu_packing_pass_times.append(gpu.timings.packing_pass); - gpu_encoding_times.append(gpu.timings.encoding); - gpu_output_transfer_times.append(gpu.timings.output_transfer); - gpu_compressed_upload_times.append(gpu.timings.compressed_upload); - if (gpu.gpu_timing_ticket) - gpu_timing_tickets.push_back(gpu.gpu_timing_ticket); + for (int round = 0; round < measurement_rounds; ++round) { + for (int group = 0; group < batches_per_round; ++group) { + const auto gpu = gpu_compressor->compress(sources_for_group(group), *gpu_destination, layers, stage_settings); + gpu_upload_times.append(gpu.timings.scratch_upload); + gpu_mipmap_times.append(gpu.timings.mipmap_generation); + gpu_compression_pass_times.append(gpu.timings.compression_pass); + gpu_packing_pass_times.append(gpu.timings.packing_pass); + gpu_encoding_times.append(gpu.timings.encoding); + gpu_output_transfer_times.append(gpu.timings.output_transfer); + gpu_compressed_upload_times.append(gpu.timings.compressed_upload); + if (gpu.gpu_timing_ticket) + gpu_timing_tickets.push_back(gpu.gpu_timing_ticket); + } } const auto cpu_compression = statistics(cpu_compression_times); @@ -722,8 +868,6 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { const auto gpu_output_transfer = statistics(gpu_output_transfer_times.total); const auto gpu_compressed_upload = statistics(gpu_compressed_upload_times.total); const auto gpu_total = statistics(gpu_total_times); - const auto cpu_psnr = linear_psnr(reconstruct(cpu_destination, resolution), source); - const auto gpu_psnr = linear_psnr(reconstruct(*gpu_destination, resolution), source); const auto algorithm_name = algorithm == nucleus::utils::ColourTexture::Format::DXT1 ? QStringLiteral("DXT1 / BC1") : QStringLiteral("ETC1 in ETC2"); QJsonObject root { @@ -735,12 +879,17 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { { QStringLiteral("gpu_backend"), backend_name }, { QStringLiteral("timing_method"), QStringLiteral("wall time; one final glFinish per end-to-end sample") }, { QStringLiteral("resolution"), int(resolution) }, - { QStringLiteral("batch_size"), m_batch_size }, - { QStringLiteral("iterations"), m_iterations }, - { QStringLiteral("warmup_iterations"), warmup_iterations }, - { QStringLiteral("gpu_stage_profile_iterations"), m_iterations }, + { QStringLiteral("batch_size"), batch_size }, + { QStringLiteral("measurement_rounds"), measurement_rounds }, + { QStringLiteral("batches_per_round"), batches_per_round }, + { QStringLiteral("measurement_samples"), sample_count }, + { QStringLiteral("warmup_rounds"), 1 }, + { QStringLiteral("gpu_stage_profile_samples"), sample_count }, { QStringLiteral("effort"), m_effort }, { QStringLiteral("mipmaps"), m_mipmaps }, + { QStringLiteral("dataset"), dataset_json() }, + { QStringLiteral("cpu_psnr_db_all_16_images"), cpu_psnr }, + { QStringLiteral("gpu_psnr_db_all_16_images"), gpu_psnr }, { QStringLiteral("cpu_compression"), to_json(cpu_compression) }, { QStringLiteral("cpu_compressed_upload"), to_json(cpu_upload) }, { QStringLiteral("cpu_end_to_end"), to_json(cpu_total) }, @@ -752,12 +901,10 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { { QStringLiteral("gpu_output_transfer"), to_json(gpu_output_transfer) }, { QStringLiteral("gpu_compressed_upload"), to_json(gpu_compressed_upload) }, { QStringLiteral("gpu_end_to_end"), to_json(gpu_total) }, - { QStringLiteral("cpu_psnr_db"), cpu_psnr }, - { QStringLiteral("gpu_psnr_db"), gpu_psnr }, - { QStringLiteral("cpu_tiles_per_second"), 1000.0 * m_batch_size / cpu_total.median }, - { QStringLiteral("gpu_tiles_per_second"), 1000.0 * m_batch_size / gpu_total.median }, + { QStringLiteral("cpu_tiles_per_second"), 1000.0 * batch_size / cpu_total.median }, + { QStringLiteral("gpu_tiles_per_second"), 1000.0 * batch_size / gpu_total.median }, { QStringLiteral("phase_order"), - QStringLiteral("CPU warmup, CPU measurement, GPU warmup, GPU end-to-end measurement, GPU stage profiling, asynchronous fence verification") }, + QStringLiteral("16-image PSNR and preview, one CPU and GPU warmup round over four batches, three CPU and GPU measurement rounds over four batches, GPU stage profiling, asynchronous fence verification") }, { QStringLiteral("gpu_stage_timing_method"), QStringLiteral("separate profiling pass; each stage glFinish-synchronised") }, { QStringLiteral("gpu_stage_wall_profile"), QJsonObject { @@ -798,15 +945,19 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { return QStringLiteral("%1 median %2 ms p95 %3 ms").arg(label, -26).arg(value.median, 8, 'f', 3).arg(value.p95, 8, 'f', 3); }; QStringList summary { - QStringLiteral("%1 — %2 × %3, batch %4, effort %5, mipmaps %6") + QStringLiteral("%1 — %2 × %3, batch %4, 12 samples, effort %5, mipmaps %6") .arg(algorithm_name) .arg(resolution) .arg(resolution) - .arg(m_batch_size) + .arg(batch_size) .arg(m_effort) .arg(m_mipmaps ? QStringLiteral("on") : QStringLiteral("off")), backend_name, gl_string(GL_RENDERER), + QStringLiteral("Quality first: one untimed 16-image pass"), + QStringLiteral("CPU PSNR (all 16 images) %1 dB").arg(cpu_psnr, 0, 'f', 2), + QStringLiteral("GPU PSNR (all 16 images) %1 dB").arg(gpu_psnr, 0, 'f', 2), + QStringLiteral("Timing: one warmup round, then three rounds × four distinct batches"), QStringLiteral("Timing: one final glFinish per end-to-end sample"), QString(), line(QStringLiteral("CPU compression"), cpu_compression), @@ -822,10 +973,8 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { line(QStringLiteral("GPU compressed upload"), gpu_compressed_upload), line(QStringLiteral("GPU end-to-end"), gpu_total), QString(), - QStringLiteral("CPU completed throughput %1 tiles/s").arg(1000.0 * m_batch_size / cpu_total.median, 0, 'f', 1), - QStringLiteral("GPU completed throughput %1 tiles/s").arg(1000.0 * m_batch_size / gpu_total.median, 0, 'f', 1), - QStringLiteral("CPU PSNR %1 dB").arg(cpu_psnr, 0, 'f', 2), - QStringLiteral("GPU PSNR %1 dB").arg(gpu_psnr, 0, 'f', 2), + QStringLiteral("CPU completed throughput %1 tiles/s").arg(1000.0 * batch_size / cpu_total.median, 0, 'f', 1), + QStringLiteral("GPU completed throughput %1 tiles/s").arg(1000.0 * batch_size / gpu_total.median, 0, 'f', 1), }; if (!m_gpu_timer->is_supported()) { root.insert(QStringLiteral("gpu_timer_query"), @@ -839,18 +988,18 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { .root = std::move(root), .summary = std::move(summary), .tickets = std::move(gpu_timing_tickets), - .query_results = std::vector>(size_t(m_iterations)), - .query_finished = std::vector(size_t(m_iterations), false), + .query_results = std::vector>(sample_count), + .query_finished = std::vector(sample_count, false), }; if (m_gpu_timer->is_supported()) - Q_ASSERT(m_pending_gpu_report->tickets.size() == size_t(m_iterations)); + Q_ASSERT(m_pending_gpu_report->tickets.size() == sample_count); PendingFenceDiagnostic fence_diagnostic; fence_diagnostic.compressor = std::move(gpu_compressor); fence_diagnostic.destination = std::move(gpu_destination); fence_diagnostic.probe_framebuffer = std::make_unique(gl_engine::Framebuffer::DepthFormat::None, std::vector { gl_engine::Framebuffer::ColourFormat::RGBA8 }, - glm::uvec2(unsigned(m_batch_size), 1u)); + glm::uvec2(batch_size, 1u)); fence_diagnostic.probe_shader = std::make_unique(R"( void main() { highp vec2 vertices[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); @@ -865,11 +1014,11 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { })", gl_engine::ShaderCodeSource::PLAINTEXT); fence_diagnostic.probe_geometry = gl_engine::helpers::create_screen_quad_geometry(); - fence_diagnostic.sources = std::move(sources); + fence_diagnostic.source_pool = std::move(all_sources); fence_diagnostic.layers = std::move(layers); fence_diagnostic.settings = gpu_settings; fence_diagnostic.settings.timing_mode = gl_engine::TextureCompressor::TimingMode::SubmissionOnly; - fence_diagnostic.iteration_count = m_iterations; + fence_diagnostic.iteration_count = sample_count; m_pending_gpu_report->fence_diagnostic.emplace(std::move(fence_diagnostic)); begin_fence_sample(*m_pending_gpu_report->fence_diagnostic); return {}; @@ -879,17 +1028,19 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { QQuickWindow* m_window = nullptr; unsigned m_seen_serial = 0; int m_effort = 4; - int m_batch_size = 4; - int m_iterations = 10; bool m_mipmaps = true; bool m_pending = false; + std::vector m_source_images; + QString m_preview_source; std::unique_ptr m_gpu_timer; std::optional m_pending_gpu_report; }; BenchmarkItem::BenchmarkItem(QQuickItem* parent) : QQuickFramebufferObject(parent) + , m_network_manager(new QNetworkAccessManager(this)) { + downloadBenchmarkData(); } QQuickFramebufferObject::Renderer* BenchmarkItem::createRenderer() const { return new BenchmarkRenderer; } @@ -904,27 +1055,6 @@ void BenchmarkItem::setEffort(int value) emit effortChanged(); } -int BenchmarkItem::batchSize() const { return m_batch_size; } -void BenchmarkItem::setBatchSize(int value) -{ - if (value != 1 && value != 4 && value != 16) - return; - if (m_batch_size == value) - return; - m_batch_size = value; - emit batchSizeChanged(); -} - -int BenchmarkItem::iterations() const { return m_iterations; } -void BenchmarkItem::setIterations(int value) -{ - value = std::clamp(value, 1, 50); - if (m_iterations == value) - return; - m_iterations = value; - emit iterationsChanged(); -} - bool BenchmarkItem::mipmaps() const { return m_mipmaps; } void BenchmarkItem::setMipmaps(bool value) { @@ -934,17 +1064,92 @@ void BenchmarkItem::setMipmaps(bool value) emit mipmapsChanged(); } +bool BenchmarkItem::dataReady() const { return m_data_ready; } +QString BenchmarkItem::dataStatus() const { return m_data_status; } bool BenchmarkItem::running() const { return m_running; } QString BenchmarkItem::resultText() const { return m_result_text; } QString BenchmarkItem::resultJson() const { return m_result_json; } +QString BenchmarkItem::previewSource() const { return m_preview_source; } + +void BenchmarkItem::downloadBenchmarkData() +{ + m_downloaded_tiles.resize(tile_groups.size() * 4); + m_downloads_remaining = int(m_downloaded_tiles.size()); + for (size_t group_index = 0; group_index < tile_groups.size(); ++group_index) { + for (int y = 0; y < 2; ++y) { + for (int x = 0; x < 2; ++x) { + const auto tile_index = group_index * 4 + size_t(y * 2 + x); + const auto url = tile_url(tile_groups[group_index], x, y); + auto* reply = m_network_manager->get(QNetworkRequest(QUrl(url))); + connect(reply, &QNetworkReply::finished, this, [this, reply, tile_index, url]() { + if (reply->error() == QNetworkReply::NoError) { + QImage image; + image.loadFromData(reply->readAll(), "JPEG"); + if (!image.isNull() && image.size() == QSize(256, 256)) + m_downloaded_tiles[tile_index] = image.convertToFormat(QImage::Format_RGBA8888); + } + if (m_downloaded_tiles[tile_index].isNull() && !m_data_status.startsWith(QStringLiteral("Unable"))) { + m_data_status = QStringLiteral("Unable to download benchmark tile: %1").arg(url); + emit dataStatusChanged(); + } + reply->deleteLater(); + --m_downloads_remaining; + if (m_downloads_remaining > 0) { + if (!m_data_status.startsWith(QStringLiteral("Unable"))) { + m_data_status = QStringLiteral("Downloading benchmark imagery… %1/%2") + .arg(int(m_downloaded_tiles.size()) - m_downloads_remaining) + .arg(m_downloaded_tiles.size()); + emit dataStatusChanged(); + } + return; + } + if (std::ranges::any_of(m_downloaded_tiles, [](const QImage& image) { return image.isNull(); })) { + m_result_text = m_data_status; + emit resultTextChanged(); + return; + } + stitchBenchmarkData(); + }); + } + } + } +} + +void BenchmarkItem::stitchBenchmarkData() +{ + m_source_images.clear(); + m_source_images.reserve(tile_groups.size()); + for (size_t group_index = 0; group_index < tile_groups.size(); ++group_index) { + QImage stitched(512, 512, QImage::Format_RGBA8888); + QPainter painter(&stitched); + for (int y = 0; y < 2; ++y) { + for (int x = 0; x < 2; ++x) + painter.drawImage(QPoint(x * 256, y * 256), m_downloaded_tiles[group_index * 4 + size_t(y * 2 + x)]); + } + painter.end(); + m_source_images.push_back(std::move(stitched)); + } + m_downloaded_tiles.clear(); + m_data_ready = true; + m_data_status = QStringLiteral("16 benchmark images ready."); + emit dataReadyChanged(); + emit dataStatusChanged(); + runBenchmark(); +} void BenchmarkItem::runBenchmark() { - if (m_running) + if (m_running || !m_data_ready) return; m_running = true; + m_result_text = QStringLiteral("Computing PSNR, then measuring batch size 4…"); + m_result_json.clear(); + m_preview_source.clear(); ++m_request_serial; emit runningChanged(); + emit resultTextChanged(); + emit resultJsonChanged(); + emit previewSourceChanged(); update(); } @@ -954,12 +1159,14 @@ void BenchmarkItem::copyResultJson() QGuiApplication::clipboard()->setText(m_result_json); } -void BenchmarkItem::publishResults(const QString& text, const QString& json) +void BenchmarkItem::publishResults(const QString& text, const QString& json, const QString& preview_source) { m_result_text = text; m_result_json = json; + m_preview_source = preview_source; m_running = false; emit resultTextChanged(); emit resultJsonChanged(); + emit previewSourceChanged(); emit runningChanged(); } diff --git a/apps/texture_compression_benchmark/BenchmarkItem.h b/apps/texture_compression_benchmark/BenchmarkItem.h index 1608074c..370ed588 100644 --- a/apps/texture_compression_benchmark/BenchmarkItem.h +++ b/apps/texture_compression_benchmark/BenchmarkItem.h @@ -6,20 +6,25 @@ #pragma once +#include #include #include #include +#include + +class QNetworkAccessManager; class BenchmarkItem : public QQuickFramebufferObject { Q_OBJECT QML_ELEMENT Q_PROPERTY(int effort READ effort WRITE setEffort NOTIFY effortChanged) - Q_PROPERTY(int batchSize READ batchSize WRITE setBatchSize NOTIFY batchSizeChanged) - Q_PROPERTY(int iterations READ iterations WRITE setIterations NOTIFY iterationsChanged) Q_PROPERTY(bool mipmaps READ mipmaps WRITE setMipmaps NOTIFY mipmapsChanged) + Q_PROPERTY(bool dataReady READ dataReady NOTIFY dataReadyChanged) + Q_PROPERTY(QString dataStatus READ dataStatus NOTIFY dataStatusChanged) Q_PROPERTY(bool running READ running NOTIFY runningChanged) Q_PROPERTY(QString resultText READ resultText NOTIFY resultTextChanged) Q_PROPERTY(QString resultJson READ resultJson NOTIFY resultJsonChanged) + Q_PROPERTY(QString previewSource READ previewSource NOTIFY previewSourceChanged) public: explicit BenchmarkItem(QQuickItem* parent = nullptr); @@ -27,38 +32,45 @@ class BenchmarkItem : public QQuickFramebufferObject { [[nodiscard]] int effort() const; void setEffort(int value); - [[nodiscard]] int batchSize() const; - void setBatchSize(int value); - [[nodiscard]] int iterations() const; - void setIterations(int value); [[nodiscard]] bool mipmaps() const; void setMipmaps(bool value); + [[nodiscard]] bool dataReady() const; + [[nodiscard]] QString dataStatus() const; [[nodiscard]] bool running() const; [[nodiscard]] QString resultText() const; [[nodiscard]] QString resultJson() const; + [[nodiscard]] QString previewSource() const; Q_INVOKABLE void runBenchmark(); Q_INVOKABLE void copyResultJson(); signals: void effortChanged(); - void batchSizeChanged(); - void iterationsChanged(); void mipmapsChanged(); + void dataReadyChanged(); + void dataStatusChanged(); void runningChanged(); void resultTextChanged(); void resultJsonChanged(); + void previewSourceChanged(); private: friend class BenchmarkRenderer; - void publishResults(const QString& text, const QString& json); + void downloadBenchmarkData(); + void stitchBenchmarkData(); + void publishResults(const QString& text, const QString& json, const QString& preview_source); int m_effort = 4; - int m_batch_size = 4; - int m_iterations = 10; bool m_mipmaps = true; + bool m_data_ready = false; bool m_running = false; unsigned m_request_serial = 0; - QString m_result_text = QStringLiteral("Run the benchmark to collect results."); + int m_downloads_remaining = 0; + QNetworkAccessManager* m_network_manager = nullptr; + std::vector m_downloaded_tiles; + std::vector m_source_images; + QString m_data_status = QStringLiteral("Downloading benchmark imagery…"); + QString m_result_text = QStringLiteral("Waiting for benchmark imagery."); QString m_result_json; + QString m_preview_source; }; diff --git a/apps/texture_compression_benchmark/CMakeLists.txt b/apps/texture_compression_benchmark/CMakeLists.txt index f4619532..492a8563 100644 --- a/apps/texture_compression_benchmark/CMakeLists.txt +++ b/apps/texture_compression_benchmark/CMakeLists.txt @@ -19,22 +19,17 @@ qt_add_qml_module(texture_compression_benchmark QML_FILES Main.qml ) -qt_add_resources(texture_compression_benchmark "benchmark_data" - PREFIX "/benchmark" - BASE "${CMAKE_SOURCE_DIR}/unittests/nucleus/data/quad" - FILES "${CMAKE_SOURCE_DIR}/unittests/nucleus/data/quad/merged.jpg" -) - qt_add_resources(texture_compression_benchmark "fonts" BASE "${alpineapp_fonts_SOURCE_DIR}" PREFIX "/fonts" FILES "${alpineapp_fonts_SOURCE_DIR}/Roboto/Roboto-Regular.ttf" ) -target_link_libraries(texture_compression_benchmark PUBLIC gl_engine Qt::Quick Qt::QuickControls2) +target_link_libraries(texture_compression_benchmark PUBLIC gl_engine Qt::Network Qt::Quick Qt::QuickControls2) alp_configure_target(texture_compression_benchmark) if (ANDROID) + add_android_openssl_libraries(texture_compression_benchmark) set_target_properties(texture_compression_benchmark PROPERTIES QT_ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android" QT_ANDROID_PACKAGE_NAME "org.alpinemaps.texturecompressionbenchmark" diff --git a/apps/texture_compression_benchmark/Main.qml b/apps/texture_compression_benchmark/Main.qml index a79e67d4..e36a68d9 100644 --- a/apps/texture_compression_benchmark/Main.qml +++ b/apps/texture_compression_benchmark/Main.qml @@ -47,7 +47,7 @@ ApplicationWindow { Layout.fillWidth: true Layout.leftMargin: 20 Layout.rightMargin: 20 - text: qsTr("Measures DXT1 or ETC1 compression of 512×512 ortho textures, including optional mipmaps and final texture-array upload. WebGL requires ETC or sRGB S3TC compressed textures.") + text: qsTr("Measures DXT1 or ETC1 compression of varied 512×512 ortho imagery in batches of four. Quality is computed once over all 16 images; timing covers three rounds of four distinct batches. WebGL requires ETC or sRGB S3TC compressed textures.") wrapMode: Text.Wrap } @@ -77,24 +77,6 @@ ApplicationWindow { onMoved: benchmark.effort = Math.round(value) } - RowLayout { - Layout.fillWidth: true - - Label { - Layout.fillWidth: true - text: qsTr("Batch size") - } - - ComboBox { - id: batchSizeBox - Layout.preferredWidth: 120 - model: [1, 4, 16] - currentIndex: 1 - enabled: !benchmark.running - onActivated: benchmark.batchSize = Number(currentText) - } - } - RowLayout { Layout.fillWidth: true @@ -109,25 +91,6 @@ ApplicationWindow { } } - RowLayout { - Layout.fillWidth: true - - Label { - Layout.fillWidth: true - text: qsTr("Measured iterations") - } - - SpinBox { - Layout.preferredWidth: 120 - from: 1 - to: 50 - value: benchmark.iterations - editable: true - enabled: !benchmark.running - onValueModified: benchmark.iterations = value - } - } - CheckBox { Layout.fillWidth: true text: qsTr("Generate and compress mipmaps") @@ -145,7 +108,7 @@ ApplicationWindow { Button { text: qsTr("Run benchmark") - enabled: !benchmark.running + enabled: benchmark.dataReady && !benchmark.running highlighted: true onClicked: benchmark.runBenchmark() } @@ -159,7 +122,9 @@ ApplicationWindow { Label { Layout.fillWidth: true - text: benchmark.running ? qsTr("Benchmarking; the display may pause to avoid contaminating GPU measurements.") : qsTr("Ready") + text: benchmark.running + ? qsTr("Computing PSNR, then measuring batch size 4; the display may pause to avoid contaminating GPU measurements.") + : benchmark.dataStatus wrapMode: Text.Wrap } } @@ -183,13 +148,56 @@ ApplicationWindow { wrapMode: TextEdit.Wrap } - Button { - text: qsTr("Copy JSON") - enabled: !benchmark.running && benchmark.resultJson.length > 0 - onClicked: benchmark.copyResultJson() + RowLayout { + Layout.fillWidth: true + + Button { + text: qsTr("Preview compressed tiles") + enabled: !benchmark.running && benchmark.previewSource.length > 0 + onClicked: previewDialogLoader.active = true + } + + Button { + text: qsTr("Copy JSON") + enabled: !benchmark.running && benchmark.resultJson.length > 0 + onClicked: benchmark.copyResultJson() + } } } } } } + + Loader { + id: previewDialogLoader + + active: false + asynchronous: true + onLoaded: { + if (status === Loader.Ready) + item.open() + } + + sourceComponent: Component { + Dialog { + id: previewDialog + + parent: Overlay.overlay + anchors.centerIn: parent + width: Math.min(root.width - 40, 820) + height: Math.min(root.height - 40, 880) + modal: true + title: qsTr("GPU-compressed tiles") + standardButtons: Dialog.Close + onClosed: previewDialogLoader.active = false + + contentItem: Image { + source: benchmark.previewSource + sourceSize: Qt.size(2048, 2048) + asynchronous: true + fillMode: Image.PreserveAspectFit + } + } + } + } } From 298158e91917be6b2b3546aa50fba89794adb689 Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Wed, 19 Aug 2026 23:48:18 +0200 Subject: [PATCH 12/38] Improve compressed texture preview --- .../BenchmarkItem.cpp | 24 ++++++++- apps/texture_compression_benchmark/Main.qml | 53 +++++++++++++++++-- 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/apps/texture_compression_benchmark/BenchmarkItem.cpp b/apps/texture_compression_benchmark/BenchmarkItem.cpp index d5d431fa..a20d8de5 100644 --- a/apps/texture_compression_benchmark/BenchmarkItem.cpp +++ b/apps/texture_compression_benchmark/BenchmarkItem.cpp @@ -288,11 +288,31 @@ QString preview_data_url(std::span images) { constexpr int columns = 4; constexpr int tile_size = 512; + static const auto linear_to_srgb = [] { + std::array result {}; + for (size_t i = 0; i < result.size(); ++i) { + const auto linear = double(i) / 255.0; + const auto srgb = linear <= 0.0031308 ? 12.92 * linear : 1.055 * std::pow(linear, 1.0 / 2.4) - 0.055; + result[i] = uint8_t(std::lround(std::clamp(srgb, 0.0, 1.0) * 255.0)); + } + return result; + }(); QImage preview(columns * tile_size, columns * tile_size, QImage::Format_RGBA8888); preview.fill(Qt::black); QPainter painter(&preview); - for (size_t i = 0; i < images.size(); ++i) - painter.drawImage(QPoint(int(i % columns) * tile_size, int(i / columns) * tile_size), images[i]); + for (size_t i = 0; i < images.size(); ++i) { + auto srgb_image = images[i].convertToFormat(QImage::Format_RGBA8888); + for (int y = 0; y < srgb_image.height(); ++y) { + auto* scanline = srgb_image.scanLine(y); + for (int x = 0; x < srgb_image.width(); ++x) { + auto* pixel = scanline + x * 4; + pixel[0] = linear_to_srgb[pixel[0]]; + pixel[1] = linear_to_srgb[pixel[1]]; + pixel[2] = linear_to_srgb[pixel[2]]; + } + } + painter.drawImage(QPoint(int(i % columns) * tile_size, int(i / columns) * tile_size), srgb_image); + } painter.end(); QByteArray png; diff --git a/apps/texture_compression_benchmark/Main.qml b/apps/texture_compression_benchmark/Main.qml index e36a68d9..f0050418 100644 --- a/apps/texture_compression_benchmark/Main.qml +++ b/apps/texture_compression_benchmark/Main.qml @@ -191,11 +191,54 @@ ApplicationWindow { standardButtons: Dialog.Close onClosed: previewDialogLoader.active = false - contentItem: Image { - source: benchmark.previewSource - sourceSize: Qt.size(2048, 2048) - asynchronous: true - fillMode: Image.PreserveAspectFit + contentItem: Flickable { + id: previewViewport + + property real zoom: 1.0 + property real pinchStartZoom: 1.0 + property real pinchStartContentX: 0.0 + property real pinchStartContentY: 0.0 + + clip: true + boundsBehavior: Flickable.StopAtBounds + contentWidth: Math.max(width, previewImage.width) + contentHeight: Math.max(height, previewImage.height) + + Image { + id: previewImage + + readonly property real fittedSize: Math.min(previewViewport.width, previewViewport.height) + + x: (previewViewport.contentWidth - width) / 2 + y: (previewViewport.contentHeight - height) / 2 + width: fittedSize * previewViewport.zoom + height: width + source: benchmark.previewSource + sourceSize: Qt.size(2048, 2048) + asynchronous: true + fillMode: Image.PreserveAspectFit + } + + PinchHandler { + target: null + + onActiveChanged: { + if (active) { + previewViewport.pinchStartZoom = previewViewport.zoom + previewViewport.pinchStartContentX = previewViewport.contentX + previewViewport.pinchStartContentY = previewViewport.contentY + } else { + previewViewport.returnToBounds() + } + } + onActiveScaleChanged: { + const newZoom = Math.max(1.0, Math.min(8.0, previewViewport.pinchStartZoom * activeScale)) + const zoomRatio = newZoom / previewViewport.pinchStartZoom + previewViewport.zoom = newZoom + previewViewport.contentX = (previewViewport.pinchStartContentX + centroid.position.x) * zoomRatio - centroid.position.x + previewViewport.contentY = (previewViewport.pinchStartContentY + centroid.position.y) * zoomRatio - centroid.position.y + } + } } } } From 4b47ac3a6a6f6fdc4bcc56d457083f60db0bb06b Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Thu, 20 Aug 2026 01:51:14 +0200 Subject: [PATCH 13/38] Add BasisU texture compression benchmarks --- CMakeLists.txt | 6 +- .../BenchmarkItem.cpp | 216 ++++++++++++++++-- .../BenchmarkItem.h | 18 ++ .../CMakeLists.txt | 28 ++- apps/texture_compression_benchmark/Main.qml | 64 +++++- nucleus/CMakeLists.txt | 16 +- .../BasisUniversalTextureCompression.cpp | 175 ++++++++++++++ .../utils/BasisUniversalTextureCompression.h | 46 ++++ nucleus/utils/ColourTexture.cpp | 9 + nucleus/utils/ColourTexture.h | 1 + unittests/nucleus/CMakeLists.txt | 5 + .../basis_universal_texture_compression.cpp | 51 +++++ 12 files changed, 601 insertions(+), 34 deletions(-) create mode 100644 nucleus/utils/BasisUniversalTextureCompression.cpp create mode 100644 nucleus/utils/BasisUniversalTextureCompression.h create mode 100644 unittests/nucleus/basis_universal_texture_compression.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4dd9be3d..1053695f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,14 +26,10 @@ option(ALP_BUILD_GL_ENGINE "include the gl engine in the buildsystem" OFF) option(ALP_BUILD_PLAIN_RENDERER "include the plain renderer in the buildsystem" ON) option(ALP_BUILD_ALPINEAPP "include the qml app in the buildsystem" ON) option(ALP_BUILD_TEXTURE_COMPRESSION_BENCHMARK "include the texture compression benchmark application" OFF) -set(ALP_WEBGPU_DEFAULT ON) -if (APPLE OR ANDROID) - set(ALP_WEBGPU_DEFAULT OFF) -endif() option(ALP_BUILD_WEBGPU_BASE "include the webgpu base library in the buildsystem" OFF) option(ALP_BUILD_WEBGPU_ENGINE "include the webgpu engine in the buildsystem" OFF) option(ALP_BUILD_WEBGPU_COMPUTE "include the webgpu compute library in the buildsystem" OFF) -option(ALP_BUILD_WEBGPU_APP "include the webgpu app in the buildsystem" ${ALP_WEBGPU_DEFAULT}) +option(ALP_BUILD_WEBGPU_APP "include the webgpu app in the buildsystem" OFF) option(ALP_WEBGPU_APP_ENABLE_COMPUTE "Build the webgpu_compute graph into the app" ON) diff --git a/apps/texture_compression_benchmark/BenchmarkItem.cpp b/apps/texture_compression_benchmark/BenchmarkItem.cpp index a20d8de5..4301dbb2 100644 --- a/apps/texture_compression_benchmark/BenchmarkItem.cpp +++ b/apps/texture_compression_benchmark/BenchmarkItem.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +40,7 @@ #include #include #include +#include #include #if defined(__EMSCRIPTEN__) #include @@ -130,6 +132,14 @@ QJsonObject to_json(const Statistics& value) { QStringLiteral("max_ms"), value.maximum } }; } +QJsonObject size_to_json(const Statistics& value) +{ + return { { QStringLiteral("median_bytes"), value.median }, + { QStringLiteral("p95_bytes"), value.p95 }, + { QStringLiteral("min_bytes"), value.minimum }, + { QStringLiteral("max_bytes"), value.maximum } }; +} + QJsonArray samples_to_json(const std::vector& values) { QJsonArray result; @@ -322,19 +332,86 @@ QString preview_data_url(std::span images) return QStringLiteral("data:image/png;base64,") + QString::fromLatin1(png.toBase64()); } -std::vector cpu_compress( - std::span sources, nucleus::utils::ColourTexture::Format algorithm, bool mipmaps) +struct CpuCompressionResult { + std::vector textures; + double source_preparation_ms = 0.0; + double encoding_ms = 0.0; + double transcoding_ms = 0.0; + size_t intermediate_bytes = 0; + size_t transcoded_bytes = 0; +}; + +QString cpu_encoder_name(BenchmarkItem::CpuEncoder encoder) +{ + switch (encoder) { + case BenchmarkItem::CpuEncoder::Goofy: + return QStringLiteral("Goofy direct"); + case BenchmarkItem::CpuEncoder::BasisEtc1s: + return QString::fromLatin1(nucleus::utils::basis_universal_format_name(nucleus::utils::BasisUniversalFormat::ETC1S)); + case BenchmarkItem::CpuEncoder::BasisUastcLdr4x4: + return QString::fromLatin1(nucleus::utils::basis_universal_format_name(nucleus::utils::BasisUniversalFormat::UASTC_LDR_4x4)); + case BenchmarkItem::CpuEncoder::BasisXuastcLdr4x4: + return QString::fromLatin1(nucleus::utils::basis_universal_format_name(nucleus::utils::BasisUniversalFormat::XUASTC_LDR_4x4)); + } + return QStringLiteral("Unknown"); +} + +nucleus::utils::BasisUniversalFormat basis_format(BenchmarkItem::CpuEncoder encoder) { - std::vector result; - result.reserve(sources.size()); + switch (encoder) { + case BenchmarkItem::CpuEncoder::BasisEtc1s: + return nucleus::utils::BasisUniversalFormat::ETC1S; + case BenchmarkItem::CpuEncoder::BasisUastcLdr4x4: + return nucleus::utils::BasisUniversalFormat::UASTC_LDR_4x4; + case BenchmarkItem::CpuEncoder::BasisXuastcLdr4x4: + return nucleus::utils::BasisUniversalFormat::XUASTC_LDR_4x4; + case BenchmarkItem::CpuEncoder::Goofy: + break; + } + return nucleus::utils::BasisUniversalFormat::ETC1S; +} + +std::expected cpu_compress(std::span sources, + nucleus::utils::ColourTexture::Format algorithm, + bool mipmaps, + BenchmarkItem::CpuEncoder encoder, + int basis_quality, + int basis_effort) +{ + CpuCompressionResult result; + result.textures.reserve(sources.size()); for (const auto& source : sources) { - if (mipmaps) { - result.push_back(nucleus::utils::generate_mipmapped_colour_texture(source, algorithm)); - } else { - nucleus::utils::MipmappedColourTexture levels; - levels.emplace_back(source, algorithm); - result.push_back(std::move(levels)); + if (encoder == BenchmarkItem::CpuEncoder::Goofy) { + const auto start = Clock::now(); + if (mipmaps) { + result.textures.push_back(nucleus::utils::generate_mipmapped_colour_texture(source, algorithm)); + } else { + nucleus::utils::MipmappedColourTexture levels; + levels.emplace_back(source, algorithm); + result.textures.push_back(std::move(levels)); + } + result.encoding_ms += elapsed_ms(start); + for (const auto& level : result.textures.back()) + result.transcoded_bytes += level.n_bytes(); + continue; } + + const nucleus::utils::BasisUniversalCompressionSettings settings { + .format = basis_format(encoder), + .target_format = algorithm, + .quality = basis_quality, + .effort = basis_effort, + .generate_mipmaps = mipmaps, + }; + auto compressed = nucleus::utils::compress_with_basis_universal(source, settings); + if (!compressed) + return std::unexpected(QString::fromStdString(compressed.error())); + result.source_preparation_ms += compressed->timings.source_preparation_ms; + result.encoding_ms += compressed->timings.encoding_ms; + result.transcoding_ms += compressed->timings.transcoding_ms; + result.intermediate_bytes += compressed->intermediate_bytes; + result.transcoded_bytes += compressed->transcoded_bytes; + result.textures.push_back(std::move(compressed->texture)); } return result; } @@ -402,6 +479,9 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { if (benchmark_item->m_request_serial == m_seen_serial) return; m_seen_serial = benchmark_item->m_request_serial; + m_cpu_encoder = benchmark_item->m_cpu_encoder; + m_basis_quality = benchmark_item->m_basis_quality; + m_basis_effort = benchmark_item->m_basis_effort; m_effort = benchmark_item->m_effort; m_mipmaps = benchmark_item->m_mipmaps; m_source_images = benchmark_item->m_source_images; @@ -763,6 +843,17 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { .generate_mipmaps = m_mipmaps, .timing_mode = gl_engine::TextureCompressor::TimingMode::EndToEnd, }; + const auto compress_cpu = [&](std::span sources) { + return cpu_compress(sources, algorithm, m_mipmaps, m_cpu_encoder, m_basis_quality, m_basis_effort); + }; + const auto compression_error = [](const QString& error) { + const QJsonObject root { + { QStringLiteral("supported"), false }, + { QStringLiteral("error"), error }, + }; + return std::pair { QStringLiteral("CPU compression failed: %1").arg(error), + QString::fromUtf8(QJsonDocument(root).toJson(QJsonDocument::Indented)) }; + }; auto upload_cpu = [&](gl_engine::Texture& destination, const std::vector& compressed) { const auto start = Clock::now(); for (size_t layer = 0; layer < compressed.size(); ++layer) @@ -778,8 +869,10 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { gl_engine::Texture cpu_quality_destination(gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); cpu_quality_destination.setParams(filter, gl_engine::Texture::Filter::Linear); cpu_quality_destination.allocate_array(resolution, resolution, unsigned(tile_groups.size())); - auto cpu_quality = cpu_compress(all_sources, algorithm, m_mipmaps); - static_cast(upload_cpu(cpu_quality_destination, cpu_quality)); + auto cpu_quality = compress_cpu(all_sources); + if (!cpu_quality) + return compression_error(cpu_quality.error()); + static_cast(upload_cpu(cpu_quality_destination, cpu_quality->textures)); gl_engine::Texture gpu_quality_destination(gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); gpu_quality_destination.setParams(filter, gl_engine::Texture::Filter::Linear); @@ -797,7 +890,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { } cpu_psnr = linear_psnr(cpu_reconstructed, all_sources); gpu_psnr = linear_psnr(gpu_reconstructed, all_sources); - m_preview_source = preview_data_url(gpu_reconstructed); + m_preview_source = preview_data_url(cpu_reconstructed); } gl_engine::Texture cpu_destination(gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); @@ -811,8 +904,13 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { m_gpu_timer = std::make_unique(); std::vector cpu_compression_times; + std::vector cpu_source_preparation_times; + std::vector cpu_encoding_times; + std::vector cpu_transcoding_times; std::vector cpu_upload_times; std::vector cpu_total_times; + std::vector cpu_intermediate_bytes; + std::vector cpu_transcoded_bytes; WallTimingSamples gpu_upload_times; WallTimingSamples gpu_mipmap_times; WallTimingSamples gpu_compression_pass_times; @@ -823,16 +921,23 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { std::vector gpu_total_times; std::vector gpu_timing_tickets; cpu_compression_times.reserve(sample_count); + cpu_source_preparation_times.reserve(sample_count); + cpu_encoding_times.reserve(sample_count); + cpu_transcoding_times.reserve(sample_count); cpu_upload_times.reserve(sample_count); cpu_total_times.reserve(sample_count); + cpu_intermediate_bytes.reserve(sample_count); + cpu_transcoded_bytes.reserve(sample_count); gpu_total_times.reserve(sample_count); // Keep CPU and GPU phases separate: mobile CPU frequency and thermal state are shared // with the GPU, so interleaving them makes the CPU result workload-dependent. // Each backend warms up once on all four distinct batches. for (int group = 0; group < batches_per_round; ++group) { - auto compressed = cpu_compress(sources_for_group(group), algorithm, m_mipmaps); - static_cast(upload_cpu(cpu_destination, compressed)); + auto compressed = compress_cpu(sources_for_group(group)); + if (!compressed) + return compression_error(compressed.error()); + static_cast(upload_cpu(cpu_destination, compressed->textures)); } for (int group = 0; group < batches_per_round; ++group) static_cast(gpu_compressor->compress(sources_for_group(group), *gpu_destination, layers, gpu_settings)); @@ -840,12 +945,19 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { for (int round = 0; round < measurement_rounds; ++round) { for (int group = 0; group < batches_per_round; ++group) { const auto cpu_start = Clock::now(); - auto compressed = cpu_compress(sources_for_group(group), algorithm, m_mipmaps); + auto compressed = compress_cpu(sources_for_group(group)); + if (!compressed) + return compression_error(compressed.error()); const auto cpu_compression_time = elapsed_ms(cpu_start); - const auto cpu_upload_time = upload_cpu(cpu_destination, compressed); + const auto cpu_upload_time = upload_cpu(cpu_destination, compressed->textures); cpu_compression_times.push_back(cpu_compression_time); + cpu_source_preparation_times.push_back(compressed->source_preparation_ms); + cpu_encoding_times.push_back(compressed->encoding_ms); + cpu_transcoding_times.push_back(compressed->transcoding_ms); cpu_upload_times.push_back(cpu_upload_time); cpu_total_times.push_back(elapsed_ms(cpu_start)); + cpu_intermediate_bytes.push_back(double(compressed->intermediate_bytes)); + cpu_transcoded_bytes.push_back(double(compressed->transcoded_bytes)); } } @@ -878,8 +990,13 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { } const auto cpu_compression = statistics(cpu_compression_times); + const auto cpu_source_preparation = statistics(cpu_source_preparation_times); + const auto cpu_encoding = statistics(cpu_encoding_times); + const auto cpu_transcoding = statistics(cpu_transcoding_times); const auto cpu_upload = statistics(cpu_upload_times); const auto cpu_total = statistics(cpu_total_times); + const auto cpu_intermediate_size = statistics(cpu_intermediate_bytes); + const auto cpu_transcoded_size = statistics(cpu_transcoded_bytes); const auto gpu_upload = statistics(gpu_upload_times.total); const auto gpu_mipmap = statistics(gpu_mipmap_times.total); const auto gpu_compression_pass = statistics(gpu_compression_pass_times.total); @@ -889,6 +1006,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { const auto gpu_compressed_upload = statistics(gpu_compressed_upload_times.total); const auto gpu_total = statistics(gpu_total_times); const auto algorithm_name = algorithm == nucleus::utils::ColourTexture::Format::DXT1 ? QStringLiteral("DXT1 / BC1") : QStringLiteral("ETC1 in ETC2"); + const auto cpu_backend_name = cpu_encoder_name(m_cpu_encoder); QJsonObject root { { QStringLiteral("renderer"), gl_string(GL_RENDERER) }, @@ -896,6 +1014,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { { QStringLiteral("version"), gl_string(GL_VERSION) }, { QStringLiteral("supported"), true }, { QStringLiteral("algorithm"), algorithm_name }, + { QStringLiteral("cpu_backend"), cpu_backend_name }, { QStringLiteral("gpu_backend"), backend_name }, { QStringLiteral("timing_method"), QStringLiteral("wall time; one final glFinish per end-to-end sample") }, { QStringLiteral("resolution"), int(resolution) }, @@ -905,12 +1024,19 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { { QStringLiteral("measurement_samples"), sample_count }, { QStringLiteral("warmup_rounds"), 1 }, { QStringLiteral("gpu_stage_profile_samples"), sample_count }, - { QStringLiteral("effort"), m_effort }, + { QStringLiteral("basis_quality"), m_basis_quality }, + { QStringLiteral("basis_effort"), m_basis_effort }, + { QStringLiteral("gpu_effort"), m_effort }, { QStringLiteral("mipmaps"), m_mipmaps }, { QStringLiteral("dataset"), dataset_json() }, { QStringLiteral("cpu_psnr_db_all_16_images"), cpu_psnr }, { QStringLiteral("gpu_psnr_db_all_16_images"), gpu_psnr }, { QStringLiteral("cpu_compression"), to_json(cpu_compression) }, + { QStringLiteral("cpu_source_preparation"), to_json(cpu_source_preparation) }, + { QStringLiteral("cpu_encoding"), to_json(cpu_encoding) }, + { QStringLiteral("cpu_transcoding"), to_json(cpu_transcoding) }, + { QStringLiteral("cpu_intermediate_size"), size_to_json(cpu_intermediate_size) }, + { QStringLiteral("cpu_transcoded_size"), size_to_json(cpu_transcoded_size) }, { QStringLiteral("cpu_compressed_upload"), to_json(cpu_upload) }, { QStringLiteral("cpu_end_to_end"), to_json(cpu_total) }, { QStringLiteral("gpu_scratch_upload"), to_json(gpu_upload) }, @@ -949,6 +1075,11 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { { QStringLiteral("raw_samples_ms"), QJsonObject { { QStringLiteral("cpu_compression"), samples_to_json(cpu_compression_times) }, + { QStringLiteral("cpu_source_preparation"), samples_to_json(cpu_source_preparation_times) }, + { QStringLiteral("cpu_encoding"), samples_to_json(cpu_encoding_times) }, + { QStringLiteral("cpu_transcoding"), samples_to_json(cpu_transcoding_times) }, + { QStringLiteral("cpu_intermediate_bytes"), samples_to_json(cpu_intermediate_bytes) }, + { QStringLiteral("cpu_transcoded_bytes"), samples_to_json(cpu_transcoded_bytes) }, { QStringLiteral("cpu_compressed_upload"), samples_to_json(cpu_upload_times) }, { QStringLiteral("cpu_end_to_end"), samples_to_json(cpu_total_times) }, { QStringLiteral("gpu_end_to_end"), samples_to_json(gpu_total_times) }, @@ -964,14 +1095,24 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { const auto line = [](QString label, Statistics value) { return QStringLiteral("%1 median %2 ms p95 %3 ms").arg(label, -26).arg(value.median, 8, 'f', 3).arg(value.p95, 8, 'f', 3); }; + const auto size_line = [](QString label, Statistics value) { + return QStringLiteral("%1 median %2 KiB p95 %3 KiB") + .arg(label, -26) + .arg(value.median / 1024.0, 8, 'f', 1) + .arg(value.p95 / 1024.0, 8, 'f', 1); + }; + const auto cpu_settings = m_cpu_encoder == BenchmarkItem::CpuEncoder::Goofy + ? QStringLiteral("CPU: %1").arg(cpu_backend_name) + : QStringLiteral("CPU: %1, quality %2, effort %3").arg(cpu_backend_name).arg(m_basis_quality).arg(m_basis_effort); QStringList summary { - QStringLiteral("%1 — %2 × %3, batch %4, 12 samples, effort %5, mipmaps %6") + QStringLiteral("%1 — %2 × %3, batch %4, 12 samples, GPU effort %5, mipmaps %6") .arg(algorithm_name) .arg(resolution) .arg(resolution) .arg(batch_size) .arg(m_effort) .arg(m_mipmaps ? QStringLiteral("on") : QStringLiteral("off")), + cpu_settings, backend_name, gl_string(GL_RENDERER), QStringLiteral("Quality first: one untimed 16-image pass"), @@ -981,6 +1122,11 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { QStringLiteral("Timing: one final glFinish per end-to-end sample"), QString(), line(QStringLiteral("CPU compression"), cpu_compression), + line(QStringLiteral(" source preparation"), cpu_source_preparation), + line(QStringLiteral(" CPU encoding"), cpu_encoding), + line(QStringLiteral(" CPU transcoding"), cpu_transcoding), + size_line(QStringLiteral(" Intermediate stream"), cpu_intermediate_size), + size_line(QStringLiteral(" GPU blocks"), cpu_transcoded_size), line(QStringLiteral("CPU compressed upload"), cpu_upload), line(QStringLiteral("CPU end-to-end"), cpu_total), QStringLiteral("GPU stages (separate serialised profiling pass)"), @@ -1047,6 +1193,9 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { QPointer m_item; QQuickWindow* m_window = nullptr; unsigned m_seen_serial = 0; + BenchmarkItem::CpuEncoder m_cpu_encoder = BenchmarkItem::CpuEncoder::Goofy; + int m_basis_quality = 75; + int m_basis_effort = 4; int m_effort = 4; bool m_mipmaps = true; bool m_pending = false; @@ -1065,6 +1214,35 @@ BenchmarkItem::BenchmarkItem(QQuickItem* parent) QQuickFramebufferObject::Renderer* BenchmarkItem::createRenderer() const { return new BenchmarkRenderer; } +BenchmarkItem::CpuEncoder BenchmarkItem::cpuEncoder() const { return m_cpu_encoder; } +void BenchmarkItem::setCpuEncoder(CpuEncoder value) +{ + if (m_cpu_encoder == value) + return; + m_cpu_encoder = value; + emit cpuEncoderChanged(); +} + +int BenchmarkItem::basisQuality() const { return m_basis_quality; } +void BenchmarkItem::setBasisQuality(int value) +{ + value = std::clamp(value, 1, 100); + if (m_basis_quality == value) + return; + m_basis_quality = value; + emit basisQualityChanged(); +} + +int BenchmarkItem::basisEffort() const { return m_basis_effort; } +void BenchmarkItem::setBasisEffort(int value) +{ + value = std::clamp(value, 0, 10); + if (m_basis_effort == value) + return; + m_basis_effort = value; + emit basisEffortChanged(); +} + int BenchmarkItem::effort() const { return m_effort; } void BenchmarkItem::setEffort(int value) { diff --git a/apps/texture_compression_benchmark/BenchmarkItem.h b/apps/texture_compression_benchmark/BenchmarkItem.h index 370ed588..73197a84 100644 --- a/apps/texture_compression_benchmark/BenchmarkItem.h +++ b/apps/texture_compression_benchmark/BenchmarkItem.h @@ -17,6 +17,9 @@ class QNetworkAccessManager; class BenchmarkItem : public QQuickFramebufferObject { Q_OBJECT QML_ELEMENT + Q_PROPERTY(CpuEncoder cpuEncoder READ cpuEncoder WRITE setCpuEncoder NOTIFY cpuEncoderChanged) + Q_PROPERTY(int basisQuality READ basisQuality WRITE setBasisQuality NOTIFY basisQualityChanged) + Q_PROPERTY(int basisEffort READ basisEffort WRITE setBasisEffort NOTIFY basisEffortChanged) Q_PROPERTY(int effort READ effort WRITE setEffort NOTIFY effortChanged) Q_PROPERTY(bool mipmaps READ mipmaps WRITE setMipmaps NOTIFY mipmapsChanged) Q_PROPERTY(bool dataReady READ dataReady NOTIFY dataReadyChanged) @@ -27,9 +30,18 @@ class BenchmarkItem : public QQuickFramebufferObject { Q_PROPERTY(QString previewSource READ previewSource NOTIFY previewSourceChanged) public: + enum class CpuEncoder { Goofy, BasisEtc1s, BasisUastcLdr4x4, BasisXuastcLdr4x4 }; + Q_ENUM(CpuEncoder) + explicit BenchmarkItem(QQuickItem* parent = nullptr); Renderer* createRenderer() const override; + [[nodiscard]] CpuEncoder cpuEncoder() const; + void setCpuEncoder(CpuEncoder value); + [[nodiscard]] int basisQuality() const; + void setBasisQuality(int value); + [[nodiscard]] int basisEffort() const; + void setBasisEffort(int value); [[nodiscard]] int effort() const; void setEffort(int value); [[nodiscard]] bool mipmaps() const; @@ -45,6 +57,9 @@ class BenchmarkItem : public QQuickFramebufferObject { Q_INVOKABLE void copyResultJson(); signals: + void cpuEncoderChanged(); + void basisQualityChanged(); + void basisEffortChanged(); void effortChanged(); void mipmapsChanged(); void dataReadyChanged(); @@ -60,6 +75,9 @@ class BenchmarkItem : public QQuickFramebufferObject { void stitchBenchmarkData(); void publishResults(const QString& text, const QString& json, const QString& preview_source); + CpuEncoder m_cpu_encoder = CpuEncoder::Goofy; + int m_basis_quality = 75; + int m_basis_effort = 4; int m_effort = 4; bool m_mipmaps = true; bool m_data_ready = false; diff --git a/apps/texture_compression_benchmark/CMakeLists.txt b/apps/texture_compression_benchmark/CMakeLists.txt index 492a8563..35490b36 100644 --- a/apps/texture_compression_benchmark/CMakeLists.txt +++ b/apps/texture_compression_benchmark/CMakeLists.txt @@ -6,6 +6,32 @@ project(texture-compression-benchmark LANGUAGES CXX) +alp_add_git_repository(basisu + URL https://github.com/BinomialLLC/basis_universal.git + COMMITISH 9bebe16726b3a61c8c213eeee3b7cffb462ef34e + DO_NOT_ADD_SUBPROJECT) +set(BASISU_EXAMPLES OFF CACHE BOOL "" FORCE) +set(BASISU_SSE OFF CACHE BOOL "" FORCE) +set(BASISU_ZSTD OFF CACHE BOOL "" FORCE) +set(BASISU_OPENCL OFF CACHE BOOL "" FORCE) +set(BASISU_DISABLE_ANDROID_ASTC_DECOMP ON CACHE BOOL "" FORCE) +add_subdirectory("${basisu_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/basisu" EXCLUDE_FROM_ALL) + +add_library(alp_basisu_texture_compression STATIC + ${CMAKE_SOURCE_DIR}/nucleus/utils/BasisUniversalTextureCompression.h + ${CMAKE_SOURCE_DIR}/nucleus/utils/BasisUniversalTextureCompression.cpp +) +target_include_directories(alp_basisu_texture_compression SYSTEM PRIVATE "${basisu_SOURCE_DIR}") +target_compile_definitions(alp_basisu_texture_compression PRIVATE + BASISD_SUPPORT_KTX2_ZSTD=0 + BASISU_SUPPORT_SSE=0 +) +target_link_libraries(alp_basisu_texture_compression PUBLIC nucleus PRIVATE basisu_encoder) +if (UNIX AND NOT ANDROID AND NOT EMSCRIPTEN) + target_link_libraries(alp_basisu_texture_compression PRIVATE m pthread) +endif() +alp_configure_target(alp_basisu_texture_compression) + qt_add_executable(texture_compression_benchmark main.cpp BenchmarkItem.h @@ -25,7 +51,7 @@ qt_add_resources(texture_compression_benchmark "fonts" FILES "${alpineapp_fonts_SOURCE_DIR}/Roboto/Roboto-Regular.ttf" ) -target_link_libraries(texture_compression_benchmark PUBLIC gl_engine Qt::Network Qt::Quick Qt::QuickControls2) +target_link_libraries(texture_compression_benchmark PUBLIC alp_basisu_texture_compression gl_engine Qt::Network Qt::Quick Qt::QuickControls2) alp_configure_target(texture_compression_benchmark) if (ANDROID) diff --git a/apps/texture_compression_benchmark/Main.qml b/apps/texture_compression_benchmark/Main.qml index f0050418..20674906 100644 --- a/apps/texture_compression_benchmark/Main.qml +++ b/apps/texture_compression_benchmark/Main.qml @@ -7,7 +7,7 @@ ApplicationWindow { id: root width: 900 - height: 760 + height: 900 minimumWidth: 360 minimumHeight: 640 visible: true @@ -47,7 +47,7 @@ ApplicationWindow { Layout.fillWidth: true Layout.leftMargin: 20 Layout.rightMargin: 20 - text: qsTr("Measures DXT1 or ETC1 compression of varied 512×512 ortho imagery in batches of four. Quality is computed once over all 16 images; timing covers three rounds of four distinct batches. WebGL requires ETC or sRGB S3TC compressed textures.") + text: qsTr("Compares direct CPU compression, Basis Universal encoding paths, and GPU compression of varied 512×512 ortho imagery in batches of four. Quality is computed once over all 16 images; timing covers three rounds of four distinct batches. WebGL requires ETC or sRGB S3TC compressed textures.") wrapMode: Text.Wrap } @@ -61,6 +61,60 @@ ApplicationWindow { anchors.fill: parent spacing: 12 + RowLayout { + Layout.fillWidth: true + + Label { + Layout.fillWidth: true + text: qsTr("CPU encoder") + } + + ComboBox { + Layout.preferredWidth: 260 + model: [ + qsTr("Goofy direct (baseline)"), + qsTr("BasisU ETC1S"), + qsTr("BasisU UASTC LDR 4×4"), + qsTr("BasisU XUASTC LDR 4×4") + ] + currentIndex: benchmark.cpuEncoder + enabled: !benchmark.running + onActivated: benchmark.cpuEncoder = currentIndex + } + } + + Label { + Layout.fillWidth: true + text: qsTr("BasisU quality: %1").arg(benchmark.basisQuality) + enabled: benchmark.cpuEncoder !== BenchmarkItem.Goofy + } + + Slider { + Layout.fillWidth: true + from: 1 + to: 100 + stepSize: 1 + value: benchmark.basisQuality + enabled: !benchmark.running && benchmark.cpuEncoder !== BenchmarkItem.Goofy + onMoved: benchmark.basisQuality = Math.round(value) + } + + Label { + Layout.fillWidth: true + text: qsTr("BasisU effort: %1").arg(benchmark.basisEffort) + enabled: benchmark.cpuEncoder !== BenchmarkItem.Goofy + } + + Slider { + Layout.fillWidth: true + from: 0 + to: 10 + stepSize: 1 + value: benchmark.basisEffort + enabled: !benchmark.running && benchmark.cpuEncoder !== BenchmarkItem.Goofy + onMoved: benchmark.basisEffort = Math.round(value) + } + Label { Layout.fillWidth: true text: qsTr("GPU effort: %1").arg(benchmark.effort) @@ -123,7 +177,7 @@ ApplicationWindow { Label { Layout.fillWidth: true text: benchmark.running - ? qsTr("Computing PSNR, then measuring batch size 4; the display may pause to avoid contaminating GPU measurements.") + ? qsTr("Computing PSNR, BasisU encoding/transcoding, and batch timings; the display may pause to avoid contaminating GPU measurements.") : benchmark.dataStatus wrapMode: Text.Wrap } @@ -152,7 +206,7 @@ ApplicationWindow { Layout.fillWidth: true Button { - text: qsTr("Preview compressed tiles") + text: qsTr("Preview selected CPU encoder") enabled: !benchmark.running && benchmark.previewSource.length > 0 onClicked: previewDialogLoader.active = true } @@ -187,7 +241,7 @@ ApplicationWindow { width: Math.min(root.width - 40, 820) height: Math.min(root.height - 40, 880) modal: true - title: qsTr("GPU-compressed tiles") + title: qsTr("Selected CPU-compressed tiles") standardButtons: Dialog.Close onClosed: previewDialogLoader.active = false diff --git a/nucleus/CMakeLists.txt b/nucleus/CMakeLists.txt index 77581fff..6755210b 100644 --- a/nucleus/CMakeLists.txt +++ b/nucleus/CMakeLists.txt @@ -29,8 +29,10 @@ if(ALP_ENABLE_LABELS) endif() alp_add_git_repository(goofy_tc URL https://github.com/AlpineMapsOrgDependencies/Goofy_slim.git COMMITISH 13b228784960a6227bb6ca704ff34161bbac1b91 DO_NOT_ADD_SUBPROJECT) alp_add_git_repository(cdt URL https://github.com/artem-ogre/CDT.git COMMITISH 46f1ce1f495a97617d90e8c833d0d29406335fdf DO_NOT_ADD_SUBPROJECT) -include(${CMAKE_SOURCE_DIR}/cmake/SetupKTX.cmake) -alp_setup_ktx(952d74f1d53452e4e976a2b7698ff7af6c13a9ed) +if (ALP_BUILD_WEBGPU_APP) + include(${CMAKE_SOURCE_DIR}/cmake/SetupKTX.cmake) + alp_setup_ktx(952d74f1d53452e4e976a2b7698ff7af6c13a9ed) +endif() add_library(zppbits INTERFACE) target_include_directories(zppbits SYSTEM INTERFACE ${zppbits_SOURCE_DIR}) @@ -115,7 +117,6 @@ qt_add_library(nucleus STATIC tile/setup.h tile/GpuArrayHelper.h tile/GpuArrayHelper.cpp tile/TextureScheduler.h tile/TextureScheduler.cpp - tile/Texture3DScheduler.h tile/Texture3DScheduler.cpp tile/GeometryScheduler.h tile/GeometryScheduler.cpp utils/easing.h utils/error.h @@ -126,6 +127,10 @@ qt_add_library(nucleus STATIC camera/gesture.h ) +if (ALP_BUILD_WEBGPU_APP) + target_sources(nucleus PRIVATE tile/Texture3DScheduler.h tile/Texture3DScheduler.cpp) +endif() + if (ALP_ENABLE_AVALANCHE_WARNING_LAYER) target_sources(nucleus PRIVATE @@ -177,7 +182,10 @@ endif() target_include_directories(nucleus PUBLIC ${CMAKE_SOURCE_DIR}) # Please keep Qt::Gui outside the nucleus. If you need it optional via a cmake based switch -target_link_libraries(nucleus PUBLIC radix Qt::Core Qt::Network zppbits nucleus_version stb_slim goofy_tc cdt KTX::ktx) +target_link_libraries(nucleus PUBLIC radix Qt::Core Qt::Network zppbits nucleus_version stb_slim goofy_tc cdt) +if (ALP_BUILD_WEBGPU_APP) + target_link_libraries(nucleus PUBLIC KTX::ktx) +endif() qt_add_resources(nucleus "height_data" PREFIX "/map" diff --git a/nucleus/utils/BasisUniversalTextureCompression.cpp b/nucleus/utils/BasisUniversalTextureCompression.cpp new file mode 100644 index 00000000..0be5a6ed --- /dev/null +++ b/nucleus/utils/BasisUniversalTextureCompression.cpp @@ -0,0 +1,175 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2026 Adam Celarek + * SPDX-License-Identifier: GPL-3.0-or-later + *****************************************************************************/ + +#include "BasisUniversalTextureCompression.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using Clock = std::chrono::steady_clock; + +double elapsed_ms(Clock::time_point start) +{ + return std::chrono::duration(Clock::now() - start).count(); +} + +basist::basis_tex_format basis_format(nucleus::utils::BasisUniversalFormat format) +{ + using Format = nucleus::utils::BasisUniversalFormat; + switch (format) { + case Format::ETC1S: + return basist::basis_tex_format::cETC1S; + case Format::UASTC_LDR_4x4: + return basist::basis_tex_format::cUASTC_LDR_4x4; + case Format::XUASTC_LDR_4x4: + return basist::basis_tex_format::cXUASTC_LDR_4x4; + } + return basist::basis_tex_format::cETC1S; +} + +basist::transcoder_texture_format transcoder_format(nucleus::utils::ColourTexture::Format format) +{ + using Format = nucleus::utils::ColourTexture::Format; + switch (format) { + case Format::DXT1: + return basist::transcoder_texture_format::cTFBC1_RGB; + case Format::ETC1: + return basist::transcoder_texture_format::cTFETC1_RGB; + case Format::Uncompressed_RGBA: + break; + } + return basist::transcoder_texture_format::cTFRGBA32; +} + +std::vector> mip_levels( + const radix::Raster& source, bool generate_mipmaps) +{ + if (!generate_mipmaps) + return { source }; + auto levels = radix::raster::generate_mipmap(source); + return levels ? std::move(*levels) : std::vector> {}; +} + +basisu::vector basis_images(const std::vector>& levels) +{ + static_assert(sizeof(basisu::color_rgba) == sizeof(glm::u8vec4)); + basisu::vector result; + result.reserve(levels.size()); + for (const auto& level : levels) { + basisu::image image(level.width(), level.height()); + std::memcpy(image.get_ptr(), level.bytes().data(), level.size_in_bytes()); + result.push_back(std::move(image)); + } + return result; +} + +void initialise_basis_universal() +{ + static std::once_flag flag; + std::call_once(flag, [] { basisu::basisu_encoder_init(); }); +} + +} // namespace + +const char* nucleus::utils::basis_universal_format_name(BasisUniversalFormat format) +{ + switch (format) { + case BasisUniversalFormat::ETC1S: + return "BasisU ETC1S"; + case BasisUniversalFormat::UASTC_LDR_4x4: + return "BasisU UASTC LDR 4x4"; + case BasisUniversalFormat::XUASTC_LDR_4x4: + return "BasisU XUASTC LDR 4x4"; + } + return "BasisU unknown"; +} + +std::expected +nucleus::utils::compress_with_basis_universal( + const radix::Raster& source, const BasisUniversalCompressionSettings& settings) +{ + if (source.buffer().empty()) + return std::unexpected("Cannot compress an empty image"); + if (settings.target_format == ColourTexture::Format::Uncompressed_RGBA) + return std::unexpected("Basis Universal target must be BC1 or ETC1"); + if (source.width() > int(basist::BASISU_MAX_SUPPORTED_TEXTURE_DIMENSION) + || source.height() > int(basist::BASISU_MAX_SUPPORTED_TEXTURE_DIMENSION)) { + return std::unexpected("Image exceeds Basis Universal's maximum dimensions"); + } + + initialise_basis_universal(); + BasisUniversalCompressionResult result; + + const auto preparation_start = Clock::now(); + const auto levels = mip_levels(source, settings.generate_mipmaps); + if (levels.empty()) + return std::unexpected("Basis Universal requires power-of-two mipmap input"); + auto images = basis_images(levels); + result.timings.source_preparation_ms = elapsed_ms(preparation_start); + + const auto encoding_start = Clock::now(); + size_t encoded_size = 0; + const auto flags = uint32_t(basisu::cFlagSRGB); + void* encoded = basisu::basis_compress2(basis_format(settings.format), + images, + flags, + std::clamp(settings.quality, 1, 100), + std::clamp(settings.effort, 0, 10), + &encoded_size); + result.timings.encoding_ms = elapsed_ms(encoding_start); + if (!encoded) + return std::unexpected("Basis Universal encoding failed"); + const auto encoded_deleter = [](void* data) { basisu::basis_free_data(data); }; + std::unique_ptr encoded_owner(encoded, encoded_deleter); + result.intermediate_bytes = encoded_size; + if (encoded_size > std::numeric_limits::max()) + return std::unexpected("Basis Universal output is too large to transcode"); + + const auto transcoding_start = Clock::now(); + basist::basisu_transcoder transcoder; + const auto encoded_size_u32 = uint32_t(encoded_size); + if (!transcoder.validate_header(encoded, encoded_size_u32)) + return std::unexpected("Basis Universal produced an invalid header"); + if (transcoder.get_total_images(encoded, encoded_size_u32) != 1) + return std::unexpected("Basis Universal produced an unexpected image count"); + if (!transcoder.start_transcoding(encoded, encoded_size_u32)) + return std::unexpected("Basis Universal transcoder initialisation failed"); + + const auto level_count = transcoder.get_total_image_levels(encoded, encoded_size_u32, 0); + if (level_count != levels.size()) + return std::unexpected("Basis Universal produced an unexpected mip level count"); + result.texture.reserve(level_count); + for (uint32_t level_index = 0; level_index < level_count; ++level_index) { + basist::basisu_image_level_info info; + if (!transcoder.get_image_level_info(encoded, encoded_size_u32, info, 0, level_index)) + return std::unexpected("Unable to inspect a Basis Universal mip level"); + const auto block_count = ((info.m_orig_width + 3u) / 4u) * ((info.m_orig_height + 3u) / 4u); + std::vector blocks(size_t(block_count) * 8u); + if (!transcoder.transcode_image_level(encoded, + encoded_size_u32, + 0, + level_index, + blocks.data(), + block_count, + transcoder_format(settings.target_format))) { + return std::unexpected("Basis Universal transcoding failed"); + } + result.transcoded_bytes += blocks.size(); + result.texture.emplace_back(std::move(blocks), info.m_orig_width, info.m_orig_height, settings.target_format); + } + result.timings.transcoding_ms = elapsed_ms(transcoding_start); + return result; +} diff --git a/nucleus/utils/BasisUniversalTextureCompression.h b/nucleus/utils/BasisUniversalTextureCompression.h new file mode 100644 index 00000000..680008e3 --- /dev/null +++ b/nucleus/utils/BasisUniversalTextureCompression.h @@ -0,0 +1,46 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2026 Adam Celarek + * SPDX-License-Identifier: GPL-3.0-or-later + *****************************************************************************/ + +#pragma once + +#include "ColourTexture.h" + +#include +#include +#include + +namespace nucleus::utils { + +enum class BasisUniversalFormat { ETC1S, UASTC_LDR_4x4, XUASTC_LDR_4x4 }; + +struct BasisUniversalCompressionSettings { + BasisUniversalFormat format = BasisUniversalFormat::ETC1S; + ColourTexture::Format target_format = ColourTexture::Format::DXT1; + int quality = 75; + int effort = 4; + bool generate_mipmaps = true; +}; + +struct BasisUniversalCompressionTimings { + double source_preparation_ms = 0.0; + double encoding_ms = 0.0; + double transcoding_ms = 0.0; + + [[nodiscard]] double total_ms() const { return source_preparation_ms + encoding_ms + transcoding_ms; } +}; + +struct BasisUniversalCompressionResult { + MipmappedColourTexture texture; + BasisUniversalCompressionTimings timings; + size_t intermediate_bytes = 0; + size_t transcoded_bytes = 0; +}; + +[[nodiscard]] const char* basis_universal_format_name(BasisUniversalFormat format); +[[nodiscard]] std::expected compress_with_basis_universal( + const radix::Raster& source, const BasisUniversalCompressionSettings& settings); + +} // namespace nucleus::utils diff --git a/nucleus/utils/ColourTexture.cpp b/nucleus/utils/ColourTexture.cpp index 74a3ca25..ce6384dc 100644 --- a/nucleus/utils/ColourTexture.cpp +++ b/nucleus/utils/ColourTexture.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #define GOOFYTC_IMPLEMENTATION #include @@ -159,6 +160,14 @@ nucleus::utils::ColourTexture::ColourTexture(const radix::Raster& i { } +nucleus::utils::ColourTexture::ColourTexture(std::vector data, unsigned width, unsigned height, Format format) + : m_data(std::move(data)) + , m_width(width) + , m_height(height) + , m_format(format) +{ +} + nucleus::utils::MipmappedColourTexture nucleus::utils::generate_mipmapped_colour_texture( const radix::Raster& texture, ColourTexture::Format format) { diff --git a/nucleus/utils/ColourTexture.h b/nucleus/utils/ColourTexture.h index 1df28a6b..158d3748 100644 --- a/nucleus/utils/ColourTexture.h +++ b/nucleus/utils/ColourTexture.h @@ -37,6 +37,7 @@ class ColourTexture { public: explicit ColourTexture(const radix::Raster& data, Format format); + ColourTexture(std::vector data, unsigned width, unsigned height, Format format); [[nodiscard]] const uint8_t* data() const { return m_data.data(); } [[nodiscard]] size_t n_bytes() const { return m_data.size(); } [[nodiscard]] unsigned width() const { return m_width; } diff --git a/unittests/nucleus/CMakeLists.txt b/unittests/nucleus/CMakeLists.txt index 867b648f..9362ac34 100644 --- a/unittests/nucleus/CMakeLists.txt +++ b/unittests/nucleus/CMakeLists.txt @@ -90,6 +90,11 @@ if (ALP_ENABLE_AVALANCHE_WARNING_LAYER) data/eaws_7-67-45.mvt ) endif() + +if (TARGET alp_basisu_texture_compression) + target_sources(unittests_nucleus PRIVATE basis_universal_texture_compression.cpp) + target_link_libraries(unittests_nucleus PUBLIC alp_basisu_texture_compression) +endif() target_link_libraries(unittests_nucleus PUBLIC nucleus Catch2::Catch2 Qt::Test Qt::Gui) target_compile_definitions(unittests_nucleus PUBLIC "ALP_TEST_DATA_DIR=\":/test_data/\"") diff --git a/unittests/nucleus/basis_universal_texture_compression.cpp b/unittests/nucleus/basis_universal_texture_compression.cpp new file mode 100644 index 00000000..8bdf5c23 --- /dev/null +++ b/unittests/nucleus/basis_universal_texture_compression.cpp @@ -0,0 +1,51 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2026 Adam Celarek + * SPDX-License-Identifier: GPL-3.0-or-later + *****************************************************************************/ + +#include + +#include + +#include + +TEST_CASE("nucleus/basis_universal_texture_compression: transcodes all LDR paths") +{ + using nucleus::utils::BasisUniversalCompressionSettings; + using nucleus::utils::BasisUniversalFormat; + using nucleus::utils::ColourTexture; + + radix::Raster source(glm::uvec2(16), glm::u8vec4(0, 0, 0, 255)); + for (unsigned y = 0; y < source.height(); ++y) { + for (unsigned x = 0; x < source.width(); ++x) + source.pixel({ x, y }) = glm::u8vec4(x * 16, y * 16, (x + y) * 8, 255); + } + + constexpr std::array formats { + BasisUniversalFormat::ETC1S, + BasisUniversalFormat::UASTC_LDR_4x4, + BasisUniversalFormat::XUASTC_LDR_4x4, + }; + constexpr std::array targets { ColourTexture::Format::DXT1, ColourTexture::Format::ETC1 }; + for (const auto format : formats) { + for (const auto target : targets) { + INFO(nucleus::utils::basis_universal_format_name(format)); + const auto result = nucleus::utils::compress_with_basis_universal(source, + BasisUniversalCompressionSettings { + .format = format, + .target_format = target, + .quality = 50, + .effort = 0, + .generate_mipmaps = true, + }); + REQUIRE(result); + CHECK(result->texture.size() == 5); + CHECK(result->texture.front().width() == 16); + CHECK(result->texture.front().height() == 16); + CHECK(result->texture.front().format() == target); + CHECK(result->intermediate_bytes > 0); + CHECK(result->transcoded_bytes == 184); + } + } +} From 10c845f202874d7cb3d3880285b2f24dabad3b9c Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Thu, 20 Aug 2026 01:57:31 +0200 Subject: [PATCH 14/38] Add fast GPU ETC1 encoder --- .../BenchmarkItem.cpp | 19 ++++- .../BenchmarkItem.h | 7 ++ apps/texture_compression_benchmark/Main.qml | 15 ++-- gl_engine/Texture.cpp | 11 ++- gl_engine/Texture.h | 2 + gl_engine/shaders/texture_compress.vert | 69 +++++++++++++++++++ 6 files changed, 117 insertions(+), 6 deletions(-) diff --git a/apps/texture_compression_benchmark/BenchmarkItem.cpp b/apps/texture_compression_benchmark/BenchmarkItem.cpp index 4301dbb2..34a2acbf 100644 --- a/apps/texture_compression_benchmark/BenchmarkItem.cpp +++ b/apps/texture_compression_benchmark/BenchmarkItem.cpp @@ -482,6 +482,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { m_cpu_encoder = benchmark_item->m_cpu_encoder; m_basis_quality = benchmark_item->m_basis_quality; m_basis_effort = benchmark_item->m_basis_effort; + m_gpu_encoder = benchmark_item->m_gpu_encoder; m_effort = benchmark_item->m_effort; m_mipmaps = benchmark_item->m_mipmaps; m_source_images = benchmark_item->m_source_images; @@ -835,11 +836,16 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { } const auto algorithm = gl_engine::Texture::compression_algorithm(); - const auto backend_name = QStringLiteral("Fragment shader + PBO"); + const auto backend_name = m_gpu_encoder == BenchmarkItem::GpuEncoder::FastRange + ? QStringLiteral("Fragment shader fast range + PBO") + : QStringLiteral("Fragment shader search + PBO"); const auto filter = m_mipmaps ? gl_engine::Texture::Filter::MipMapLinear : gl_engine::Texture::Filter::Linear; const gl_engine::TextureCompressor::Settings gpu_settings { .algorithm = algorithm, .effort = unsigned(m_effort), + .encoder = m_gpu_encoder == BenchmarkItem::GpuEncoder::FastRange + ? gl_engine::TextureCompressor::Encoder::FastRange + : gl_engine::TextureCompressor::Encoder::Search, .generate_mipmaps = m_mipmaps, .timing_mode = gl_engine::TextureCompressor::TimingMode::EndToEnd, }; @@ -1026,6 +1032,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { { QStringLiteral("gpu_stage_profile_samples"), sample_count }, { QStringLiteral("basis_quality"), m_basis_quality }, { QStringLiteral("basis_effort"), m_basis_effort }, + { QStringLiteral("gpu_encoder"), backend_name }, { QStringLiteral("gpu_effort"), m_effort }, { QStringLiteral("mipmaps"), m_mipmaps }, { QStringLiteral("dataset"), dataset_json() }, @@ -1196,6 +1203,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { BenchmarkItem::CpuEncoder m_cpu_encoder = BenchmarkItem::CpuEncoder::Goofy; int m_basis_quality = 75; int m_basis_effort = 4; + BenchmarkItem::GpuEncoder m_gpu_encoder = BenchmarkItem::GpuEncoder::FastRange; int m_effort = 4; bool m_mipmaps = true; bool m_pending = false; @@ -1243,6 +1251,15 @@ void BenchmarkItem::setBasisEffort(int value) emit basisEffortChanged(); } +BenchmarkItem::GpuEncoder BenchmarkItem::gpuEncoder() const { return m_gpu_encoder; } +void BenchmarkItem::setGpuEncoder(GpuEncoder value) +{ + if (m_gpu_encoder == value) + return; + m_gpu_encoder = value; + emit gpuEncoderChanged(); +} + int BenchmarkItem::effort() const { return m_effort; } void BenchmarkItem::setEffort(int value) { diff --git a/apps/texture_compression_benchmark/BenchmarkItem.h b/apps/texture_compression_benchmark/BenchmarkItem.h index 73197a84..fd35e8e2 100644 --- a/apps/texture_compression_benchmark/BenchmarkItem.h +++ b/apps/texture_compression_benchmark/BenchmarkItem.h @@ -20,6 +20,7 @@ class BenchmarkItem : public QQuickFramebufferObject { Q_PROPERTY(CpuEncoder cpuEncoder READ cpuEncoder WRITE setCpuEncoder NOTIFY cpuEncoderChanged) Q_PROPERTY(int basisQuality READ basisQuality WRITE setBasisQuality NOTIFY basisQualityChanged) Q_PROPERTY(int basisEffort READ basisEffort WRITE setBasisEffort NOTIFY basisEffortChanged) + Q_PROPERTY(GpuEncoder gpuEncoder READ gpuEncoder WRITE setGpuEncoder NOTIFY gpuEncoderChanged) Q_PROPERTY(int effort READ effort WRITE setEffort NOTIFY effortChanged) Q_PROPERTY(bool mipmaps READ mipmaps WRITE setMipmaps NOTIFY mipmapsChanged) Q_PROPERTY(bool dataReady READ dataReady NOTIFY dataReadyChanged) @@ -32,6 +33,8 @@ class BenchmarkItem : public QQuickFramebufferObject { public: enum class CpuEncoder { Goofy, BasisEtc1s, BasisUastcLdr4x4, BasisXuastcLdr4x4 }; Q_ENUM(CpuEncoder) + enum class GpuEncoder { Search, FastRange }; + Q_ENUM(GpuEncoder) explicit BenchmarkItem(QQuickItem* parent = nullptr); Renderer* createRenderer() const override; @@ -42,6 +45,8 @@ class BenchmarkItem : public QQuickFramebufferObject { void setBasisQuality(int value); [[nodiscard]] int basisEffort() const; void setBasisEffort(int value); + [[nodiscard]] GpuEncoder gpuEncoder() const; + void setGpuEncoder(GpuEncoder value); [[nodiscard]] int effort() const; void setEffort(int value); [[nodiscard]] bool mipmaps() const; @@ -60,6 +65,7 @@ class BenchmarkItem : public QQuickFramebufferObject { void cpuEncoderChanged(); void basisQualityChanged(); void basisEffortChanged(); + void gpuEncoderChanged(); void effortChanged(); void mipmapsChanged(); void dataReadyChanged(); @@ -78,6 +84,7 @@ class BenchmarkItem : public QQuickFramebufferObject { CpuEncoder m_cpu_encoder = CpuEncoder::Goofy; int m_basis_quality = 75; int m_basis_effort = 4; + GpuEncoder m_gpu_encoder = GpuEncoder::FastRange; int m_effort = 4; bool m_mipmaps = true; bool m_data_ready = false; diff --git a/apps/texture_compression_benchmark/Main.qml b/apps/texture_compression_benchmark/Main.qml index 20674906..45fc71b4 100644 --- a/apps/texture_compression_benchmark/Main.qml +++ b/apps/texture_compression_benchmark/Main.qml @@ -118,6 +118,7 @@ ApplicationWindow { Label { Layout.fillWidth: true text: qsTr("GPU effort: %1").arg(benchmark.effort) + enabled: benchmark.gpuEncoder === BenchmarkItem.Search } Slider { @@ -127,7 +128,7 @@ ApplicationWindow { to: 10 stepSize: 1 value: benchmark.effort - enabled: !benchmark.running + enabled: !benchmark.running && benchmark.gpuEncoder === BenchmarkItem.Search onMoved: benchmark.effort = Math.round(value) } @@ -139,9 +140,15 @@ ApplicationWindow { text: qsTr("GPU encoder") } - Label { - Layout.preferredWidth: 220 - text: qsTr("Fragment shader + PBO") + ComboBox { + Layout.preferredWidth: 260 + model: [ + qsTr("Search (reference)"), + qsTr("Fast range (Goofy-inspired)") + ] + currentIndex: benchmark.gpuEncoder + enabled: !benchmark.running + onActivated: benchmark.gpuEncoder = currentIndex } } diff --git a/gl_engine/Texture.cpp b/gl_engine/Texture.cpp index b4959368..4b3066ec 100644 --- a/gl_engine/Texture.cpp +++ b/gl_engine/Texture.cpp @@ -693,6 +693,7 @@ struct gl_engine::TextureCompressor::Impl { GLuint packing_renderbuffer = 0; std::unique_ptr dxt1_fragment_program; std::unique_ptr etc1_fragment_program; + std::unique_ptr etc1_fast_fragment_program; std::unique_ptr packing_program; Impl(unsigned texture_width, unsigned texture_height, unsigned maximum_batch_size) @@ -775,6 +776,10 @@ struct gl_engine::TextureCompressor::Impl { "texture_compress.vert", ShaderCodeSource::FILE, std::vector { QStringLiteral("#define ALP_COMPRESS_ETC1") }); + etc1_fast_fragment_program = std::make_unique("texture_compress_raster.vert", + "texture_compress.vert", + ShaderCodeSource::FILE, + std::vector { QStringLiteral("#define ALP_COMPRESS_ETC1"), QStringLiteral("#define ALP_COMPRESS_ETC1_FAST") }); packing_program = std::make_unique( "texture_compress_raster.vert", "texture_compress_pack.frag", ShaderCodeSource::FILE); @@ -784,6 +789,7 @@ struct gl_engine::TextureCompressor::Impl { { dxt1_fragment_program.reset(); etc1_fragment_program.reset(); + etc1_fast_fragment_program.reset(); packing_program.reset(); if (!QOpenGLContext::currentContext()) return; @@ -946,7 +952,10 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: GLboolean scissor_enabled = GL_FALSE; result.timings.compression_pass = measure_stage(GpuTimer::Stage::CompressionPass, [&]() { - auto* program = settings.algorithm == nucleus::utils::ColourTexture::Format::DXT1 ? m->dxt1_fragment_program.get() : m->etc1_fragment_program.get(); + auto* program = settings.algorithm == nucleus::utils::ColourTexture::Format::DXT1 + ? m->dxt1_fragment_program.get() + : settings.encoder == Encoder::FastRange ? m->etc1_fast_fragment_program.get() + : m->etc1_fragment_program.get(); f->glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previous_draw_framebuffer); f->glGetIntegerv(GL_VIEWPORT, previous_viewport); f->glGetBooleanv(GL_COLOR_WRITEMASK, previous_colour_mask); diff --git a/gl_engine/Texture.h b/gl_engine/Texture.h index c90d5d1b..01b64124 100644 --- a/gl_engine/Texture.h +++ b/gl_engine/Texture.h @@ -93,6 +93,7 @@ class Texture { class TextureCompressor { public: enum class TimingMode { EndToEnd, IndividualStages, SubmissionOnly }; + enum class Encoder { Search, FastRange }; struct GpuTimings { double scratch_upload_ms = 0.0; @@ -135,6 +136,7 @@ class TextureCompressor { struct Settings { nucleus::utils::ColourTexture::Format algorithm = nucleus::utils::ColourTexture::Format::DXT1; unsigned effort = 0; + Encoder encoder = Encoder::Search; bool generate_mipmaps = true; TimingMode timing_mode = TimingMode::EndToEnd; GpuTimer* gpu_timer = nullptr; diff --git a/gl_engine/shaders/texture_compress.vert b/gl_engine/shaders/texture_compress.vert index ef6c1a6d..7da577b5 100644 --- a/gl_engine/shaders/texture_compress.vert +++ b/gl_engine/shaders/texture_compress.vert @@ -108,6 +108,71 @@ highp int modifier(highp int table, highp int index) return modifiers[table][index]; } +highp int brightness(highp uvec3 colour) +{ + return int((colour.r + 2u * colour.g + colour.b + 2u) / 4u); +} + +highp int table_for_range(highp int range) +{ + if (range < 22) + return 0; + if (range < 44) + return 1; + if (range < 74) + return 2; + if (range < 106) + return 3; + if (range < 152) + return 4; + if (range < 182) + return 5; + if (range < 254) + return 6; + return 7; +} + +highp uvec2 encode_etc1_fast(highp uvec3 pixels[16]) +{ + highp uvec3 minimum_colour = uvec3(255u); + highp uvec3 maximum_colour = uvec3(0u); + highp uvec3 sum = uvec3(0u); + for (int i = 0; i < 16; ++i) { + minimum_colour = min(minimum_colour, pixels[i]); + maximum_colour = max(maximum_colour, pixels[i]); + sum += pixels[i]; + } + + highp int minimum_brightness = brightness(minimum_colour); + highp int maximum_brightness = brightness(maximum_colour); + highp int range = max(8, maximum_brightness - minimum_brightness); + highp int middle = (minimum_brightness + maximum_brightness + 1) / 2; + highp ivec3 average = ivec3((sum + 8u) / 16u); + highp int correction = middle - brightness(uvec3(average)); + highp ivec3 adjusted = clamp(average + ivec3(correction), ivec3(0), ivec3(255)); + highp uvec3 base5 = (uvec3(adjusted) * 31u + 127u) / 255u; + + highp int table = table_for_range(range); + highp int threshold = (range * 3 + 4) / 8; + highp uint indices = 0u; + for (int y = 0; y < 4; ++y) { + for (int x = 0; x < 4; ++x) { + highp int pixel_index = y * 4 + x; + highp int delta = brightness(pixels[pixel_index]) - middle; + highp uint selected = uint(abs(delta) >= threshold); + if (delta < 0) + selected += 2u; + highp uint bit_position = uint(x * 4 + y); + indices |= (selected & 1u) << bit_position; + indices |= (selected >> 1u) << (bit_position + 16u); + } + } + + highp uint control = uint(table) << 5u | uint(table) << 2u | 2u; + highp uint header = base5.r << 3u | base5.g << 11u | base5.b << 19u | control << 24u; + return uvec2(header, byte_swap(indices)); +} + highp uvec2 encode_etc1(highp uvec3 pixels[16]) { highp uvec3 sum = uvec3(0u); @@ -181,7 +246,11 @@ highp uvec2 compress_block(highp ivec2 block, } #ifdef ALP_COMPRESS_ETC1 +#ifdef ALP_COMPRESS_ETC1_FAST + return encode_etc1_fast(pixels); +#else return encode_etc1(pixels); +#endif #else return encode_dxt1(pixels); #endif From d313fe6085749665b844fe605f27f537f6ff0f62 Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Thu, 20 Aug 2026 07:16:06 +0200 Subject: [PATCH 15/38] Add high-quality fast split ETC1 encoder --- .../BenchmarkItem.cpp | 19 +++- .../BenchmarkItem.h | 4 +- apps/texture_compression_benchmark/Main.qml | 3 +- gl_engine/Texture.cpp | 19 +++- gl_engine/Texture.h | 2 +- gl_engine/shaders/texture_compress.vert | 107 +++++++++++++++++- 6 files changed, 140 insertions(+), 14 deletions(-) diff --git a/apps/texture_compression_benchmark/BenchmarkItem.cpp b/apps/texture_compression_benchmark/BenchmarkItem.cpp index 34a2acbf..e1246fc0 100644 --- a/apps/texture_compression_benchmark/BenchmarkItem.cpp +++ b/apps/texture_compression_benchmark/BenchmarkItem.cpp @@ -836,16 +836,25 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { } const auto algorithm = gl_engine::Texture::compression_algorithm(); - const auto backend_name = m_gpu_encoder == BenchmarkItem::GpuEncoder::FastRange - ? QStringLiteral("Fragment shader fast range + PBO") - : QStringLiteral("Fragment shader search + PBO"); + const auto backend_name = [this]() { + switch (m_gpu_encoder) { + case BenchmarkItem::GpuEncoder::FastRange: + return QStringLiteral("Fragment shader fast range + PBO"); + case BenchmarkItem::GpuEncoder::FastSplit: + return QStringLiteral("Fragment shader fast split + PBO"); + case BenchmarkItem::GpuEncoder::Search: + return QStringLiteral("Fragment shader search + PBO"); + } + return QStringLiteral("Fragment shader search + PBO"); + }(); const auto filter = m_mipmaps ? gl_engine::Texture::Filter::MipMapLinear : gl_engine::Texture::Filter::Linear; const gl_engine::TextureCompressor::Settings gpu_settings { .algorithm = algorithm, .effort = unsigned(m_effort), .encoder = m_gpu_encoder == BenchmarkItem::GpuEncoder::FastRange ? gl_engine::TextureCompressor::Encoder::FastRange - : gl_engine::TextureCompressor::Encoder::Search, + : m_gpu_encoder == BenchmarkItem::GpuEncoder::FastSplit ? gl_engine::TextureCompressor::Encoder::FastSplit + : gl_engine::TextureCompressor::Encoder::Search, .generate_mipmaps = m_mipmaps, .timing_mode = gl_engine::TextureCompressor::TimingMode::EndToEnd, }; @@ -1203,7 +1212,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { BenchmarkItem::CpuEncoder m_cpu_encoder = BenchmarkItem::CpuEncoder::Goofy; int m_basis_quality = 75; int m_basis_effort = 4; - BenchmarkItem::GpuEncoder m_gpu_encoder = BenchmarkItem::GpuEncoder::FastRange; + BenchmarkItem::GpuEncoder m_gpu_encoder = BenchmarkItem::GpuEncoder::FastSplit; int m_effort = 4; bool m_mipmaps = true; bool m_pending = false; diff --git a/apps/texture_compression_benchmark/BenchmarkItem.h b/apps/texture_compression_benchmark/BenchmarkItem.h index fd35e8e2..fe9dba6d 100644 --- a/apps/texture_compression_benchmark/BenchmarkItem.h +++ b/apps/texture_compression_benchmark/BenchmarkItem.h @@ -33,7 +33,7 @@ class BenchmarkItem : public QQuickFramebufferObject { public: enum class CpuEncoder { Goofy, BasisEtc1s, BasisUastcLdr4x4, BasisXuastcLdr4x4 }; Q_ENUM(CpuEncoder) - enum class GpuEncoder { Search, FastRange }; + enum class GpuEncoder { Search, FastRange, FastSplit }; Q_ENUM(GpuEncoder) explicit BenchmarkItem(QQuickItem* parent = nullptr); @@ -84,7 +84,7 @@ class BenchmarkItem : public QQuickFramebufferObject { CpuEncoder m_cpu_encoder = CpuEncoder::Goofy; int m_basis_quality = 75; int m_basis_effort = 4; - GpuEncoder m_gpu_encoder = GpuEncoder::FastRange; + GpuEncoder m_gpu_encoder = GpuEncoder::FastSplit; int m_effort = 4; bool m_mipmaps = true; bool m_data_ready = false; diff --git a/apps/texture_compression_benchmark/Main.qml b/apps/texture_compression_benchmark/Main.qml index 45fc71b4..3bcb60fe 100644 --- a/apps/texture_compression_benchmark/Main.qml +++ b/apps/texture_compression_benchmark/Main.qml @@ -144,7 +144,8 @@ ApplicationWindow { Layout.preferredWidth: 260 model: [ qsTr("Search (reference)"), - qsTr("Fast range (Goofy-inspired)") + qsTr("Fast range (Goofy-inspired)"), + qsTr("Fast split (two sub-blocks)") ] currentIndex: benchmark.gpuEncoder enabled: !benchmark.running diff --git a/gl_engine/Texture.cpp b/gl_engine/Texture.cpp index 4b3066ec..6d86e1b7 100644 --- a/gl_engine/Texture.cpp +++ b/gl_engine/Texture.cpp @@ -694,6 +694,7 @@ struct gl_engine::TextureCompressor::Impl { std::unique_ptr dxt1_fragment_program; std::unique_ptr etc1_fragment_program; std::unique_ptr etc1_fast_fragment_program; + std::unique_ptr etc1_fast_split_fragment_program; std::unique_ptr packing_program; Impl(unsigned texture_width, unsigned texture_height, unsigned maximum_batch_size) @@ -780,6 +781,10 @@ struct gl_engine::TextureCompressor::Impl { "texture_compress.vert", ShaderCodeSource::FILE, std::vector { QStringLiteral("#define ALP_COMPRESS_ETC1"), QStringLiteral("#define ALP_COMPRESS_ETC1_FAST") }); + etc1_fast_split_fragment_program = std::make_unique("texture_compress_raster.vert", + "texture_compress.vert", + ShaderCodeSource::FILE, + std::vector { QStringLiteral("#define ALP_COMPRESS_ETC1"), QStringLiteral("#define ALP_COMPRESS_ETC1_SPLIT") }); packing_program = std::make_unique( "texture_compress_raster.vert", "texture_compress_pack.frag", ShaderCodeSource::FILE); @@ -790,6 +795,7 @@ struct gl_engine::TextureCompressor::Impl { dxt1_fragment_program.reset(); etc1_fragment_program.reset(); etc1_fast_fragment_program.reset(); + etc1_fast_split_fragment_program.reset(); packing_program.reset(); if (!QOpenGLContext::currentContext()) return; @@ -952,10 +958,15 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: GLboolean scissor_enabled = GL_FALSE; result.timings.compression_pass = measure_stage(GpuTimer::Stage::CompressionPass, [&]() { - auto* program = settings.algorithm == nucleus::utils::ColourTexture::Format::DXT1 - ? m->dxt1_fragment_program.get() - : settings.encoder == Encoder::FastRange ? m->etc1_fast_fragment_program.get() - : m->etc1_fragment_program.get(); + auto* program = m->dxt1_fragment_program.get(); + if (settings.algorithm == nucleus::utils::ColourTexture::Format::ETC1) { + if (settings.encoder == Encoder::FastRange) + program = m->etc1_fast_fragment_program.get(); + else if (settings.encoder == Encoder::FastSplit) + program = m->etc1_fast_split_fragment_program.get(); + else + program = m->etc1_fragment_program.get(); + } f->glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previous_draw_framebuffer); f->glGetIntegerv(GL_VIEWPORT, previous_viewport); f->glGetBooleanv(GL_COLOR_WRITEMASK, previous_colour_mask); diff --git a/gl_engine/Texture.h b/gl_engine/Texture.h index 01b64124..49fed090 100644 --- a/gl_engine/Texture.h +++ b/gl_engine/Texture.h @@ -93,7 +93,7 @@ class Texture { class TextureCompressor { public: enum class TimingMode { EndToEnd, IndividualStages, SubmissionOnly }; - enum class Encoder { Search, FastRange }; + enum class Encoder { Search, FastRange, FastSplit }; struct GpuTimings { double scratch_upload_ms = 0.0; diff --git a/gl_engine/shaders/texture_compress.vert b/gl_engine/shaders/texture_compress.vert index 7da577b5..00dab318 100644 --- a/gl_engine/shaders/texture_compress.vert +++ b/gl_engine/shaders/texture_compress.vert @@ -173,6 +173,109 @@ highp uvec2 encode_etc1_fast(highp uvec3 pixels[16]) return uvec2(header, byte_swap(indices)); } +struct FastEtc1Subblock { + highp ivec3 colour; + highp int table; + highp int middle; + highp int threshold; +}; + +struct FastEtc1Block { + highp uvec2 encoded; + highp uint error; +}; + +FastEtc1Subblock fast_subblock_parameters(highp uvec3 pixels[16], bool flip, highp int subblock) +{ + highp uvec3 minimum_colour = uvec3(255u); + highp uvec3 maximum_colour = uvec3(0u); + highp uvec3 sum = uvec3(0u); + for (int i = 0; i < 16; ++i) { + highp int x = i & 3; + highp int y = i >> 2; + bool belongs_to_first = flip ? y < 2 : x < 2; + if (belongs_to_first != (subblock == 0)) + continue; + minimum_colour = min(minimum_colour, pixels[i]); + maximum_colour = max(maximum_colour, pixels[i]); + sum += pixels[i]; + } + + highp int minimum_brightness = brightness(minimum_colour); + highp int maximum_brightness = brightness(maximum_colour); + highp int range = max(8, maximum_brightness - minimum_brightness); + highp int middle = (minimum_brightness + maximum_brightness + 1) / 2; + highp ivec3 average = ivec3((sum + 4u) / 8u); + highp int correction = middle - brightness(uvec3(average)); + highp ivec3 colour = clamp(average + ivec3(correction), ivec3(0), ivec3(255)); + return FastEtc1Subblock(colour, table_for_range(range), middle, (range * 3 + 4) / 8); +} + +FastEtc1Block encode_etc1_split_orientation(highp uvec3 pixels[16], bool flip) +{ + FastEtc1Subblock first = fast_subblock_parameters(pixels, flip, 0); + FastEtc1Subblock second = fast_subblock_parameters(pixels, flip, 1); + highp uvec3 first_base5 = (uvec3(first.colour) * 31u + 127u) / 255u; + highp uvec3 second_base5 = (uvec3(second.colour) * 31u + 127u) / 255u; + highp ivec3 base_delta = ivec3(second_base5) - ivec3(first_base5); + bool differential = all(greaterThanEqual(base_delta, ivec3(-4))) && all(lessThanEqual(base_delta, ivec3(3))); + + highp uint header; + highp ivec3 first_decoded; + highp ivec3 second_decoded; + if (differential) { + highp uvec3 delta3 = uvec3(base_delta) & 7u; + header = first_base5.r << 3u | delta3.r + | first_base5.g << 11u | delta3.g << 8u + | first_base5.b << 19u | delta3.b << 16u; + first_decoded = ivec3((first_base5 << 3u) | (first_base5 >> 2u)); + second_decoded = ivec3((second_base5 << 3u) | (second_base5 >> 2u)); + } else { + highp uvec3 first_base4 = (uvec3(first.colour) * 15u + 127u) / 255u; + highp uvec3 second_base4 = (uvec3(second.colour) * 15u + 127u) / 255u; + header = first_base4.r << 4u | second_base4.r + | first_base4.g << 12u | second_base4.g << 8u + | first_base4.b << 20u | second_base4.b << 16u; + first_decoded = ivec3((first_base4 << 4u) | first_base4); + second_decoded = ivec3((second_base4 << 4u) | second_base4); + } + + highp uint indices = 0u; + highp uint total_error = 0u; + for (int y = 0; y < 4; ++y) { + for (int x = 0; x < 4; ++x) { + highp int pixel_index = y * 4 + x; + bool use_second = flip ? y >= 2 : x >= 2; + highp int middle = use_second ? second.middle : first.middle; + highp int threshold = use_second ? second.threshold : first.threshold; + highp int table = use_second ? second.table : first.table; + highp ivec3 decoded = use_second ? second_decoded : first_decoded; + highp int brightness_delta = brightness(pixels[pixel_index]) - middle; + highp uint selected = uint(abs(brightness_delta) >= threshold); + if (brightness_delta < 0) + selected += 2u; + + highp ivec3 reconstructed = clamp(decoded + ivec3(modifier(table, int(selected))), ivec3(0), ivec3(255)); + highp ivec3 colour_delta = ivec3(pixels[pixel_index]) - reconstructed; + total_error += uint(colour_delta.x * colour_delta.x + colour_delta.y * colour_delta.y + colour_delta.z * colour_delta.z); + highp uint bit_position = uint(x * 4 + y); + indices |= (selected & 1u) << bit_position; + indices |= (selected >> 1u) << (bit_position + 16u); + } + } + + highp uint control = uint(first.table) << 5u | uint(second.table) << 2u + | (differential ? 2u : 0u) | (flip ? 1u : 0u); + return FastEtc1Block(uvec2(header | control << 24u, byte_swap(indices)), total_error); +} + +highp uvec2 encode_etc1_fast_split(highp uvec3 pixels[16]) +{ + FastEtc1Block vertical = encode_etc1_split_orientation(pixels, false); + FastEtc1Block horizontal = encode_etc1_split_orientation(pixels, true); + return horizontal.error < vertical.error ? horizontal.encoded : vertical.encoded; +} + highp uvec2 encode_etc1(highp uvec3 pixels[16]) { highp uvec3 sum = uvec3(0u); @@ -246,7 +349,9 @@ highp uvec2 compress_block(highp ivec2 block, } #ifdef ALP_COMPRESS_ETC1 -#ifdef ALP_COMPRESS_ETC1_FAST +#ifdef ALP_COMPRESS_ETC1_SPLIT + return encode_etc1_fast_split(pixels); +#elif defined(ALP_COMPRESS_ETC1_FAST) return encode_etc1_fast(pixels); #else return encode_etc1(pixels); From 8c9a6786fdbfbf6deefa935d264ac87b34c28e0d Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Thu, 20 Aug 2026 07:27:58 +0200 Subject: [PATCH 16/38] Optimize fast split ETC1 compression --- .../BenchmarkItem.cpp | 10 +- .../BenchmarkItem.h | 4 +- apps/texture_compression_benchmark/Main.qml | 4 +- gl_engine/Texture.cpp | 16 ++ gl_engine/Texture.h | 2 +- gl_engine/shaders/texture_compress.vert | 242 +++++++++++++++++- 6 files changed, 271 insertions(+), 7 deletions(-) diff --git a/apps/texture_compression_benchmark/BenchmarkItem.cpp b/apps/texture_compression_benchmark/BenchmarkItem.cpp index e1246fc0..3e520b0d 100644 --- a/apps/texture_compression_benchmark/BenchmarkItem.cpp +++ b/apps/texture_compression_benchmark/BenchmarkItem.cpp @@ -842,6 +842,10 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { return QStringLiteral("Fragment shader fast range + PBO"); case BenchmarkItem::GpuEncoder::FastSplit: return QStringLiteral("Fragment shader fast split + PBO"); + case BenchmarkItem::GpuEncoder::FastSplitFused: + return QStringLiteral("Fragment shader fast split fused + PBO"); + case BenchmarkItem::GpuEncoder::FastSplitBounds: + return QStringLiteral("Fragment shader fast split bounds + PBO"); case BenchmarkItem::GpuEncoder::Search: return QStringLiteral("Fragment shader search + PBO"); } @@ -854,7 +858,9 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { .encoder = m_gpu_encoder == BenchmarkItem::GpuEncoder::FastRange ? gl_engine::TextureCompressor::Encoder::FastRange : m_gpu_encoder == BenchmarkItem::GpuEncoder::FastSplit ? gl_engine::TextureCompressor::Encoder::FastSplit - : gl_engine::TextureCompressor::Encoder::Search, + : m_gpu_encoder == BenchmarkItem::GpuEncoder::FastSplitFused ? gl_engine::TextureCompressor::Encoder::FastSplitFused + : m_gpu_encoder == BenchmarkItem::GpuEncoder::FastSplitBounds ? gl_engine::TextureCompressor::Encoder::FastSplitBounds + : gl_engine::TextureCompressor::Encoder::Search, .generate_mipmaps = m_mipmaps, .timing_mode = gl_engine::TextureCompressor::TimingMode::EndToEnd, }; @@ -1212,7 +1218,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { BenchmarkItem::CpuEncoder m_cpu_encoder = BenchmarkItem::CpuEncoder::Goofy; int m_basis_quality = 75; int m_basis_effort = 4; - BenchmarkItem::GpuEncoder m_gpu_encoder = BenchmarkItem::GpuEncoder::FastSplit; + BenchmarkItem::GpuEncoder m_gpu_encoder = BenchmarkItem::GpuEncoder::FastSplitFused; int m_effort = 4; bool m_mipmaps = true; bool m_pending = false; diff --git a/apps/texture_compression_benchmark/BenchmarkItem.h b/apps/texture_compression_benchmark/BenchmarkItem.h index fe9dba6d..6b9930c7 100644 --- a/apps/texture_compression_benchmark/BenchmarkItem.h +++ b/apps/texture_compression_benchmark/BenchmarkItem.h @@ -33,7 +33,7 @@ class BenchmarkItem : public QQuickFramebufferObject { public: enum class CpuEncoder { Goofy, BasisEtc1s, BasisUastcLdr4x4, BasisXuastcLdr4x4 }; Q_ENUM(CpuEncoder) - enum class GpuEncoder { Search, FastRange, FastSplit }; + enum class GpuEncoder { Search, FastRange, FastSplit, FastSplitFused, FastSplitBounds }; Q_ENUM(GpuEncoder) explicit BenchmarkItem(QQuickItem* parent = nullptr); @@ -84,7 +84,7 @@ class BenchmarkItem : public QQuickFramebufferObject { CpuEncoder m_cpu_encoder = CpuEncoder::Goofy; int m_basis_quality = 75; int m_basis_effort = 4; - GpuEncoder m_gpu_encoder = GpuEncoder::FastSplit; + GpuEncoder m_gpu_encoder = GpuEncoder::FastSplitFused; int m_effort = 4; bool m_mipmaps = true; bool m_data_ready = false; diff --git a/apps/texture_compression_benchmark/Main.qml b/apps/texture_compression_benchmark/Main.qml index 3bcb60fe..29a716ba 100644 --- a/apps/texture_compression_benchmark/Main.qml +++ b/apps/texture_compression_benchmark/Main.qml @@ -145,7 +145,9 @@ ApplicationWindow { model: [ qsTr("Search (reference)"), qsTr("Fast range (Goofy-inspired)"), - qsTr("Fast split (two sub-blocks)") + qsTr("Fast split (two sub-blocks)"), + qsTr("Fast split fused"), + qsTr("Fast split bounds") ] currentIndex: benchmark.gpuEncoder enabled: !benchmark.running diff --git a/gl_engine/Texture.cpp b/gl_engine/Texture.cpp index 6d86e1b7..43a8a0df 100644 --- a/gl_engine/Texture.cpp +++ b/gl_engine/Texture.cpp @@ -695,6 +695,8 @@ struct gl_engine::TextureCompressor::Impl { std::unique_ptr etc1_fragment_program; std::unique_ptr etc1_fast_fragment_program; std::unique_ptr etc1_fast_split_fragment_program; + std::unique_ptr etc1_fast_split_fused_fragment_program; + std::unique_ptr etc1_fast_split_bounds_fragment_program; std::unique_ptr packing_program; Impl(unsigned texture_width, unsigned texture_height, unsigned maximum_batch_size) @@ -785,6 +787,14 @@ struct gl_engine::TextureCompressor::Impl { "texture_compress.vert", ShaderCodeSource::FILE, std::vector { QStringLiteral("#define ALP_COMPRESS_ETC1"), QStringLiteral("#define ALP_COMPRESS_ETC1_SPLIT") }); + etc1_fast_split_fused_fragment_program = std::make_unique("texture_compress_raster.vert", + "texture_compress.vert", + ShaderCodeSource::FILE, + std::vector { QStringLiteral("#define ALP_COMPRESS_ETC1"), QStringLiteral("#define ALP_COMPRESS_ETC1_SPLIT_FUSED") }); + etc1_fast_split_bounds_fragment_program = std::make_unique("texture_compress_raster.vert", + "texture_compress.vert", + ShaderCodeSource::FILE, + std::vector { QStringLiteral("#define ALP_COMPRESS_ETC1"), QStringLiteral("#define ALP_COMPRESS_ETC1_SPLIT_BOUNDS") }); packing_program = std::make_unique( "texture_compress_raster.vert", "texture_compress_pack.frag", ShaderCodeSource::FILE); @@ -796,6 +806,8 @@ struct gl_engine::TextureCompressor::Impl { etc1_fragment_program.reset(); etc1_fast_fragment_program.reset(); etc1_fast_split_fragment_program.reset(); + etc1_fast_split_fused_fragment_program.reset(); + etc1_fast_split_bounds_fragment_program.reset(); packing_program.reset(); if (!QOpenGLContext::currentContext()) return; @@ -964,6 +976,10 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: program = m->etc1_fast_fragment_program.get(); else if (settings.encoder == Encoder::FastSplit) program = m->etc1_fast_split_fragment_program.get(); + else if (settings.encoder == Encoder::FastSplitFused) + program = m->etc1_fast_split_fused_fragment_program.get(); + else if (settings.encoder == Encoder::FastSplitBounds) + program = m->etc1_fast_split_bounds_fragment_program.get(); else program = m->etc1_fragment_program.get(); } diff --git a/gl_engine/Texture.h b/gl_engine/Texture.h index 49fed090..c1626681 100644 --- a/gl_engine/Texture.h +++ b/gl_engine/Texture.h @@ -93,7 +93,7 @@ class Texture { class TextureCompressor { public: enum class TimingMode { EndToEnd, IndividualStages, SubmissionOnly }; - enum class Encoder { Search, FastRange, FastSplit }; + enum class Encoder { Search, FastRange, FastSplit, FastSplitFused, FastSplitBounds }; struct GpuTimings { double scratch_upload_ms = 0.0; diff --git a/gl_engine/shaders/texture_compress.vert b/gl_engine/shaders/texture_compress.vert index 00dab318..92acf307 100644 --- a/gl_engine/shaders/texture_compress.vert +++ b/gl_engine/shaders/texture_compress.vert @@ -276,6 +276,242 @@ highp uvec2 encode_etc1_fast_split(highp uvec3 pixels[16]) return horizontal.error < vertical.error ? horizontal.encoded : vertical.encoded; } +FastEtc1Subblock fast_subblock_from_statistics(highp uvec3 minimum_colour, + highp uvec3 maximum_colour, + highp uvec3 sum) +{ + highp int minimum_brightness = brightness(minimum_colour); + highp int maximum_brightness = brightness(maximum_colour); + highp int range = max(8, maximum_brightness - minimum_brightness); + highp int middle = (minimum_brightness + maximum_brightness + 1) / 2; + highp ivec3 average = ivec3((sum + 4u) / 8u); + highp int correction = middle - brightness(uvec3(average)); + highp ivec3 colour = clamp(average + ivec3(correction), ivec3(0), ivec3(255)); + return FastEtc1Subblock(colour, table_for_range(range), middle, (range * 3 + 4) / 8); +} + +struct FastEtc1Bases { + highp uint header; + highp ivec3 first_decoded; + highp ivec3 second_decoded; + highp uint differential_bit; +}; + +FastEtc1Bases fast_split_bases(FastEtc1Subblock first, FastEtc1Subblock second) +{ + highp uvec3 first_base5 = (uvec3(first.colour) * 31u + 127u) / 255u; + highp uvec3 second_base5 = (uvec3(second.colour) * 31u + 127u) / 255u; + highp ivec3 base_delta = ivec3(second_base5) - ivec3(first_base5); + bool differential = all(greaterThanEqual(base_delta, ivec3(-4))) && all(lessThanEqual(base_delta, ivec3(3))); + if (differential) { + highp uvec3 delta3 = uvec3(base_delta) & 7u; + highp uint header = first_base5.r << 3u | delta3.r + | first_base5.g << 11u | delta3.g << 8u + | first_base5.b << 19u | delta3.b << 16u; + return FastEtc1Bases(header, + ivec3((first_base5 << 3u) | (first_base5 >> 2u)), + ivec3((second_base5 << 3u) | (second_base5 >> 2u)), + 2u); + } + + highp uvec3 first_base4 = (uvec3(first.colour) * 15u + 127u) / 255u; + highp uvec3 second_base4 = (uvec3(second.colour) * 15u + 127u) / 255u; + highp uint header = first_base4.r << 4u | second_base4.r + | first_base4.g << 12u | second_base4.g << 8u + | first_base4.b << 20u | second_base4.b << 16u; + return FastEtc1Bases(header, + ivec3((first_base4 << 4u) | first_base4), + ivec3((second_base4 << 4u) | second_base4), + 0u); +} + +highp uvec2 encode_etc1_fast_split_fused(highp uvec3 pixels[16]) +{ + highp uvec3 left_minimum = uvec3(255u); + highp uvec3 left_maximum = uvec3(0u); + highp uvec3 left_sum = uvec3(0u); + highp uvec3 right_minimum = uvec3(255u); + highp uvec3 right_maximum = uvec3(0u); + highp uvec3 right_sum = uvec3(0u); + highp uvec3 top_minimum = uvec3(255u); + highp uvec3 top_maximum = uvec3(0u); + highp uvec3 top_sum = uvec3(0u); + highp uvec3 bottom_minimum = uvec3(255u); + highp uvec3 bottom_maximum = uvec3(0u); + highp uvec3 bottom_sum = uvec3(0u); + for (int y = 0; y < 4; ++y) { + for (int x = 0; x < 4; ++x) { + highp uvec3 pixel = pixels[y * 4 + x]; + if (x < 2) { + left_minimum = min(left_minimum, pixel); + left_maximum = max(left_maximum, pixel); + left_sum += pixel; + } else { + right_minimum = min(right_minimum, pixel); + right_maximum = max(right_maximum, pixel); + right_sum += pixel; + } + if (y < 2) { + top_minimum = min(top_minimum, pixel); + top_maximum = max(top_maximum, pixel); + top_sum += pixel; + } else { + bottom_minimum = min(bottom_minimum, pixel); + bottom_maximum = max(bottom_maximum, pixel); + bottom_sum += pixel; + } + } + } + + FastEtc1Subblock left = fast_subblock_from_statistics(left_minimum, left_maximum, left_sum); + FastEtc1Subblock right = fast_subblock_from_statistics(right_minimum, right_maximum, right_sum); + FastEtc1Subblock top = fast_subblock_from_statistics(top_minimum, top_maximum, top_sum); + FastEtc1Subblock bottom = fast_subblock_from_statistics(bottom_minimum, bottom_maximum, bottom_sum); + FastEtc1Bases vertical_bases = fast_split_bases(left, right); + FastEtc1Bases horizontal_bases = fast_split_bases(top, bottom); + + highp uint vertical_indices = 0u; + highp uint horizontal_indices = 0u; + highp uint vertical_error = 0u; + highp uint horizontal_error = 0u; + for (int y = 0; y < 4; ++y) { + for (int x = 0; x < 4; ++x) { + highp int pixel_index = y * 4 + x; + highp uvec3 pixel = pixels[pixel_index]; + highp int pixel_brightness = brightness(pixel); + bool use_right = x >= 2; + bool use_bottom = y >= 2; + + highp int vertical_middle = use_right ? right.middle : left.middle; + highp int vertical_threshold = use_right ? right.threshold : left.threshold; + highp int vertical_table = use_right ? right.table : left.table; + highp ivec3 vertical_base = use_right ? vertical_bases.second_decoded : vertical_bases.first_decoded; + highp int vertical_brightness_delta = pixel_brightness - vertical_middle; + highp uint vertical_selected = uint(abs(vertical_brightness_delta) >= vertical_threshold); + if (vertical_brightness_delta < 0) + vertical_selected += 2u; + highp ivec3 vertical_reconstructed + = clamp(vertical_base + ivec3(modifier(vertical_table, int(vertical_selected))), ivec3(0), ivec3(255)); + highp ivec3 vertical_delta = ivec3(pixel) - vertical_reconstructed; + vertical_error += uint(vertical_delta.x * vertical_delta.x + vertical_delta.y * vertical_delta.y + vertical_delta.z * vertical_delta.z); + + highp int horizontal_middle = use_bottom ? bottom.middle : top.middle; + highp int horizontal_threshold = use_bottom ? bottom.threshold : top.threshold; + highp int horizontal_table = use_bottom ? bottom.table : top.table; + highp ivec3 horizontal_base = use_bottom ? horizontal_bases.second_decoded : horizontal_bases.first_decoded; + highp int horizontal_brightness_delta = pixel_brightness - horizontal_middle; + highp uint horizontal_selected = uint(abs(horizontal_brightness_delta) >= horizontal_threshold); + if (horizontal_brightness_delta < 0) + horizontal_selected += 2u; + highp ivec3 horizontal_reconstructed + = clamp(horizontal_base + ivec3(modifier(horizontal_table, int(horizontal_selected))), ivec3(0), ivec3(255)); + highp ivec3 horizontal_delta = ivec3(pixel) - horizontal_reconstructed; + horizontal_error += uint(horizontal_delta.x * horizontal_delta.x + horizontal_delta.y * horizontal_delta.y + horizontal_delta.z * horizontal_delta.z); + + highp uint bit_position = uint(x * 4 + y); + vertical_indices |= (vertical_selected & 1u) << bit_position; + vertical_indices |= (vertical_selected >> 1u) << (bit_position + 16u); + horizontal_indices |= (horizontal_selected & 1u) << bit_position; + horizontal_indices |= (horizontal_selected >> 1u) << (bit_position + 16u); + } + } + + highp uint vertical_control + = uint(left.table) << 5u | uint(right.table) << 2u | vertical_bases.differential_bit; + highp uint horizontal_control + = uint(top.table) << 5u | uint(bottom.table) << 2u | horizontal_bases.differential_bit | 1u; + highp uvec2 vertical = uvec2(vertical_bases.header | vertical_control << 24u, byte_swap(vertical_indices)); + highp uvec2 horizontal = uvec2(horizontal_bases.header | horizontal_control << 24u, byte_swap(horizontal_indices)); + return horizontal_error < vertical_error ? horizontal : vertical; +} + +highp uvec2 encode_etc1_selected_orientation(highp uvec3 pixels[16], + FastEtc1Subblock first, + FastEtc1Subblock second, + bool flip) +{ + FastEtc1Bases bases = fast_split_bases(first, second); + highp uint indices = 0u; + for (int y = 0; y < 4; ++y) { + for (int x = 0; x < 4; ++x) { + highp int pixel_index = y * 4 + x; + bool use_second = flip ? y >= 2 : x >= 2; + highp int middle = use_second ? second.middle : first.middle; + highp int threshold = use_second ? second.threshold : first.threshold; + highp int brightness_delta = brightness(pixels[pixel_index]) - middle; + highp uint selected = uint(abs(brightness_delta) >= threshold); + if (brightness_delta < 0) + selected += 2u; + highp uint bit_position = uint(x * 4 + y); + indices |= (selected & 1u) << bit_position; + indices |= (selected >> 1u) << (bit_position + 16u); + } + } + + highp uint control = uint(first.table) << 5u | uint(second.table) << 2u + | bases.differential_bit | (flip ? 1u : 0u); + return uvec2(bases.header | control << 24u, byte_swap(indices)); +} + +highp uint fast_bounds_score(highp uvec3 minimum_colour, highp uvec3 maximum_colour) +{ + highp uvec3 range = maximum_colour - minimum_colour; + return range.r * range.r + range.g * range.g + range.b * range.b; +} + +highp uvec2 encode_etc1_fast_split_bounds(highp uvec3 pixels[16]) +{ + highp uvec3 left_minimum = uvec3(255u); + highp uvec3 left_maximum = uvec3(0u); + highp uvec3 left_sum = uvec3(0u); + highp uvec3 right_minimum = uvec3(255u); + highp uvec3 right_maximum = uvec3(0u); + highp uvec3 right_sum = uvec3(0u); + highp uvec3 top_minimum = uvec3(255u); + highp uvec3 top_maximum = uvec3(0u); + highp uvec3 top_sum = uvec3(0u); + highp uvec3 bottom_minimum = uvec3(255u); + highp uvec3 bottom_maximum = uvec3(0u); + highp uvec3 bottom_sum = uvec3(0u); + for (int y = 0; y < 4; ++y) { + for (int x = 0; x < 4; ++x) { + highp uvec3 pixel = pixels[y * 4 + x]; + if (x < 2) { + left_minimum = min(left_minimum, pixel); + left_maximum = max(left_maximum, pixel); + left_sum += pixel; + } else { + right_minimum = min(right_minimum, pixel); + right_maximum = max(right_maximum, pixel); + right_sum += pixel; + } + if (y < 2) { + top_minimum = min(top_minimum, pixel); + top_maximum = max(top_maximum, pixel); + top_sum += pixel; + } else { + bottom_minimum = min(bottom_minimum, pixel); + bottom_maximum = max(bottom_maximum, pixel); + bottom_sum += pixel; + } + } + } + + highp uint vertical_score = fast_bounds_score(left_minimum, left_maximum) + + fast_bounds_score(right_minimum, right_maximum); + highp uint horizontal_score = fast_bounds_score(top_minimum, top_maximum) + + fast_bounds_score(bottom_minimum, bottom_maximum); + if (horizontal_score < vertical_score) { + FastEtc1Subblock top = fast_subblock_from_statistics(top_minimum, top_maximum, top_sum); + FastEtc1Subblock bottom = fast_subblock_from_statistics(bottom_minimum, bottom_maximum, bottom_sum); + return encode_etc1_selected_orientation(pixels, top, bottom, true); + } + + FastEtc1Subblock left = fast_subblock_from_statistics(left_minimum, left_maximum, left_sum); + FastEtc1Subblock right = fast_subblock_from_statistics(right_minimum, right_maximum, right_sum); + return encode_etc1_selected_orientation(pixels, left, right, false); +} + highp uvec2 encode_etc1(highp uvec3 pixels[16]) { highp uvec3 sum = uvec3(0u); @@ -349,7 +585,11 @@ highp uvec2 compress_block(highp ivec2 block, } #ifdef ALP_COMPRESS_ETC1 -#ifdef ALP_COMPRESS_ETC1_SPLIT +#ifdef ALP_COMPRESS_ETC1_SPLIT_BOUNDS + return encode_etc1_fast_split_bounds(pixels); +#elif defined(ALP_COMPRESS_ETC1_SPLIT_FUSED) + return encode_etc1_fast_split_fused(pixels); +#elif defined(ALP_COMPRESS_ETC1_SPLIT) return encode_etc1_fast_split(pixels); #elif defined(ALP_COMPRESS_ETC1_FAST) return encode_etc1_fast(pixels); From 76f5f82762754ed20dc8efc1ac1c71ea2c2e3cd7 Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Thu, 20 Aug 2026 08:25:29 +0200 Subject: [PATCH 17/38] Report GPU mipmap generation time --- apps/texture_compression_benchmark/BenchmarkItem.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/texture_compression_benchmark/BenchmarkItem.cpp b/apps/texture_compression_benchmark/BenchmarkItem.cpp index 3e520b0d..a2660d11 100644 --- a/apps/texture_compression_benchmark/BenchmarkItem.cpp +++ b/apps/texture_compression_benchmark/BenchmarkItem.cpp @@ -782,6 +782,8 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { }); m_pending_gpu_report->summary.push_back(QString()); m_pending_gpu_report->summary.push_back(QStringLiteral("Actual GPU time (timer query)")); + m_pending_gpu_report->summary.push_back( + QStringLiteral("Mipmap generation median %1 ms").arg(statistics(mipmap_generation).median, 8, 'f', 3)); m_pending_gpu_report->summary.push_back( QStringLiteral("Compression pass median %1 ms").arg(statistics(compression_pass).median, 8, 'f', 3)); m_pending_gpu_report->summary.push_back( From c83e9e4183728f560a40ab62a7f5f216acff3671 Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Thu, 20 Aug 2026 10:13:14 +0200 Subject: [PATCH 18/38] Add GPU encoder preview comparison --- .../BenchmarkItem.cpp | 346 ++++++++++++------ .../BenchmarkItem.h | 31 +- apps/texture_compression_benchmark/Main.qml | 123 ++++--- 3 files changed, 337 insertions(+), 163 deletions(-) diff --git a/apps/texture_compression_benchmark/BenchmarkItem.cpp b/apps/texture_compression_benchmark/BenchmarkItem.cpp index a2660d11..51e55a61 100644 --- a/apps/texture_compression_benchmark/BenchmarkItem.cpp +++ b/apps/texture_compression_benchmark/BenchmarkItem.cpp @@ -6,7 +6,6 @@ #include "BenchmarkItem.h" -#include #include #include #include @@ -294,44 +293,6 @@ QImage reconstruct(gl_engine::Texture& texture, unsigned resolution, unsigned la return result; } -QString preview_data_url(std::span images) -{ - constexpr int columns = 4; - constexpr int tile_size = 512; - static const auto linear_to_srgb = [] { - std::array result {}; - for (size_t i = 0; i < result.size(); ++i) { - const auto linear = double(i) / 255.0; - const auto srgb = linear <= 0.0031308 ? 12.92 * linear : 1.055 * std::pow(linear, 1.0 / 2.4) - 0.055; - result[i] = uint8_t(std::lround(std::clamp(srgb, 0.0, 1.0) * 255.0)); - } - return result; - }(); - QImage preview(columns * tile_size, columns * tile_size, QImage::Format_RGBA8888); - preview.fill(Qt::black); - QPainter painter(&preview); - for (size_t i = 0; i < images.size(); ++i) { - auto srgb_image = images[i].convertToFormat(QImage::Format_RGBA8888); - for (int y = 0; y < srgb_image.height(); ++y) { - auto* scanline = srgb_image.scanLine(y); - for (int x = 0; x < srgb_image.width(); ++x) { - auto* pixel = scanline + x * 4; - pixel[0] = linear_to_srgb[pixel[0]]; - pixel[1] = linear_to_srgb[pixel[1]]; - pixel[2] = linear_to_srgb[pixel[2]]; - } - } - painter.drawImage(QPoint(int(i % columns) * tile_size, int(i / columns) * tile_size), srgb_image); - } - painter.end(); - - QByteArray png; - QBuffer buffer(&png); - buffer.open(QIODevice::WriteOnly); - preview.save(&buffer, "PNG"); - return QStringLiteral("data:image/png;base64,") + QString::fromLatin1(png.toBase64()); -} - struct CpuCompressionResult { std::vector textures; double source_preparation_ms = 0.0; @@ -356,6 +317,65 @@ QString cpu_encoder_name(BenchmarkItem::CpuEncoder encoder) return QStringLiteral("Unknown"); } +constexpr std::array gpu_encoders { + BenchmarkItem::GpuEncoder::Search, + BenchmarkItem::GpuEncoder::FastRange, + BenchmarkItem::GpuEncoder::FastSplit, + BenchmarkItem::GpuEncoder::FastSplitFused, + BenchmarkItem::GpuEncoder::FastSplitBounds, +}; + +QString gpu_encoder_name(BenchmarkItem::GpuEncoder encoder, int effort) +{ + switch (encoder) { + case BenchmarkItem::GpuEncoder::Search: + return QStringLiteral("GPU Search (reference), effort %1").arg(effort); + case BenchmarkItem::GpuEncoder::FastRange: + return QStringLiteral("GPU Fast range"); + case BenchmarkItem::GpuEncoder::FastSplit: + return QStringLiteral("GPU Fast split"); + case BenchmarkItem::GpuEncoder::FastSplitFused: + return QStringLiteral("GPU Fast split fused"); + case BenchmarkItem::GpuEncoder::FastSplitBounds: + return QStringLiteral("GPU Fast split bounds"); + } + return QStringLiteral("GPU Search (reference)"); +} + +QString gpu_backend_name(BenchmarkItem::GpuEncoder encoder) +{ + switch (encoder) { + case BenchmarkItem::GpuEncoder::Search: + return QStringLiteral("Fragment shader search + PBO"); + case BenchmarkItem::GpuEncoder::FastRange: + return QStringLiteral("Fragment shader fast range + PBO"); + case BenchmarkItem::GpuEncoder::FastSplit: + return QStringLiteral("Fragment shader fast split + PBO"); + case BenchmarkItem::GpuEncoder::FastSplitFused: + return QStringLiteral("Fragment shader fast split fused + PBO"); + case BenchmarkItem::GpuEncoder::FastSplitBounds: + return QStringLiteral("Fragment shader fast split bounds + PBO"); + } + return QStringLiteral("Fragment shader search + PBO"); +} + +gl_engine::TextureCompressor::Encoder compressor_encoder(BenchmarkItem::GpuEncoder encoder) +{ + switch (encoder) { + case BenchmarkItem::GpuEncoder::Search: + return gl_engine::TextureCompressor::Encoder::Search; + case BenchmarkItem::GpuEncoder::FastRange: + return gl_engine::TextureCompressor::Encoder::FastRange; + case BenchmarkItem::GpuEncoder::FastSplit: + return gl_engine::TextureCompressor::Encoder::FastSplit; + case BenchmarkItem::GpuEncoder::FastSplitFused: + return gl_engine::TextureCompressor::Encoder::FastSplitFused; + case BenchmarkItem::GpuEncoder::FastSplitBounds: + return gl_engine::TextureCompressor::Encoder::FastSplitBounds; + } + return gl_engine::TextureCompressor::Encoder::Search; +} + nucleus::utils::BasisUniversalFormat basis_format(BenchmarkItem::CpuEncoder encoder) { switch (encoder) { @@ -476,6 +496,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { auto* benchmark_item = static_cast(item); m_item = benchmark_item; m_window = benchmark_item->window(); + m_preview_encoder = benchmark_item->m_preview_encoder; if (benchmark_item->m_request_serial == m_seen_serial) return; m_seen_serial = benchmark_item->m_request_serial; @@ -486,14 +507,13 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { m_effort = benchmark_item->m_effort; m_mipmaps = benchmark_item->m_mipmaps; m_source_images = benchmark_item->m_source_images; - m_preview_source.clear(); + m_preview_results.clear(); + m_preview_textures = {}; m_pending = true; } void render() override { - if (!m_pending && !m_pending_gpu_report) - return; m_window->beginExternalCommands(); std::optional> completed; if (m_pending) { @@ -502,30 +522,76 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { if (!m_pending_gpu_report) completed = immediate; } else { - completed = poll_gpu_report(); + if (m_pending_gpu_report) + completed = poll_gpu_report(); } + draw_preview(); m_window->endExternalCommands(); if (completed) { QPointer item = m_item; const auto [text, json] = std::move(*completed); - const auto preview_source = m_preview_source; - QMetaObject::invokeMethod(m_item, [item, text, json, preview_source]() { + const auto preview_results = m_preview_results; + QMetaObject::invokeMethod(m_item, [item, text, json, preview_results]() { if (item) - item->publishResults(text, json, preview_source); + item->publishResults(text, json, preview_results); }); - } else { + } else if (m_pending || m_pending_gpu_report) { request_another_frame(); } } - QOpenGLFramebufferObject* createFramebufferObject(const QSize&) override + QOpenGLFramebufferObject* createFramebufferObject(const QSize& size) override { QOpenGLFramebufferObjectFormat format; format.setAttachment(QOpenGLFramebufferObject::NoAttachment); - return new QOpenGLFramebufferObject(QSize(1, 1), format); + return new QOpenGLFramebufferObject(size.expandedTo(QSize(1, 1)), format); } private: + void draw_preview() + { + if (m_preview_encoder < 0 || size_t(m_preview_encoder) >= m_preview_textures.size() + || !m_preview_textures[size_t(m_preview_encoder)]) + return; + + if (!m_preview_shader) { + m_preview_shader = std::make_unique(R"( + out highp vec2 texcoords; + void main() { + highp vec2 vertices[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); + gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0); + texcoords = 0.5 * gl_Position.xy + vec2(0.5); + })", + R"( + uniform lowp sampler2DArray texture_sampler; + in highp vec2 texcoords; + out lowp vec4 out_color; + void main() { + highp vec2 grid_position = texcoords * 4.0; + highp ivec2 cell = min(ivec2(grid_position), ivec2(3)); + highp float layer = float((3 - cell.y) * 4 + cell.x); + highp vec2 tile_coordinates = fract(grid_position); + out_color = textureLod(texture_sampler, + vec3(tile_coordinates.x, 1.0 - tile_coordinates.y, layer), 0.0); + })", + gl_engine::ShaderCodeSource::PLAINTEXT); + m_preview_geometry = gl_engine::helpers::create_screen_quad_geometry(); + } + + auto* f = QOpenGLContext::currentContext()->extraFunctions(); + framebufferObject()->bind(); + f->glViewport(0, 0, framebufferObject()->width(), framebufferObject()->height()); + f->glDisable(GL_BLEND); + f->glDisable(GL_CULL_FACE); + f->glDisable(GL_DEPTH_TEST); + f->glDisable(GL_SCISSOR_TEST); + m_preview_shader->bind(); + m_preview_textures[size_t(m_preview_encoder)]->bind(0); + m_preview_shader->set_uniform("texture_sampler", 0); + m_preview_geometry.draw(); + m_preview_shader->release(); + } + void request_another_frame() { update(); @@ -838,31 +904,12 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { } const auto algorithm = gl_engine::Texture::compression_algorithm(); - const auto backend_name = [this]() { - switch (m_gpu_encoder) { - case BenchmarkItem::GpuEncoder::FastRange: - return QStringLiteral("Fragment shader fast range + PBO"); - case BenchmarkItem::GpuEncoder::FastSplit: - return QStringLiteral("Fragment shader fast split + PBO"); - case BenchmarkItem::GpuEncoder::FastSplitFused: - return QStringLiteral("Fragment shader fast split fused + PBO"); - case BenchmarkItem::GpuEncoder::FastSplitBounds: - return QStringLiteral("Fragment shader fast split bounds + PBO"); - case BenchmarkItem::GpuEncoder::Search: - return QStringLiteral("Fragment shader search + PBO"); - } - return QStringLiteral("Fragment shader search + PBO"); - }(); + const auto backend_name = gpu_backend_name(m_gpu_encoder); const auto filter = m_mipmaps ? gl_engine::Texture::Filter::MipMapLinear : gl_engine::Texture::Filter::Linear; const gl_engine::TextureCompressor::Settings gpu_settings { .algorithm = algorithm, .effort = unsigned(m_effort), - .encoder = m_gpu_encoder == BenchmarkItem::GpuEncoder::FastRange - ? gl_engine::TextureCompressor::Encoder::FastRange - : m_gpu_encoder == BenchmarkItem::GpuEncoder::FastSplit ? gl_engine::TextureCompressor::Encoder::FastSplit - : m_gpu_encoder == BenchmarkItem::GpuEncoder::FastSplitFused ? gl_engine::TextureCompressor::Encoder::FastSplitFused - : m_gpu_encoder == BenchmarkItem::GpuEncoder::FastSplitBounds ? gl_engine::TextureCompressor::Encoder::FastSplitBounds - : gl_engine::TextureCompressor::Encoder::Search, + .encoder = compressor_encoder(m_gpu_encoder), .generate_mipmaps = m_mipmaps, .timing_mode = gl_engine::TextureCompressor::TimingMode::EndToEnd, }; @@ -885,45 +932,57 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { return elapsed_ms(start); }; - // Quality is evaluated first with one untimed compression of all 16 images. + // Quality is evaluated first over all 16 images. These destinations remain resident and + // are sampled directly by the preview renderer. double cpu_psnr = 0.0; double gpu_psnr = 0.0; { - gl_engine::Texture cpu_quality_destination(gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); - cpu_quality_destination.setParams(filter, gl_engine::Texture::Filter::Linear); - cpu_quality_destination.allocate_array(resolution, resolution, unsigned(tile_groups.size())); + m_preview_results.clear(); + m_preview_results.reserve(1 + gpu_encoders.size()); + m_preview_textures[0] = std::make_unique( + gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); + m_preview_textures[0]->setParams(filter, gl_engine::Texture::Filter::Linear); + m_preview_textures[0]->allocate_array(resolution, resolution, unsigned(tile_groups.size())); auto cpu_quality = compress_cpu(all_sources); if (!cpu_quality) return compression_error(cpu_quality.error()); - static_cast(upload_cpu(cpu_quality_destination, cpu_quality->textures)); - - gl_engine::Texture gpu_quality_destination(gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); - gpu_quality_destination.setParams(filter, gl_engine::Texture::Filter::Linear); - gpu_quality_destination.allocate_array(resolution, resolution, unsigned(tile_groups.size())); - gl_engine::TextureCompressor gpu_quality_compressor(resolution, resolution, unsigned(tile_groups.size())); - static_cast(gpu_quality_compressor.compress(all_sources, gpu_quality_destination, quality_layers, gpu_settings)); + static_cast(upload_cpu(*m_preview_textures[0], cpu_quality->textures)); std::vector cpu_reconstructed; - std::vector gpu_reconstructed; cpu_reconstructed.reserve(all_sources.size()); - gpu_reconstructed.reserve(all_sources.size()); - for (unsigned layer = 0; layer < all_sources.size(); ++layer) { - cpu_reconstructed.push_back(reconstruct(cpu_quality_destination, resolution, layer)); - gpu_reconstructed.push_back(reconstruct(gpu_quality_destination, resolution, layer)); - } + for (unsigned layer = 0; layer < all_sources.size(); ++layer) + cpu_reconstructed.push_back(reconstruct(*m_preview_textures[0], resolution, layer)); cpu_psnr = linear_psnr(cpu_reconstructed, all_sources); - gpu_psnr = linear_psnr(gpu_reconstructed, all_sources); - m_preview_source = preview_data_url(cpu_reconstructed); + m_preview_results.push_back({ QStringLiteral("CPU %1").arg(cpu_encoder_name(m_cpu_encoder)), cpu_psnr, 0.0 }); + + for (size_t encoder_index = 0; encoder_index < gpu_encoders.size(); ++encoder_index) { + const auto encoder = gpu_encoders[encoder_index]; + auto& preview_texture = m_preview_textures[encoder_index + 1]; + preview_texture = std::make_unique( + gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); + preview_texture->setParams(filter, gl_engine::Texture::Filter::Linear); + preview_texture->allocate_array(resolution, resolution, unsigned(tile_groups.size())); + gl_engine::TextureCompressor preview_compressor(resolution, resolution, unsigned(tile_groups.size())); + auto preview_settings = gpu_settings; + preview_settings.encoder = compressor_encoder(encoder); + static_cast(preview_compressor.compress(all_sources, *preview_texture, quality_layers, preview_settings)); + + std::vector reconstructed; + reconstructed.reserve(all_sources.size()); + for (unsigned layer = 0; layer < all_sources.size(); ++layer) + reconstructed.push_back(reconstruct(*preview_texture, resolution, layer)); + const auto psnr = linear_psnr(reconstructed, all_sources); + m_preview_results.push_back({ gpu_encoder_name(encoder, m_effort), psnr, 0.0 }); + if (encoder == m_gpu_encoder) + gpu_psnr = psnr; + } } gl_engine::Texture cpu_destination(gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); cpu_destination.setParams(filter, gl_engine::Texture::Filter::Linear); cpu_destination.allocate_array(resolution, resolution, batch_size); - auto gpu_destination = std::make_unique( - gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); - gpu_destination->setParams(filter, gl_engine::Texture::Filter::Linear); - gpu_destination->allocate_array(resolution, resolution, batch_size); - auto gpu_compressor = std::make_unique(resolution, resolution, batch_size); + std::unique_ptr gpu_destination; + std::unique_ptr gpu_compressor; m_gpu_timer = std::make_unique(); std::vector cpu_compression_times; @@ -962,9 +1021,6 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { return compression_error(compressed.error()); static_cast(upload_cpu(cpu_destination, compressed->textures)); } - for (int group = 0; group < batches_per_round; ++group) - static_cast(gpu_compressor->compress(sources_for_group(group), *gpu_destination, layers, gpu_settings)); - for (int round = 0; round < measurement_rounds; ++round) { for (int group = 0; group < batches_per_round; ++group) { const auto cpu_start = Clock::now(); @@ -984,12 +1040,35 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { } } - for (int round = 0; round < measurement_rounds; ++round) { - for (int group = 0; group < batches_per_round; ++group) { - const auto gpu = gpu_compressor->compress(sources_for_group(group), *gpu_destination, layers, gpu_settings); - gpu_total_times.push_back(gpu.timings.total_ms); + for (size_t encoder_index = 0; encoder_index < gpu_encoders.size(); ++encoder_index) { + const auto encoder = gpu_encoders[encoder_index]; + auto destination = std::make_unique( + gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); + destination->setParams(filter, gl_engine::Texture::Filter::Linear); + destination->allocate_array(resolution, resolution, batch_size); + auto compressor = std::make_unique(resolution, resolution, batch_size); + auto settings = gpu_settings; + settings.encoder = compressor_encoder(encoder); + + for (int group = 0; group < batches_per_round; ++group) + static_cast(compressor->compress(sources_for_group(group), *destination, layers, settings)); + + std::vector encoder_times; + encoder_times.reserve(sample_count); + for (int round = 0; round < measurement_rounds; ++round) { + for (int group = 0; group < batches_per_round; ++group) { + const auto gpu = compressor->compress(sources_for_group(group), *destination, layers, settings); + encoder_times.push_back(gpu.timings.total_ms); + } + } + m_preview_results[encoder_index + 1].compression_time_ms = statistics(encoder_times).median; + if (encoder == m_gpu_encoder) { + gpu_total_times = std::move(encoder_times); + gpu_destination = std::move(destination); + gpu_compressor = std::move(compressor); } } + Q_ASSERT(gpu_destination && gpu_compressor); // Stage timings are collected in a separate profiling phase. Each stage is completed // independently, so these values diagnose where time is spent but are not summed to @@ -1028,6 +1107,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { const auto gpu_output_transfer = statistics(gpu_output_transfer_times.total); const auto gpu_compressed_upload = statistics(gpu_compressed_upload_times.total); const auto gpu_total = statistics(gpu_total_times); + m_preview_results[0].compression_time_ms = cpu_total.median; const auto algorithm_name = algorithm == nucleus::utils::ColourTexture::Format::DXT1 ? QStringLiteral("DXT1 / BC1") : QStringLiteral("ETC1 in ETC2"); const auto cpu_backend_name = cpu_encoder_name(m_cpu_encoder); @@ -1223,9 +1303,13 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { BenchmarkItem::GpuEncoder m_gpu_encoder = BenchmarkItem::GpuEncoder::FastSplitFused; int m_effort = 4; bool m_mipmaps = true; + int m_preview_encoder = 0; bool m_pending = false; std::vector m_source_images; - QString m_preview_source; + std::vector m_preview_results; + std::array, 1 + gpu_encoders.size()> m_preview_textures; + std::unique_ptr m_preview_shader; + gl_engine::helpers::ScreenQuadGeometry m_preview_geometry; std::unique_ptr m_gpu_timer; std::optional m_pending_gpu_report; }; @@ -1234,6 +1318,7 @@ BenchmarkItem::BenchmarkItem(QQuickItem* parent) : QQuickFramebufferObject(parent) , m_network_manager(new QNetworkAccessManager(this)) { + setMirrorVertically(true); downloadBenchmarkData(); } @@ -1301,7 +1386,45 @@ QString BenchmarkItem::dataStatus() const { return m_data_status; } bool BenchmarkItem::running() const { return m_running; } QString BenchmarkItem::resultText() const { return m_result_text; } QString BenchmarkItem::resultJson() const { return m_result_json; } -QString BenchmarkItem::previewSource() const { return m_preview_source; } +int BenchmarkItem::previewEncoder() const { return m_preview_encoder; } +void BenchmarkItem::setPreviewEncoder(int value) +{ + if (!m_preview_results.empty()) + value = std::clamp(value, 0, int(m_preview_results.size()) - 1); + else + value = 0; + if (m_preview_encoder == value) + return; + m_preview_encoder = value; + emit previewEncoderChanged(); + emit previewDetailsChanged(); + update(); +} + +bool BenchmarkItem::previewReady() const { return !m_preview_results.empty(); } +QStringList BenchmarkItem::previewEncoders() const +{ + QStringList result; + result.reserve(qsizetype(m_preview_results.size())); + for (const auto& preview : m_preview_results) + result.push_back(preview.name); + return result; +} + +QString BenchmarkItem::previewName() const +{ + return previewReady() ? m_preview_results[size_t(m_preview_encoder)].name : QString {}; +} + +double BenchmarkItem::previewPsnr() const +{ + return previewReady() ? m_preview_results[size_t(m_preview_encoder)].psnr : 0.0; +} + +double BenchmarkItem::previewCompressionTime() const +{ + return previewReady() ? m_preview_results[size_t(m_preview_encoder)].compression_time_ms : 0.0; +} void BenchmarkItem::downloadBenchmarkData() { @@ -1376,12 +1499,15 @@ void BenchmarkItem::runBenchmark() m_running = true; m_result_text = QStringLiteral("Computing PSNR, then measuring batch size 4…"); m_result_json.clear(); - m_preview_source.clear(); + m_preview_results.clear(); + m_preview_encoder = 0; ++m_request_serial; emit runningChanged(); emit resultTextChanged(); emit resultJsonChanged(); - emit previewSourceChanged(); + emit previewEncoderChanged(); + emit previewResultsChanged(); + emit previewDetailsChanged(); update(); } @@ -1391,14 +1517,18 @@ void BenchmarkItem::copyResultJson() QGuiApplication::clipboard()->setText(m_result_json); } -void BenchmarkItem::publishResults(const QString& text, const QString& json, const QString& preview_source) +void BenchmarkItem::publishResults( + const QString& text, const QString& json, const std::vector& preview_results) { m_result_text = text; m_result_json = json; - m_preview_source = preview_source; + m_preview_results = preview_results; + m_preview_encoder = std::clamp(m_preview_encoder, 0, std::max(0, int(m_preview_results.size()) - 1)); m_running = false; emit resultTextChanged(); emit resultJsonChanged(); - emit previewSourceChanged(); + emit previewResultsChanged(); + emit previewDetailsChanged(); emit runningChanged(); + update(); } diff --git a/apps/texture_compression_benchmark/BenchmarkItem.h b/apps/texture_compression_benchmark/BenchmarkItem.h index 6b9930c7..224d7097 100644 --- a/apps/texture_compression_benchmark/BenchmarkItem.h +++ b/apps/texture_compression_benchmark/BenchmarkItem.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -28,7 +29,12 @@ class BenchmarkItem : public QQuickFramebufferObject { Q_PROPERTY(bool running READ running NOTIFY runningChanged) Q_PROPERTY(QString resultText READ resultText NOTIFY resultTextChanged) Q_PROPERTY(QString resultJson READ resultJson NOTIFY resultJsonChanged) - Q_PROPERTY(QString previewSource READ previewSource NOTIFY previewSourceChanged) + Q_PROPERTY(int previewEncoder READ previewEncoder WRITE setPreviewEncoder NOTIFY previewEncoderChanged) + Q_PROPERTY(bool previewReady READ previewReady NOTIFY previewResultsChanged) + Q_PROPERTY(QStringList previewEncoders READ previewEncoders NOTIFY previewResultsChanged) + Q_PROPERTY(QString previewName READ previewName NOTIFY previewDetailsChanged) + Q_PROPERTY(double previewPsnr READ previewPsnr NOTIFY previewDetailsChanged) + Q_PROPERTY(double previewCompressionTime READ previewCompressionTime NOTIFY previewDetailsChanged) public: enum class CpuEncoder { Goofy, BasisEtc1s, BasisUastcLdr4x4, BasisXuastcLdr4x4 }; @@ -36,6 +42,12 @@ class BenchmarkItem : public QQuickFramebufferObject { enum class GpuEncoder { Search, FastRange, FastSplit, FastSplitFused, FastSplitBounds }; Q_ENUM(GpuEncoder) + struct PreviewResult { + QString name; + double psnr = 0.0; + double compression_time_ms = 0.0; + }; + explicit BenchmarkItem(QQuickItem* parent = nullptr); Renderer* createRenderer() const override; @@ -56,7 +68,13 @@ class BenchmarkItem : public QQuickFramebufferObject { [[nodiscard]] bool running() const; [[nodiscard]] QString resultText() const; [[nodiscard]] QString resultJson() const; - [[nodiscard]] QString previewSource() const; + [[nodiscard]] int previewEncoder() const; + void setPreviewEncoder(int value); + [[nodiscard]] bool previewReady() const; + [[nodiscard]] QStringList previewEncoders() const; + [[nodiscard]] QString previewName() const; + [[nodiscard]] double previewPsnr() const; + [[nodiscard]] double previewCompressionTime() const; Q_INVOKABLE void runBenchmark(); Q_INVOKABLE void copyResultJson(); @@ -73,13 +91,15 @@ class BenchmarkItem : public QQuickFramebufferObject { void runningChanged(); void resultTextChanged(); void resultJsonChanged(); - void previewSourceChanged(); + void previewEncoderChanged(); + void previewResultsChanged(); + void previewDetailsChanged(); private: friend class BenchmarkRenderer; void downloadBenchmarkData(); void stitchBenchmarkData(); - void publishResults(const QString& text, const QString& json, const QString& preview_source); + void publishResults(const QString& text, const QString& json, const std::vector& preview_results); CpuEncoder m_cpu_encoder = CpuEncoder::Goofy; int m_basis_quality = 75; @@ -97,5 +117,6 @@ class BenchmarkItem : public QQuickFramebufferObject { QString m_data_status = QStringLiteral("Downloading benchmark imagery…"); QString m_result_text = QStringLiteral("Waiting for benchmark imagery."); QString m_result_json; - QString m_preview_source; + int m_preview_encoder = 0; + std::vector m_preview_results; }; diff --git a/apps/texture_compression_benchmark/Main.qml b/apps/texture_compression_benchmark/Main.qml index 29a716ba..bc548603 100644 --- a/apps/texture_compression_benchmark/Main.qml +++ b/apps/texture_compression_benchmark/Main.qml @@ -17,11 +17,16 @@ ApplicationWindow { BenchmarkItem { id: benchmark + + readonly property bool showingPreview: previewDialogLoader.status === Loader.Ready + && previewDialogLoader.item.opened + + parent: showingPreview ? previewDialogLoader.item.previewHost : root.contentItem x: 0 y: 0 - z: -1 - width: 1 - height: 1 + z: showingPreview ? 0 : -1 + width: showingPreview ? parent.width : 1 + height: showingPreview ? parent.height : 1 } ScrollView { @@ -216,9 +221,14 @@ ApplicationWindow { Layout.fillWidth: true Button { - text: qsTr("Preview selected CPU encoder") - enabled: !benchmark.running && benchmark.previewSource.length > 0 - onClicked: previewDialogLoader.active = true + text: qsTr("Preview encoders") + enabled: !benchmark.running && benchmark.previewReady + onClicked: { + if (previewDialogLoader.status === Loader.Ready) + previewDialogLoader.item.open() + else + previewDialogLoader.active = true + } } Button { @@ -246,61 +256,74 @@ ApplicationWindow { Dialog { id: previewDialog + property alias previewHost: previewHost + parent: Overlay.overlay anchors.centerIn: parent width: Math.min(root.width - 40, 820) height: Math.min(root.height - 40, 880) modal: true - title: qsTr("Selected CPU-compressed tiles") + title: qsTr("Compressed tile preview") standardButtons: Dialog.Close - onClosed: previewDialogLoader.active = false - - contentItem: Flickable { - id: previewViewport - - property real zoom: 1.0 - property real pinchStartZoom: 1.0 - property real pinchStartContentX: 0.0 - property real pinchStartContentY: 0.0 - - clip: true - boundsBehavior: Flickable.StopAtBounds - contentWidth: Math.max(width, previewImage.width) - contentHeight: Math.max(height, previewImage.height) - - Image { - id: previewImage - - readonly property real fittedSize: Math.min(previewViewport.width, previewViewport.height) - - x: (previewViewport.contentWidth - width) / 2 - y: (previewViewport.contentHeight - height) / 2 - width: fittedSize * previewViewport.zoom - height: width - source: benchmark.previewSource - sourceSize: Qt.size(2048, 2048) - asynchronous: true - fillMode: Image.PreserveAspectFit + + contentItem: ColumnLayout { + spacing: 8 + + ComboBox { + Layout.fillWidth: true + model: benchmark.previewEncoders + currentIndex: benchmark.previewEncoder + onActivated: benchmark.previewEncoder = currentIndex } - PinchHandler { - target: null + Item { + id: previewContainer - onActiveChanged: { - if (active) { - previewViewport.pinchStartZoom = previewViewport.zoom - previewViewport.pinchStartContentX = previewViewport.contentX - previewViewport.pinchStartContentY = previewViewport.contentY - } else { - previewViewport.returnToBounds() + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: 240 + + Flickable { + id: previewViewport + + anchors.fill: parent + clip: true + boundsBehavior: Flickable.StopAtBounds + contentWidth: Math.max(width, previewHost.width * previewHost.scale) + contentHeight: Math.max(height, previewHost.height * previewHost.scale) + + Item { + id: previewHost + + readonly property real fittedSize: Math.min(previewViewport.width, previewViewport.height) + + x: (previewViewport.contentWidth - width) / 2 + y: (previewViewport.contentHeight - height) / 2 + width: fittedSize + height: width + + PinchHandler { + target: previewHost + rotationAxis.enabled: false + xAxis.enabled: false + yAxis.enabled: false + scaleAxis.minimum: 1 + scaleAxis.maximum: 8 + } } } - onActiveScaleChanged: { - const newZoom = Math.max(1.0, Math.min(8.0, previewViewport.pinchStartZoom * activeScale)) - const zoomRatio = newZoom / previewViewport.pinchStartZoom - previewViewport.zoom = newZoom - previewViewport.contentX = (previewViewport.pinchStartContentX + centroid.position.x) * zoomRatio - centroid.position.x - previewViewport.contentY = (previewViewport.pinchStartContentY + centroid.position.y) * zoomRatio - centroid.position.y + + Frame { + anchors.left: parent.left + anchors.top: parent.top + anchors.margins: 8 + + Label { + text: qsTr("%1\nPSNR: %2 dB\nCompleted compression: %3 ms") + .arg(benchmark.previewName) + .arg(benchmark.previewPsnr.toFixed(2)) + .arg(benchmark.previewCompressionTime.toFixed(3)) + } } } } From c47a843910e3f0ece0276469fa5f9c4059b7dff9 Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Thu, 20 Aug 2026 11:47:45 +0200 Subject: [PATCH 19/38] Improve texture encoder previews --- .../BenchmarkItem.cpp | 43 ++++++++++++++----- apps/texture_compression_benchmark/Main.qml | 22 +++++++--- 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/apps/texture_compression_benchmark/BenchmarkItem.cpp b/apps/texture_compression_benchmark/BenchmarkItem.cpp index 51e55a61..046bb538 100644 --- a/apps/texture_compression_benchmark/BenchmarkItem.cpp +++ b/apps/texture_compression_benchmark/BenchmarkItem.cpp @@ -324,6 +324,9 @@ constexpr std::array gpu_encoders { BenchmarkItem::GpuEncoder::FastSplitFused, BenchmarkItem::GpuEncoder::FastSplitBounds, }; +constexpr size_t uncompressed_preview_index = 0; +constexpr size_t cpu_preview_index = 1; +constexpr size_t first_gpu_preview_index = 2; QString gpu_encoder_name(BenchmarkItem::GpuEncoder encoder, int effort) { @@ -566,13 +569,19 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { uniform lowp sampler2DArray texture_sampler; in highp vec2 texcoords; out lowp vec4 out_color; + highp vec3 linear_to_srgb(highp vec3 linear) { + return mix(12.92 * linear, + 1.055 * pow(linear, vec3(1.0 / 2.4)) - 0.055, + step(vec3(0.0031308), linear)); + } void main() { highp vec2 grid_position = texcoords * 4.0; highp ivec2 cell = min(ivec2(grid_position), ivec2(3)); highp float layer = float((3 - cell.y) * 4 + cell.x); highp vec2 tile_coordinates = fract(grid_position); - out_color = textureLod(texture_sampler, + highp vec4 linear_color = textureLod(texture_sampler, vec3(tile_coordinates.x, 1.0 - tile_coordinates.y, layer), 0.0); + out_color = vec4(linear_to_srgb(linear_color.rgb), linear_color.a); })", gl_engine::ShaderCodeSource::PLAINTEXT); m_preview_geometry = gl_engine::helpers::create_screen_quad_geometry(); @@ -938,26 +947,38 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { double gpu_psnr = 0.0; { m_preview_results.clear(); - m_preview_results.reserve(1 + gpu_encoders.size()); - m_preview_textures[0] = std::make_unique( + m_preview_results.reserve(first_gpu_preview_index + gpu_encoders.size()); + + m_preview_textures[uncompressed_preview_index] = std::make_unique( + gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::SRGBA8); + m_preview_textures[uncompressed_preview_index]->setParams( + gl_engine::Texture::Filter::Linear, gl_engine::Texture::Filter::Linear); + m_preview_textures[uncompressed_preview_index]->allocate_array( + resolution, resolution, unsigned(tile_groups.size())); + for (size_t layer = 0; layer < all_sources.size(); ++layer) + m_preview_textures[uncompressed_preview_index]->upload(all_sources[layer], unsigned(layer)); + m_preview_results.push_back( + { QStringLiteral("Uncompressed reference"), std::numeric_limits::infinity(), -1.0 }); + + m_preview_textures[cpu_preview_index] = std::make_unique( gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); - m_preview_textures[0]->setParams(filter, gl_engine::Texture::Filter::Linear); - m_preview_textures[0]->allocate_array(resolution, resolution, unsigned(tile_groups.size())); + m_preview_textures[cpu_preview_index]->setParams(filter, gl_engine::Texture::Filter::Linear); + m_preview_textures[cpu_preview_index]->allocate_array(resolution, resolution, unsigned(tile_groups.size())); auto cpu_quality = compress_cpu(all_sources); if (!cpu_quality) return compression_error(cpu_quality.error()); - static_cast(upload_cpu(*m_preview_textures[0], cpu_quality->textures)); + static_cast(upload_cpu(*m_preview_textures[cpu_preview_index], cpu_quality->textures)); std::vector cpu_reconstructed; cpu_reconstructed.reserve(all_sources.size()); for (unsigned layer = 0; layer < all_sources.size(); ++layer) - cpu_reconstructed.push_back(reconstruct(*m_preview_textures[0], resolution, layer)); + cpu_reconstructed.push_back(reconstruct(*m_preview_textures[cpu_preview_index], resolution, layer)); cpu_psnr = linear_psnr(cpu_reconstructed, all_sources); m_preview_results.push_back({ QStringLiteral("CPU %1").arg(cpu_encoder_name(m_cpu_encoder)), cpu_psnr, 0.0 }); for (size_t encoder_index = 0; encoder_index < gpu_encoders.size(); ++encoder_index) { const auto encoder = gpu_encoders[encoder_index]; - auto& preview_texture = m_preview_textures[encoder_index + 1]; + auto& preview_texture = m_preview_textures[encoder_index + first_gpu_preview_index]; preview_texture = std::make_unique( gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); preview_texture->setParams(filter, gl_engine::Texture::Filter::Linear); @@ -1061,7 +1082,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { encoder_times.push_back(gpu.timings.total_ms); } } - m_preview_results[encoder_index + 1].compression_time_ms = statistics(encoder_times).median; + m_preview_results[encoder_index + first_gpu_preview_index].compression_time_ms = statistics(encoder_times).median; if (encoder == m_gpu_encoder) { gpu_total_times = std::move(encoder_times); gpu_destination = std::move(destination); @@ -1107,7 +1128,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { const auto gpu_output_transfer = statistics(gpu_output_transfer_times.total); const auto gpu_compressed_upload = statistics(gpu_compressed_upload_times.total); const auto gpu_total = statistics(gpu_total_times); - m_preview_results[0].compression_time_ms = cpu_total.median; + m_preview_results[cpu_preview_index].compression_time_ms = cpu_total.median; const auto algorithm_name = algorithm == nucleus::utils::ColourTexture::Format::DXT1 ? QStringLiteral("DXT1 / BC1") : QStringLiteral("ETC1 in ETC2"); const auto cpu_backend_name = cpu_encoder_name(m_cpu_encoder); @@ -1307,7 +1328,7 @@ class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { bool m_pending = false; std::vector m_source_images; std::vector m_preview_results; - std::array, 1 + gpu_encoders.size()> m_preview_textures; + std::array, first_gpu_preview_index + gpu_encoders.size()> m_preview_textures; std::unique_ptr m_preview_shader; gl_engine::helpers::ScreenQuadGeometry m_preview_geometry; std::unique_ptr m_gpu_timer; diff --git a/apps/texture_compression_benchmark/Main.qml b/apps/texture_compression_benchmark/Main.qml index bc548603..f5ffd001 100644 --- a/apps/texture_compression_benchmark/Main.qml +++ b/apps/texture_compression_benchmark/Main.qml @@ -313,16 +313,28 @@ ApplicationWindow { } } - Frame { + Rectangle { anchors.left: parent.left anchors.top: parent.top anchors.margins: 8 + implicitWidth: previewDetails.implicitWidth + 24 + implicitHeight: previewDetails.implicitHeight + 24 + color: Qt.rgba(1, 1, 1, 0.8) + radius: 4 Label { - text: qsTr("%1\nPSNR: %2 dB\nCompleted compression: %3 ms") - .arg(benchmark.previewName) - .arg(benchmark.previewPsnr.toFixed(2)) - .arg(benchmark.previewCompressionTime.toFixed(3)) + id: previewDetails + + readonly property bool showingReference: benchmark.previewCompressionTime < 0 + + anchors.fill: parent + anchors.margins: 12 + text: showingReference + ? qsTr("%1\nPSNR: ∞\nCompression: N/A").arg(benchmark.previewName) + : qsTr("%1\nPSNR: %2 dB\nCompleted compression: %3 ms") + .arg(benchmark.previewName) + .arg(benchmark.previewPsnr.toFixed(2)) + .arg(benchmark.previewCompressionTime.toFixed(3)) } } } From 017a0bc62b33caee21818c5cd60275dc1f60944c Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Thu, 20 Aug 2026 16:06:06 +0200 Subject: [PATCH 20/38] Fix threaded WASM compression benchmark --- apps/texture_compression_benchmark/BenchmarkItem.cpp | 8 ++++---- apps/texture_compression_benchmark/CMakeLists.txt | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/texture_compression_benchmark/BenchmarkItem.cpp b/apps/texture_compression_benchmark/BenchmarkItem.cpp index 046bb538..88f926e6 100644 --- a/apps/texture_compression_benchmark/BenchmarkItem.cpp +++ b/apps/texture_compression_benchmark/BenchmarkItem.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #if defined(__EMSCRIPTEN__) #include #endif @@ -1459,10 +1460,9 @@ void BenchmarkItem::downloadBenchmarkData() auto* reply = m_network_manager->get(QNetworkRequest(QUrl(url))); connect(reply, &QNetworkReply::finished, this, [this, reply, tile_index, url]() { if (reply->error() == QNetworkReply::NoError) { - QImage image; - image.loadFromData(reply->readAll(), "JPEG"); - if (!image.isNull() && image.size() == QSize(256, 256)) - m_downloaded_tiles[tile_index] = image.convertToFormat(QImage::Format_RGBA8888); + const auto image = nucleus::utils::image_loader::rgba8(reply->readAll()); + if (image && image->size() == glm::uvec2(256u)) + m_downloaded_tiles[tile_index] = nucleus::tile::conversion::to_QImage(*image); } if (m_downloaded_tiles[tile_index].isNull() && !m_data_status.startsWith(QStringLiteral("Unable"))) { m_data_status = QStringLiteral("Unable to download benchmark tile: %1").arg(url); diff --git a/apps/texture_compression_benchmark/CMakeLists.txt b/apps/texture_compression_benchmark/CMakeLists.txt index 35490b36..029070ec 100644 --- a/apps/texture_compression_benchmark/CMakeLists.txt +++ b/apps/texture_compression_benchmark/CMakeLists.txt @@ -16,6 +16,9 @@ set(BASISU_ZSTD OFF CACHE BOOL "" FORCE) set(BASISU_OPENCL OFF CACHE BOOL "" FORCE) set(BASISU_DISABLE_ANDROID_ASTC_DECOMP ON CACHE BOOL "" FORCE) add_subdirectory("${basisu_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/basisu" EXCLUDE_FROM_ALL) +if (EMSCRIPTEN AND ALP_ENABLE_THREADING) + target_compile_options(basisu_encoder PRIVATE -pthread) +endif() add_library(alp_basisu_texture_compression STATIC ${CMAKE_SOURCE_DIR}/nucleus/utils/BasisUniversalTextureCompression.h From 48040ee1621207b8e7860618e3a2026920d8db28 Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Fri, 21 Aug 2026 10:49:05 +0200 Subject: [PATCH 21/38] Simplify texture compression preview --- CMakeLists.txt | 6 +- .../BenchmarkItem.cpp | 1555 ----------------- .../BenchmarkItem.h | 122 -- .../CMakeLists.txt | 69 +- apps/texture_compression_benchmark/Main.qml | 375 +--- .../TexturePreviewItem.cpp | 458 +++++ .../TexturePreviewItem.h | 68 + .../android/AndroidManifest.xml | 6 +- apps/texture_compression_benchmark/main.cpp | 6 +- gl_engine/Texture.cpp | 343 +--- gl_engine/Texture.h | 64 - .../BasisUniversalTextureCompression.cpp | 175 -- .../utils/BasisUniversalTextureCompression.h | 46 - unittests/gl_engine/texture.cpp | 34 - unittests/nucleus/CMakeLists.txt | 4 - .../basis_universal_texture_compression.cpp | 51 - 16 files changed, 634 insertions(+), 2748 deletions(-) delete mode 100644 apps/texture_compression_benchmark/BenchmarkItem.cpp delete mode 100644 apps/texture_compression_benchmark/BenchmarkItem.h create mode 100644 apps/texture_compression_benchmark/TexturePreviewItem.cpp create mode 100644 apps/texture_compression_benchmark/TexturePreviewItem.h delete mode 100644 nucleus/utils/BasisUniversalTextureCompression.cpp delete mode 100644 nucleus/utils/BasisUniversalTextureCompression.h delete mode 100644 unittests/nucleus/basis_universal_texture_compression.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 1053695f..7d55d803 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,7 +25,7 @@ option(ALP_BUILD_UNITTESTS "include unit test targets in the buildsystem" ON) option(ALP_BUILD_GL_ENGINE "include the gl engine in the buildsystem" OFF) option(ALP_BUILD_PLAIN_RENDERER "include the plain renderer in the buildsystem" ON) option(ALP_BUILD_ALPINEAPP "include the qml app in the buildsystem" ON) -option(ALP_BUILD_TEXTURE_COMPRESSION_BENCHMARK "include the texture compression benchmark application" OFF) +option(ALP_BUILD_TEXTURE_COMPRESSION_PREVIEW "include the texture compression preview application" OFF) option(ALP_BUILD_WEBGPU_BASE "include the webgpu base library in the buildsystem" OFF) option(ALP_BUILD_WEBGPU_ENGINE "include the webgpu engine in the buildsystem" OFF) option(ALP_BUILD_WEBGPU_COMPUTE "include the webgpu compute library in the buildsystem" OFF) @@ -121,7 +121,7 @@ endif() add_subdirectory(nucleus) -if (ALP_BUILD_GL_ENGINE OR ALP_BUILD_PLAIN_RENDERER OR ALP_BUILD_ALPINEAPP OR ALP_BUILD_TEXTURE_COMPRESSION_BENCHMARK) +if (ALP_BUILD_GL_ENGINE OR ALP_BUILD_PLAIN_RENDERER OR ALP_BUILD_ALPINEAPP OR ALP_BUILD_TEXTURE_COMPRESSION_PREVIEW) add_subdirectory(gl_engine) endif() if (ALP_BUILD_PLAIN_RENDERER) @@ -136,7 +136,7 @@ if (ALP_BUILD_ALPINEAPP) endif() add_subdirectory(app) endif() -if (ALP_BUILD_TEXTURE_COMPRESSION_BENCHMARK) +if (ALP_BUILD_TEXTURE_COMPRESSION_PREVIEW) add_subdirectory(apps/texture_compression_benchmark) endif() diff --git a/apps/texture_compression_benchmark/BenchmarkItem.cpp b/apps/texture_compression_benchmark/BenchmarkItem.cpp deleted file mode 100644 index 88f926e6..00000000 --- a/apps/texture_compression_benchmark/BenchmarkItem.cpp +++ /dev/null @@ -1,1555 +0,0 @@ -/***************************************************************************** - * AlpineMaps.org - * Copyright (C) 2026 Adam Celarek - * SPDX-License-Identifier: GPL-3.0-or-later - *****************************************************************************/ - -#include "BenchmarkItem.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#if defined(__EMSCRIPTEN__) -#include -#endif - -namespace { -using Raster = radix::Raster; -using Clock = std::chrono::steady_clock; - -struct TileGroup { - const char* name; - const char* category; - int zoom; - int y; - int x; -}; - -constexpr std::array tile_groups { { - { "Vienna", "city", 17, 45448, 71496 }, - { "Salzburg", "city", 16, 22832, 35144 }, - { "Graz", "city", 16, 23030, 35578 }, - { "Neusiedler See", "lake", 13, 2852, 4476 }, - { "Attersee", "lake", 14, 5702, 8808 }, - { "Wörthersee", "lake", 15, 11574, 17670 }, - { "Grossglockner", "mountain", 16, 23030, 35078 }, - { "Dachstein", "mountain", 15, 11460, 17622 }, - { "Arlberg", "mountain", 14, 5752, 8656 }, - { "Ötztal", "mountain", 16, 23084, 34746 }, - { "Wienerwald", "forest", 14, 5684, 8926 }, - { "Kalkalpen", "forest", 15, 11418, 17692 }, - { "Bregenzerwald", "forest", 16, 22956, 34570 }, - { "Marchfeld", "fields", 15, 11358, 17904 }, - { "Burgenland", "fields", 16, 22910, 35770 }, - { "Weinviertel", "fields", 14, 5656, 8938 }, -} }; - -QString tile_url(const TileGroup& group, int x_offset, int y_offset) -{ - return QStringLiteral("https://gataki.cg.tuwien.ac.at/raw/basemap/tiles/%1/%2/%3.jpeg") - .arg(group.zoom) - .arg(group.y + y_offset) - .arg(group.x + x_offset); -} - -QJsonArray dataset_json() -{ - QJsonArray result; - for (const auto& group : tile_groups) { - QJsonArray urls; - for (int y = 0; y < 2; ++y) { - for (int x = 0; x < 2; ++x) - urls.append(tile_url(group, x, y)); - } - result.append(QJsonObject { - { QStringLiteral("name"), QString::fromUtf8(group.name) }, - { QStringLiteral("category"), QString::fromLatin1(group.category) }, - { QStringLiteral("zoom"), group.zoom }, - { QStringLiteral("top_left_x"), group.x }, - { QStringLiteral("top_left_y"), group.y }, - { QStringLiteral("urls"), urls }, - }); - } - return result; -} - -struct Statistics { - double median = 0.0; - double p95 = 0.0; - double minimum = 0.0; - double maximum = 0.0; -}; - -Statistics statistics(std::vector values) -{ - Q_ASSERT(!values.empty()); - std::ranges::sort(values); - const auto percentile = [&](double fraction) { - const auto index = std::min(values.size() - 1, size_t(std::ceil(fraction * double(values.size()))) - 1); - return values[index]; - }; - return { percentile(0.5), percentile(0.95), values.front(), values.back() }; -} - -QJsonObject to_json(const Statistics& value) -{ - return { { QStringLiteral("median_ms"), value.median }, - { QStringLiteral("p95_ms"), value.p95 }, - { QStringLiteral("min_ms"), value.minimum }, - { QStringLiteral("max_ms"), value.maximum } }; -} - -QJsonObject size_to_json(const Statistics& value) -{ - return { { QStringLiteral("median_bytes"), value.median }, - { QStringLiteral("p95_bytes"), value.p95 }, - { QStringLiteral("min_bytes"), value.minimum }, - { QStringLiteral("max_bytes"), value.maximum } }; -} - -QJsonArray samples_to_json(const std::vector& values) -{ - QJsonArray result; - for (const auto value : values) - result.append(value); - return result; -} - -struct WallTimingSamples { - std::vector total; - std::vector submission; - std::vector completion_wait; - - void append(const gl_engine::TextureCompressor::StageTiming& timing) - { - total.push_back(timing.total_ms()); - submission.push_back(timing.submission_ms); - completion_wait.push_back(timing.completion_wait_ms); - } - - [[nodiscard]] QJsonObject statistics_json() const - { - return { - { QStringLiteral("total"), to_json(statistics(total)) }, - { QStringLiteral("submission"), to_json(statistics(submission)) }, - { QStringLiteral("completion_wait"), to_json(statistics(completion_wait)) }, - }; - } - - [[nodiscard]] QJsonObject raw_json() const - { - return { - { QStringLiteral("total"), samples_to_json(total) }, - { QStringLiteral("submission"), samples_to_json(submission) }, - { QStringLiteral("completion_wait"), samples_to_json(completion_wait) }, - }; - } -}; - -struct PendingFenceDiagnostic { - std::unique_ptr compressor; - std::unique_ptr destination; - std::unique_ptr probe_framebuffer; - std::unique_ptr probe_shader; - gl_engine::helpers::ScreenQuadGeometry probe_geometry; - std::vector source_pool; - std::vector sources; - std::vector layers; - gl_engine::TextureCompressor::Settings settings; - GLsync fence = nullptr; - Clock::time_point started_at; - int iteration = 0; - int iteration_count = 0; - int current_poll_count = 0; - GLenum wait_error = GL_NO_ERROR; - double failure_elapsed_ms = 0.0; - QString status = QStringLiteral("pending"); - std::vector submission_ms; - std::vector fence_completion_ms; - std::vector verification_readback_ms; - std::vector verified_end_to_end_ms; - std::vector poll_counts; - std::vector sample_checksums; - std::vector last_source_markers; - std::vector last_sampled_pixels; -}; - -struct PendingGpuReport { - QJsonObject root; - QStringList summary; - std::vector tickets; - std::vector> query_results; - std::vector query_finished; - int disjoint_samples = 0; - std::optional fence_diagnostic; -}; - -double elapsed_ms(Clock::time_point start) -{ - return std::chrono::duration(Clock::now() - start).count(); -} - -double srgb_to_linear(uint8_t value) -{ - const auto normalised = double(value) / 255.0; - if (normalised <= 0.04045) - return normalised / 12.92; - return std::pow((normalised + 0.055) / 1.055, 2.4); -} - -double linear_squared_error(const QImage& reconstructed, const Raster& source) -{ - double squared_error = 0.0; - for (int y = 0; y < reconstructed.height(); ++y) { - for (int x = 0; x < reconstructed.width(); ++x) { - const auto actual = reconstructed.pixel(x, y); - const auto expected = source.pixel({ x, y }); - const std::array actual_channels { qRed(actual) / 255.0, qGreen(actual) / 255.0, qBlue(actual) / 255.0 }; - const std::array expected_channels { - srgb_to_linear(expected.x), srgb_to_linear(expected.y), srgb_to_linear(expected.z) - }; - for (size_t channel = 0; channel < actual_channels.size(); ++channel) { - const auto difference = actual_channels[channel] - expected_channels[channel]; - squared_error += difference * difference; - } - } - } - return squared_error; -} - -double linear_psnr(std::span reconstructed, std::span sources) -{ - Q_ASSERT(reconstructed.size() == sources.size()); - double squared_error = 0.0; - uint64_t channel_count = 0; - for (size_t i = 0; i < sources.size(); ++i) { - squared_error += linear_squared_error(reconstructed[i], sources[i]); - channel_count += uint64_t(reconstructed[i].width()) * uint64_t(reconstructed[i].height()) * 3; - } - const auto mse = squared_error / double(channel_count); - return mse == 0.0 ? std::numeric_limits::infinity() : 10.0 * std::log10(1.0 / mse); -} - -QImage reconstruct(gl_engine::Texture& texture, unsigned resolution, unsigned layer) -{ - gl_engine::Framebuffer framebuffer( - gl_engine::Framebuffer::DepthFormat::None, { gl_engine::Framebuffer::ColourFormat::RGBA8 }, { resolution, resolution }); - framebuffer.bind(); - gl_engine::ShaderProgram shader(R"( - out highp vec2 texcoords; - void main() { - vec2 vertices[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); - gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0); - texcoords = 0.5 * gl_Position.xy + vec2(0.5); - })", - R"( - uniform lowp sampler2DArray texture_sampler; - uniform highp float texture_layer; - in highp vec2 texcoords; - out lowp vec4 out_color; - void main() { - out_color = textureLod(texture_sampler, vec3(texcoords.x, 1.0 - texcoords.y, texture_layer), 0.0); - })", - gl_engine::ShaderCodeSource::PLAINTEXT); - shader.bind(); - texture.bind(0); - shader.set_uniform("texture_sampler", 0); - shader.set_uniform("texture_layer", float(layer)); - gl_engine::helpers::create_screen_quad_geometry().draw(); - auto result = framebuffer.read_colour_attachment(0); - gl_engine::Framebuffer::unbind(); - return result; -} - -struct CpuCompressionResult { - std::vector textures; - double source_preparation_ms = 0.0; - double encoding_ms = 0.0; - double transcoding_ms = 0.0; - size_t intermediate_bytes = 0; - size_t transcoded_bytes = 0; -}; - -QString cpu_encoder_name(BenchmarkItem::CpuEncoder encoder) -{ - switch (encoder) { - case BenchmarkItem::CpuEncoder::Goofy: - return QStringLiteral("Goofy direct"); - case BenchmarkItem::CpuEncoder::BasisEtc1s: - return QString::fromLatin1(nucleus::utils::basis_universal_format_name(nucleus::utils::BasisUniversalFormat::ETC1S)); - case BenchmarkItem::CpuEncoder::BasisUastcLdr4x4: - return QString::fromLatin1(nucleus::utils::basis_universal_format_name(nucleus::utils::BasisUniversalFormat::UASTC_LDR_4x4)); - case BenchmarkItem::CpuEncoder::BasisXuastcLdr4x4: - return QString::fromLatin1(nucleus::utils::basis_universal_format_name(nucleus::utils::BasisUniversalFormat::XUASTC_LDR_4x4)); - } - return QStringLiteral("Unknown"); -} - -constexpr std::array gpu_encoders { - BenchmarkItem::GpuEncoder::Search, - BenchmarkItem::GpuEncoder::FastRange, - BenchmarkItem::GpuEncoder::FastSplit, - BenchmarkItem::GpuEncoder::FastSplitFused, - BenchmarkItem::GpuEncoder::FastSplitBounds, -}; -constexpr size_t uncompressed_preview_index = 0; -constexpr size_t cpu_preview_index = 1; -constexpr size_t first_gpu_preview_index = 2; - -QString gpu_encoder_name(BenchmarkItem::GpuEncoder encoder, int effort) -{ - switch (encoder) { - case BenchmarkItem::GpuEncoder::Search: - return QStringLiteral("GPU Search (reference), effort %1").arg(effort); - case BenchmarkItem::GpuEncoder::FastRange: - return QStringLiteral("GPU Fast range"); - case BenchmarkItem::GpuEncoder::FastSplit: - return QStringLiteral("GPU Fast split"); - case BenchmarkItem::GpuEncoder::FastSplitFused: - return QStringLiteral("GPU Fast split fused"); - case BenchmarkItem::GpuEncoder::FastSplitBounds: - return QStringLiteral("GPU Fast split bounds"); - } - return QStringLiteral("GPU Search (reference)"); -} - -QString gpu_backend_name(BenchmarkItem::GpuEncoder encoder) -{ - switch (encoder) { - case BenchmarkItem::GpuEncoder::Search: - return QStringLiteral("Fragment shader search + PBO"); - case BenchmarkItem::GpuEncoder::FastRange: - return QStringLiteral("Fragment shader fast range + PBO"); - case BenchmarkItem::GpuEncoder::FastSplit: - return QStringLiteral("Fragment shader fast split + PBO"); - case BenchmarkItem::GpuEncoder::FastSplitFused: - return QStringLiteral("Fragment shader fast split fused + PBO"); - case BenchmarkItem::GpuEncoder::FastSplitBounds: - return QStringLiteral("Fragment shader fast split bounds + PBO"); - } - return QStringLiteral("Fragment shader search + PBO"); -} - -gl_engine::TextureCompressor::Encoder compressor_encoder(BenchmarkItem::GpuEncoder encoder) -{ - switch (encoder) { - case BenchmarkItem::GpuEncoder::Search: - return gl_engine::TextureCompressor::Encoder::Search; - case BenchmarkItem::GpuEncoder::FastRange: - return gl_engine::TextureCompressor::Encoder::FastRange; - case BenchmarkItem::GpuEncoder::FastSplit: - return gl_engine::TextureCompressor::Encoder::FastSplit; - case BenchmarkItem::GpuEncoder::FastSplitFused: - return gl_engine::TextureCompressor::Encoder::FastSplitFused; - case BenchmarkItem::GpuEncoder::FastSplitBounds: - return gl_engine::TextureCompressor::Encoder::FastSplitBounds; - } - return gl_engine::TextureCompressor::Encoder::Search; -} - -nucleus::utils::BasisUniversalFormat basis_format(BenchmarkItem::CpuEncoder encoder) -{ - switch (encoder) { - case BenchmarkItem::CpuEncoder::BasisEtc1s: - return nucleus::utils::BasisUniversalFormat::ETC1S; - case BenchmarkItem::CpuEncoder::BasisUastcLdr4x4: - return nucleus::utils::BasisUniversalFormat::UASTC_LDR_4x4; - case BenchmarkItem::CpuEncoder::BasisXuastcLdr4x4: - return nucleus::utils::BasisUniversalFormat::XUASTC_LDR_4x4; - case BenchmarkItem::CpuEncoder::Goofy: - break; - } - return nucleus::utils::BasisUniversalFormat::ETC1S; -} - -std::expected cpu_compress(std::span sources, - nucleus::utils::ColourTexture::Format algorithm, - bool mipmaps, - BenchmarkItem::CpuEncoder encoder, - int basis_quality, - int basis_effort) -{ - CpuCompressionResult result; - result.textures.reserve(sources.size()); - for (const auto& source : sources) { - if (encoder == BenchmarkItem::CpuEncoder::Goofy) { - const auto start = Clock::now(); - if (mipmaps) { - result.textures.push_back(nucleus::utils::generate_mipmapped_colour_texture(source, algorithm)); - } else { - nucleus::utils::MipmappedColourTexture levels; - levels.emplace_back(source, algorithm); - result.textures.push_back(std::move(levels)); - } - result.encoding_ms += elapsed_ms(start); - for (const auto& level : result.textures.back()) - result.transcoded_bytes += level.n_bytes(); - continue; - } - - const nucleus::utils::BasisUniversalCompressionSettings settings { - .format = basis_format(encoder), - .target_format = algorithm, - .quality = basis_quality, - .effort = basis_effort, - .generate_mipmaps = mipmaps, - }; - auto compressed = nucleus::utils::compress_with_basis_universal(source, settings); - if (!compressed) - return std::unexpected(QString::fromStdString(compressed.error())); - result.source_preparation_ms += compressed->timings.source_preparation_ms; - result.encoding_ms += compressed->timings.encoding_ms; - result.transcoding_ms += compressed->timings.transcoding_ms; - result.intermediate_bytes += compressed->intermediate_bytes; - result.transcoded_bytes += compressed->transcoded_bytes; - result.textures.push_back(std::move(compressed->texture)); - } - return result; -} - -QString gl_string(GLenum name) -{ - const auto* value = QOpenGLContext::currentContext()->functions()->glGetString(name); - return value ? QString::fromLatin1(reinterpret_cast(value)) : QStringLiteral("unavailable"); -} - -QJsonArray pixels_to_json(const std::vector& pixels) -{ - QJsonArray result; - for (const auto& pixel : pixels) { - result.append(QJsonArray { int(pixel.x), int(pixel.y), int(pixel.z), int(pixel.w) }); - } - return result; -} - -GLsync create_gpu_fence(QOpenGLExtraFunctions* f) -{ -#if defined(__EMSCRIPTEN__) - static_cast(f); - return emscripten_glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); -#else - return f->glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); -#endif -} - -void flush_gpu_commands(QOpenGLExtraFunctions* f) -{ - f->glFlush(); -} - -GLenum poll_gpu_fence(QOpenGLExtraFunctions* f, GLsync fence) -{ -#if defined(__EMSCRIPTEN__) - GLint status = GL_UNSIGNALED; - f->glGetSynciv(fence, GL_SYNC_STATUS, 1, nullptr, &status); - return status == GL_SIGNALED ? GL_ALREADY_SIGNALED : GL_TIMEOUT_EXPIRED; -#else - return f->glClientWaitSync(fence, 0, 0); -#endif -} - -void delete_gpu_fence(QOpenGLExtraFunctions* f, GLsync fence) -{ -#if defined(__EMSCRIPTEN__) - static_cast(f); - emscripten_glDeleteSync(fence); -#else - f->glDeleteSync(fence); -#endif -} - -} // namespace - -class BenchmarkRenderer final : public QQuickFramebufferObject::Renderer { -public: - void synchronize(QQuickFramebufferObject* item) override - { - auto* benchmark_item = static_cast(item); - m_item = benchmark_item; - m_window = benchmark_item->window(); - m_preview_encoder = benchmark_item->m_preview_encoder; - if (benchmark_item->m_request_serial == m_seen_serial) - return; - m_seen_serial = benchmark_item->m_request_serial; - m_cpu_encoder = benchmark_item->m_cpu_encoder; - m_basis_quality = benchmark_item->m_basis_quality; - m_basis_effort = benchmark_item->m_basis_effort; - m_gpu_encoder = benchmark_item->m_gpu_encoder; - m_effort = benchmark_item->m_effort; - m_mipmaps = benchmark_item->m_mipmaps; - m_source_images = benchmark_item->m_source_images; - m_preview_results.clear(); - m_preview_textures = {}; - m_pending = true; - } - - void render() override - { - m_window->beginExternalCommands(); - std::optional> completed; - if (m_pending) { - m_pending = false; - const auto immediate = run(); - if (!m_pending_gpu_report) - completed = immediate; - } else { - if (m_pending_gpu_report) - completed = poll_gpu_report(); - } - draw_preview(); - m_window->endExternalCommands(); - if (completed) { - QPointer item = m_item; - const auto [text, json] = std::move(*completed); - const auto preview_results = m_preview_results; - QMetaObject::invokeMethod(m_item, [item, text, json, preview_results]() { - if (item) - item->publishResults(text, json, preview_results); - }); - } else if (m_pending || m_pending_gpu_report) { - request_another_frame(); - } - } - - QOpenGLFramebufferObject* createFramebufferObject(const QSize& size) override - { - QOpenGLFramebufferObjectFormat format; - format.setAttachment(QOpenGLFramebufferObject::NoAttachment); - return new QOpenGLFramebufferObject(size.expandedTo(QSize(1, 1)), format); - } - -private: - void draw_preview() - { - if (m_preview_encoder < 0 || size_t(m_preview_encoder) >= m_preview_textures.size() - || !m_preview_textures[size_t(m_preview_encoder)]) - return; - - if (!m_preview_shader) { - m_preview_shader = std::make_unique(R"( - out highp vec2 texcoords; - void main() { - highp vec2 vertices[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); - gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0); - texcoords = 0.5 * gl_Position.xy + vec2(0.5); - })", - R"( - uniform lowp sampler2DArray texture_sampler; - in highp vec2 texcoords; - out lowp vec4 out_color; - highp vec3 linear_to_srgb(highp vec3 linear) { - return mix(12.92 * linear, - 1.055 * pow(linear, vec3(1.0 / 2.4)) - 0.055, - step(vec3(0.0031308), linear)); - } - void main() { - highp vec2 grid_position = texcoords * 4.0; - highp ivec2 cell = min(ivec2(grid_position), ivec2(3)); - highp float layer = float((3 - cell.y) * 4 + cell.x); - highp vec2 tile_coordinates = fract(grid_position); - highp vec4 linear_color = textureLod(texture_sampler, - vec3(tile_coordinates.x, 1.0 - tile_coordinates.y, layer), 0.0); - out_color = vec4(linear_to_srgb(linear_color.rgb), linear_color.a); - })", - gl_engine::ShaderCodeSource::PLAINTEXT); - m_preview_geometry = gl_engine::helpers::create_screen_quad_geometry(); - } - - auto* f = QOpenGLContext::currentContext()->extraFunctions(); - framebufferObject()->bind(); - f->glViewport(0, 0, framebufferObject()->width(), framebufferObject()->height()); - f->glDisable(GL_BLEND); - f->glDisable(GL_CULL_FACE); - f->glDisable(GL_DEPTH_TEST); - f->glDisable(GL_SCISSOR_TEST); - m_preview_shader->bind(); - m_preview_textures[size_t(m_preview_encoder)]->bind(0); - m_preview_shader->set_uniform("texture_sampler", 0); - m_preview_geometry.draw(); - m_preview_shader->release(); - } - - void request_another_frame() - { - update(); - QPointer window = m_window; - QMetaObject::invokeMethod(m_window, [window]() { - if (window) - window->update(); - }); - } - - void begin_fence_sample(PendingFenceDiagnostic& diagnostic) - { - constexpr size_t batch_size = 4; - const auto group_offset = size_t(diagnostic.iteration % 4) * batch_size; - diagnostic.sources.assign( - diagnostic.source_pool.begin() + ptrdiff_t(group_offset), diagnostic.source_pool.begin() + ptrdiff_t(group_offset + batch_size)); - diagnostic.last_source_markers.clear(); - diagnostic.last_source_markers.reserve(diagnostic.sources.size()); - for (size_t layer = 0; layer < diagnostic.sources.size(); ++layer) { - const auto marker = glm::u8vec4(uint8_t((37 + 53 * layer + 29 * size_t(diagnostic.iteration)) % 256), - uint8_t((83 + 97 * layer + 47 * size_t(diagnostic.iteration)) % 256), - uint8_t((149 + 31 * layer + 71 * size_t(diagnostic.iteration)) % 256), - 255); - diagnostic.last_source_markers.push_back(marker); - const auto centre = diagnostic.sources[layer].size() / 2u; - for (unsigned y = centre.y - 8; y < centre.y + 8; ++y) { - for (unsigned x = centre.x - 8; x < centre.x + 8; ++x) - diagnostic.sources[layer].pixel({ x, y }) = marker; - } - } - - diagnostic.started_at = Clock::now(); - static_cast(diagnostic.compressor->compress( - diagnostic.sources, *diagnostic.destination, diagnostic.layers, diagnostic.settings)); - auto* f = QOpenGLContext::currentContext()->extraFunctions(); - diagnostic.fence = create_gpu_fence(f); - flush_gpu_commands(f); - diagnostic.submission_ms.push_back(elapsed_ms(diagnostic.started_at)); - diagnostic.current_poll_count = 0; - if (!diagnostic.fence) { - diagnostic.status = QStringLiteral("fence_creation_failed"); - diagnostic.wait_error = f->glGetError(); - } - } - - QImage read_fence_probe(PendingFenceDiagnostic& diagnostic) - { - diagnostic.probe_framebuffer->bind(); - diagnostic.probe_shader->bind(); - diagnostic.destination->bind(0); - diagnostic.probe_shader->set_uniform("texture_sampler", 0); - diagnostic.probe_geometry.draw(); - auto image = diagnostic.probe_framebuffer->read_colour_attachment(0); - gl_engine::Framebuffer::unbind(); - return image; - } - - bool poll_fence_diagnostic(PendingFenceDiagnostic& diagnostic) - { - if (diagnostic.status != QStringLiteral("pending")) - return true; - - ++diagnostic.current_poll_count; - auto* f = QOpenGLContext::currentContext()->extraFunctions(); - const auto wait_status = poll_gpu_fence(f, diagnostic.fence); - if (wait_status == GL_TIMEOUT_EXPIRED && elapsed_ms(diagnostic.started_at) < 5000.0) - return false; - if (wait_status == GL_TIMEOUT_EXPIRED) { - diagnostic.status = QStringLiteral("fence_timeout"); - diagnostic.failure_elapsed_ms = elapsed_ms(diagnostic.started_at); - delete_gpu_fence(f, diagnostic.fence); - diagnostic.fence = nullptr; - return true; - } - if (wait_status == GL_WAIT_FAILED) { - diagnostic.status = QStringLiteral("client_wait_failed"); - diagnostic.wait_error = f->glGetError(); - delete_gpu_fence(f, diagnostic.fence); - diagnostic.fence = nullptr; - return true; - } - if (wait_status != GL_ALREADY_SIGNALED && wait_status != GL_CONDITION_SATISFIED) { - diagnostic.status = QStringLiteral("unexpected_wait_status"); - diagnostic.wait_error = wait_status; - delete_gpu_fence(f, diagnostic.fence); - diagnostic.fence = nullptr; - return true; - } - - diagnostic.fence_completion_ms.push_back(elapsed_ms(diagnostic.started_at)); - diagnostic.poll_counts.push_back(diagnostic.current_poll_count); - delete_gpu_fence(f, diagnostic.fence); - diagnostic.fence = nullptr; - - const auto readback_start = Clock::now(); - const auto sampled = read_fence_probe(diagnostic); - diagnostic.verification_readback_ms.push_back(elapsed_ms(readback_start)); - diagnostic.verified_end_to_end_ms.push_back(elapsed_ms(diagnostic.started_at)); - - uint64_t checksum = 14695981039346656037ull; - diagnostic.last_sampled_pixels.clear(); - diagnostic.last_sampled_pixels.reserve(size_t(sampled.width())); - for (int x = 0; x < sampled.width(); ++x) { - const auto pixel = sampled.pixel(x, 0); - const auto rgba = glm::u8vec4(qRed(pixel), qGreen(pixel), qBlue(pixel), qAlpha(pixel)); - diagnostic.last_sampled_pixels.push_back(rgba); - for (const auto channel : { rgba.x, rgba.y, rgba.z, rgba.w }) { - checksum ^= channel; - checksum *= 1099511628211ull; - } - } - diagnostic.sample_checksums.push_back(QStringLiteral("0x%1").arg(checksum, 16, 16, QLatin1Char('0'))); - - ++diagnostic.iteration; - if (diagnostic.iteration < diagnostic.iteration_count) { - begin_fence_sample(diagnostic); - return false; - } - diagnostic.status = QStringLiteral("valid"); - return true; - } - - void append_fence_report(PendingGpuReport& report, const PendingFenceDiagnostic& diagnostic) - { - QJsonArray poll_counts; - for (const auto count : diagnostic.poll_counts) - poll_counts.append(count); - QJsonArray checksums; - for (const auto& checksum : diagnostic.sample_checksums) - checksums.append(checksum); - - QJsonObject json { - { QStringLiteral("supported"), true }, - { QStringLiteral("status"), diagnostic.status }, - { QStringLiteral("requested_samples"), diagnostic.iteration_count }, - { QStringLiteral("completed_samples"), int(diagnostic.verified_end_to_end_ms.size()) }, - { QStringLiteral("timing_method"), - QStringLiteral("glFenceSync + later-frame nonblocking status polling; dependent sampled-texture CPU readback") }, - { QStringLiteral("wait_error"), int(diagnostic.wait_error) }, - { QStringLiteral("watchdog_ms"), 5000 }, - { QStringLiteral("failure_elapsed_ms"), diagnostic.failure_elapsed_ms }, - { QStringLiteral("poll_counts"), poll_counts }, - { QStringLiteral("sample_checksums_fnv1a64"), checksums }, - { QStringLiteral("last_source_markers_srgb8"), pixels_to_json(diagnostic.last_source_markers) }, - { QStringLiteral("last_sampled_layers_linear_rgba8"), pixels_to_json(diagnostic.last_sampled_pixels) }, - }; - if (!diagnostic.verified_end_to_end_ms.empty()) { - json.insert(QStringLiteral("submission"), to_json(statistics(diagnostic.submission_ms))); - json.insert(QStringLiteral("fence_completion"), to_json(statistics(diagnostic.fence_completion_ms))); - json.insert(QStringLiteral("verification_readback"), to_json(statistics(diagnostic.verification_readback_ms))); - json.insert(QStringLiteral("verified_end_to_end"), to_json(statistics(diagnostic.verified_end_to_end_ms))); - json.insert(QStringLiteral("raw_samples_ms"), - QJsonObject { - { QStringLiteral("submission"), samples_to_json(diagnostic.submission_ms) }, - { QStringLiteral("fence_completion"), samples_to_json(diagnostic.fence_completion_ms) }, - { QStringLiteral("verification_readback"), samples_to_json(diagnostic.verification_readback_ms) }, - { QStringLiteral("verified_end_to_end"), samples_to_json(diagnostic.verified_end_to_end_ms) }, - }); - report.summary.push_back(QString()); - report.summary.push_back(QStringLiteral("Fence + dependent readback verification")); - report.summary.push_back(QStringLiteral("Fence completion median %1 ms") - .arg(statistics(diagnostic.fence_completion_ms).median, 8, 'f', 3)); - report.summary.push_back(QStringLiteral("Verified end-to-end median %1 ms") - .arg(statistics(diagnostic.verified_end_to_end_ms).median, 8, 'f', 3)); - } - report.root.insert(QStringLiteral("gpu_fence_verification"), json); - } - - std::pair finish_report() - { - const auto json = QString::fromUtf8(QJsonDocument(m_pending_gpu_report->root).toJson(QJsonDocument::Indented)); - const auto text = m_pending_gpu_report->summary.join('\n'); - qInfo().noquote() << json; - m_pending_gpu_report.reset(); - return { text, json }; - } - - std::optional> poll_gpu_report() - { - Q_ASSERT(m_pending_gpu_report); - if (m_pending_gpu_report->fence_diagnostic) { - if (!poll_fence_diagnostic(*m_pending_gpu_report->fence_diagnostic)) - return std::nullopt; - append_fence_report(*m_pending_gpu_report, *m_pending_gpu_report->fence_diagnostic); - m_pending_gpu_report->fence_diagnostic.reset(); - } - if (!m_gpu_timer->is_supported()) - return finish_report(); - - bool all_finished = true; - for (size_t i = 0; i < m_pending_gpu_report->tickets.size(); ++i) { - if (m_pending_gpu_report->query_finished[i]) - continue; - gl_engine::TextureCompressor::GpuTimings timings; - const auto status = m_gpu_timer->poll(m_pending_gpu_report->tickets[i], timings); - if (status == gl_engine::TextureCompressor::GpuTimer::PollStatus::Pending) { - all_finished = false; - continue; - } - m_pending_gpu_report->query_finished[i] = true; - if (status == gl_engine::TextureCompressor::GpuTimer::PollStatus::Ready) - m_pending_gpu_report->query_results[i] = timings; - else - ++m_pending_gpu_report->disjoint_samples; - } - if (!all_finished) - return std::nullopt; - - std::vector scratch_upload; - std::vector mipmap_generation; - std::vector compression_pass; - std::vector packing_pass; - std::vector output_transfer; - std::vector compressed_upload; - std::vector total; - for (const auto& result : m_pending_gpu_report->query_results) { - if (!result) - continue; - scratch_upload.push_back(result->scratch_upload_ms); - mipmap_generation.push_back(result->mipmap_generation_ms); - compression_pass.push_back(result->compression_pass_ms); - packing_pass.push_back(result->packing_pass_ms); - output_transfer.push_back(result->output_transfer_ms); - compressed_upload.push_back(result->compressed_upload_ms); - total.push_back(result->total_ms()); - } - - QJsonObject gpu_timer_json { - { QStringLiteral("supported"), true }, - { QStringLiteral("timing_method"), QStringLiteral("EXT_disjoint_timer_query; asynchronous GPU elapsed time") }, - { QStringLiteral("requested_samples"), int(m_pending_gpu_report->tickets.size()) }, - { QStringLiteral("valid_samples"), int(total.size()) }, - { QStringLiteral("disjoint_samples"), m_pending_gpu_report->disjoint_samples }, - }; - if (!total.empty()) { - gpu_timer_json.insert(QStringLiteral("status"), - m_pending_gpu_report->disjoint_samples ? QStringLiteral("partial") : QStringLiteral("valid")); - gpu_timer_json.insert(QStringLiteral("scratch_upload"), to_json(statistics(scratch_upload))); - gpu_timer_json.insert(QStringLiteral("mipmap_generation"), to_json(statistics(mipmap_generation))); - gpu_timer_json.insert(QStringLiteral("compression_pass"), to_json(statistics(compression_pass))); - gpu_timer_json.insert(QStringLiteral("packing_pass"), to_json(statistics(packing_pass))); - gpu_timer_json.insert(QStringLiteral("output_transfer"), to_json(statistics(output_transfer))); - gpu_timer_json.insert(QStringLiteral("compressed_upload"), to_json(statistics(compressed_upload))); - gpu_timer_json.insert(QStringLiteral("total_profiled_stages"), to_json(statistics(total))); - gpu_timer_json.insert(QStringLiteral("raw_samples_ms"), - QJsonObject { - { QStringLiteral("scratch_upload"), samples_to_json(scratch_upload) }, - { QStringLiteral("mipmap_generation"), samples_to_json(mipmap_generation) }, - { QStringLiteral("compression_pass"), samples_to_json(compression_pass) }, - { QStringLiteral("packing_pass"), samples_to_json(packing_pass) }, - { QStringLiteral("output_transfer"), samples_to_json(output_transfer) }, - { QStringLiteral("compressed_upload"), samples_to_json(compressed_upload) }, - { QStringLiteral("total_profiled_stages"), samples_to_json(total) }, - }); - m_pending_gpu_report->summary.push_back(QString()); - m_pending_gpu_report->summary.push_back(QStringLiteral("Actual GPU time (timer query)")); - m_pending_gpu_report->summary.push_back( - QStringLiteral("Mipmap generation median %1 ms").arg(statistics(mipmap_generation).median, 8, 'f', 3)); - m_pending_gpu_report->summary.push_back( - QStringLiteral("Compression pass median %1 ms").arg(statistics(compression_pass).median, 8, 'f', 3)); - m_pending_gpu_report->summary.push_back( - QStringLiteral("Packing pass median %1 ms").arg(statistics(packing_pass).median, 8, 'f', 3)); - m_pending_gpu_report->summary.push_back( - QStringLiteral("Profiled GPU stages total median %1 ms").arg(statistics(total).median, 8, 'f', 3)); - } else { - gpu_timer_json.insert(QStringLiteral("status"), QStringLiteral("disjoint")); - } - m_pending_gpu_report->root.insert(QStringLiteral("gpu_timer_query"), gpu_timer_json); - return finish_report(); - } - - std::pair run() - { - constexpr unsigned resolution = 512; - constexpr int batch_size = 4; - constexpr int batches_per_round = 4; - constexpr int measurement_rounds = 3; - constexpr int sample_count = batches_per_round * measurement_rounds; - if (m_source_images.size() < tile_groups.size()) { - const QJsonObject error { - { QStringLiteral("supported"), false }, - { QStringLiteral("error"), QStringLiteral("Benchmark imagery is incomplete") }, - }; - return { QStringLiteral("Unable to load the complete benchmark dataset."), - QString::fromUtf8(QJsonDocument(error).toJson(QJsonDocument::Indented)) }; - } - std::vector all_sources; - all_sources.reserve(tile_groups.size()); - for (const auto& image : m_source_images) - all_sources.push_back(nucleus::tile::conversion::to_rgba8raster(image)); - std::vector layers(size_t(batch_size), 0u); - std::iota(layers.begin(), layers.end(), 0u); - std::vector quality_layers(tile_groups.size(), 0u); - std::iota(quality_layers.begin(), quality_layers.end(), 0u); - const auto sources_for_group = [&](int group) { - return std::span(all_sources).subspan(size_t(group * batch_size), size_t(batch_size)); - }; - - if (!gl_engine::TextureCompressor::is_supported()) { - QJsonObject root { - { QStringLiteral("renderer"), gl_string(GL_RENDERER) }, - { QStringLiteral("vendor"), gl_string(GL_VENDOR) }, - { QStringLiteral("version"), gl_string(GL_VERSION) }, - { QStringLiteral("supported"), false }, - { QStringLiteral("error"), QStringLiteral("No supported WebGL compressed texture format") }, - }; - const auto json = QString::fromUtf8(QJsonDocument(root).toJson(QJsonDocument::Indented)); - qInfo().noquote() << json; - return { QStringLiteral("GPU compression is unavailable: this WebGL device exposes neither ETC nor sRGB S3TC."), json }; - } - - const auto algorithm = gl_engine::Texture::compression_algorithm(); - const auto backend_name = gpu_backend_name(m_gpu_encoder); - const auto filter = m_mipmaps ? gl_engine::Texture::Filter::MipMapLinear : gl_engine::Texture::Filter::Linear; - const gl_engine::TextureCompressor::Settings gpu_settings { - .algorithm = algorithm, - .effort = unsigned(m_effort), - .encoder = compressor_encoder(m_gpu_encoder), - .generate_mipmaps = m_mipmaps, - .timing_mode = gl_engine::TextureCompressor::TimingMode::EndToEnd, - }; - const auto compress_cpu = [&](std::span sources) { - return cpu_compress(sources, algorithm, m_mipmaps, m_cpu_encoder, m_basis_quality, m_basis_effort); - }; - const auto compression_error = [](const QString& error) { - const QJsonObject root { - { QStringLiteral("supported"), false }, - { QStringLiteral("error"), error }, - }; - return std::pair { QStringLiteral("CPU compression failed: %1").arg(error), - QString::fromUtf8(QJsonDocument(root).toJson(QJsonDocument::Indented)) }; - }; - auto upload_cpu = [&](gl_engine::Texture& destination, const std::vector& compressed) { - const auto start = Clock::now(); - for (size_t layer = 0; layer < compressed.size(); ++layer) - destination.upload(compressed[layer], unsigned(layer)); - QOpenGLContext::currentContext()->extraFunctions()->glFinish(); - return elapsed_ms(start); - }; - - // Quality is evaluated first over all 16 images. These destinations remain resident and - // are sampled directly by the preview renderer. - double cpu_psnr = 0.0; - double gpu_psnr = 0.0; - { - m_preview_results.clear(); - m_preview_results.reserve(first_gpu_preview_index + gpu_encoders.size()); - - m_preview_textures[uncompressed_preview_index] = std::make_unique( - gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::SRGBA8); - m_preview_textures[uncompressed_preview_index]->setParams( - gl_engine::Texture::Filter::Linear, gl_engine::Texture::Filter::Linear); - m_preview_textures[uncompressed_preview_index]->allocate_array( - resolution, resolution, unsigned(tile_groups.size())); - for (size_t layer = 0; layer < all_sources.size(); ++layer) - m_preview_textures[uncompressed_preview_index]->upload(all_sources[layer], unsigned(layer)); - m_preview_results.push_back( - { QStringLiteral("Uncompressed reference"), std::numeric_limits::infinity(), -1.0 }); - - m_preview_textures[cpu_preview_index] = std::make_unique( - gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); - m_preview_textures[cpu_preview_index]->setParams(filter, gl_engine::Texture::Filter::Linear); - m_preview_textures[cpu_preview_index]->allocate_array(resolution, resolution, unsigned(tile_groups.size())); - auto cpu_quality = compress_cpu(all_sources); - if (!cpu_quality) - return compression_error(cpu_quality.error()); - static_cast(upload_cpu(*m_preview_textures[cpu_preview_index], cpu_quality->textures)); - - std::vector cpu_reconstructed; - cpu_reconstructed.reserve(all_sources.size()); - for (unsigned layer = 0; layer < all_sources.size(); ++layer) - cpu_reconstructed.push_back(reconstruct(*m_preview_textures[cpu_preview_index], resolution, layer)); - cpu_psnr = linear_psnr(cpu_reconstructed, all_sources); - m_preview_results.push_back({ QStringLiteral("CPU %1").arg(cpu_encoder_name(m_cpu_encoder)), cpu_psnr, 0.0 }); - - for (size_t encoder_index = 0; encoder_index < gpu_encoders.size(); ++encoder_index) { - const auto encoder = gpu_encoders[encoder_index]; - auto& preview_texture = m_preview_textures[encoder_index + first_gpu_preview_index]; - preview_texture = std::make_unique( - gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); - preview_texture->setParams(filter, gl_engine::Texture::Filter::Linear); - preview_texture->allocate_array(resolution, resolution, unsigned(tile_groups.size())); - gl_engine::TextureCompressor preview_compressor(resolution, resolution, unsigned(tile_groups.size())); - auto preview_settings = gpu_settings; - preview_settings.encoder = compressor_encoder(encoder); - static_cast(preview_compressor.compress(all_sources, *preview_texture, quality_layers, preview_settings)); - - std::vector reconstructed; - reconstructed.reserve(all_sources.size()); - for (unsigned layer = 0; layer < all_sources.size(); ++layer) - reconstructed.push_back(reconstruct(*preview_texture, resolution, layer)); - const auto psnr = linear_psnr(reconstructed, all_sources); - m_preview_results.push_back({ gpu_encoder_name(encoder, m_effort), psnr, 0.0 }); - if (encoder == m_gpu_encoder) - gpu_psnr = psnr; - } - } - - gl_engine::Texture cpu_destination(gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); - cpu_destination.setParams(filter, gl_engine::Texture::Filter::Linear); - cpu_destination.allocate_array(resolution, resolution, batch_size); - std::unique_ptr gpu_destination; - std::unique_ptr gpu_compressor; - m_gpu_timer = std::make_unique(); - - std::vector cpu_compression_times; - std::vector cpu_source_preparation_times; - std::vector cpu_encoding_times; - std::vector cpu_transcoding_times; - std::vector cpu_upload_times; - std::vector cpu_total_times; - std::vector cpu_intermediate_bytes; - std::vector cpu_transcoded_bytes; - WallTimingSamples gpu_upload_times; - WallTimingSamples gpu_mipmap_times; - WallTimingSamples gpu_compression_pass_times; - WallTimingSamples gpu_packing_pass_times; - WallTimingSamples gpu_encoding_times; - WallTimingSamples gpu_output_transfer_times; - WallTimingSamples gpu_compressed_upload_times; - std::vector gpu_total_times; - std::vector gpu_timing_tickets; - cpu_compression_times.reserve(sample_count); - cpu_source_preparation_times.reserve(sample_count); - cpu_encoding_times.reserve(sample_count); - cpu_transcoding_times.reserve(sample_count); - cpu_upload_times.reserve(sample_count); - cpu_total_times.reserve(sample_count); - cpu_intermediate_bytes.reserve(sample_count); - cpu_transcoded_bytes.reserve(sample_count); - gpu_total_times.reserve(sample_count); - - // Keep CPU and GPU phases separate: mobile CPU frequency and thermal state are shared - // with the GPU, so interleaving them makes the CPU result workload-dependent. - // Each backend warms up once on all four distinct batches. - for (int group = 0; group < batches_per_round; ++group) { - auto compressed = compress_cpu(sources_for_group(group)); - if (!compressed) - return compression_error(compressed.error()); - static_cast(upload_cpu(cpu_destination, compressed->textures)); - } - for (int round = 0; round < measurement_rounds; ++round) { - for (int group = 0; group < batches_per_round; ++group) { - const auto cpu_start = Clock::now(); - auto compressed = compress_cpu(sources_for_group(group)); - if (!compressed) - return compression_error(compressed.error()); - const auto cpu_compression_time = elapsed_ms(cpu_start); - const auto cpu_upload_time = upload_cpu(cpu_destination, compressed->textures); - cpu_compression_times.push_back(cpu_compression_time); - cpu_source_preparation_times.push_back(compressed->source_preparation_ms); - cpu_encoding_times.push_back(compressed->encoding_ms); - cpu_transcoding_times.push_back(compressed->transcoding_ms); - cpu_upload_times.push_back(cpu_upload_time); - cpu_total_times.push_back(elapsed_ms(cpu_start)); - cpu_intermediate_bytes.push_back(double(compressed->intermediate_bytes)); - cpu_transcoded_bytes.push_back(double(compressed->transcoded_bytes)); - } - } - - for (size_t encoder_index = 0; encoder_index < gpu_encoders.size(); ++encoder_index) { - const auto encoder = gpu_encoders[encoder_index]; - auto destination = std::make_unique( - gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); - destination->setParams(filter, gl_engine::Texture::Filter::Linear); - destination->allocate_array(resolution, resolution, batch_size); - auto compressor = std::make_unique(resolution, resolution, batch_size); - auto settings = gpu_settings; - settings.encoder = compressor_encoder(encoder); - - for (int group = 0; group < batches_per_round; ++group) - static_cast(compressor->compress(sources_for_group(group), *destination, layers, settings)); - - std::vector encoder_times; - encoder_times.reserve(sample_count); - for (int round = 0; round < measurement_rounds; ++round) { - for (int group = 0; group < batches_per_round; ++group) { - const auto gpu = compressor->compress(sources_for_group(group), *destination, layers, settings); - encoder_times.push_back(gpu.timings.total_ms); - } - } - m_preview_results[encoder_index + first_gpu_preview_index].compression_time_ms = statistics(encoder_times).median; - if (encoder == m_gpu_encoder) { - gpu_total_times = std::move(encoder_times); - gpu_destination = std::move(destination); - gpu_compressor = std::move(compressor); - } - } - Q_ASSERT(gpu_destination && gpu_compressor); - - // Stage timings are collected in a separate profiling phase. Each stage is completed - // independently, so these values diagnose where time is spent but are not summed to - // produce the end-to-end result above. - auto stage_settings = gpu_settings; - stage_settings.timing_mode = gl_engine::TextureCompressor::TimingMode::IndividualStages; - stage_settings.gpu_timer = m_gpu_timer.get(); - for (int round = 0; round < measurement_rounds; ++round) { - for (int group = 0; group < batches_per_round; ++group) { - const auto gpu = gpu_compressor->compress(sources_for_group(group), *gpu_destination, layers, stage_settings); - gpu_upload_times.append(gpu.timings.scratch_upload); - gpu_mipmap_times.append(gpu.timings.mipmap_generation); - gpu_compression_pass_times.append(gpu.timings.compression_pass); - gpu_packing_pass_times.append(gpu.timings.packing_pass); - gpu_encoding_times.append(gpu.timings.encoding); - gpu_output_transfer_times.append(gpu.timings.output_transfer); - gpu_compressed_upload_times.append(gpu.timings.compressed_upload); - if (gpu.gpu_timing_ticket) - gpu_timing_tickets.push_back(gpu.gpu_timing_ticket); - } - } - - const auto cpu_compression = statistics(cpu_compression_times); - const auto cpu_source_preparation = statistics(cpu_source_preparation_times); - const auto cpu_encoding = statistics(cpu_encoding_times); - const auto cpu_transcoding = statistics(cpu_transcoding_times); - const auto cpu_upload = statistics(cpu_upload_times); - const auto cpu_total = statistics(cpu_total_times); - const auto cpu_intermediate_size = statistics(cpu_intermediate_bytes); - const auto cpu_transcoded_size = statistics(cpu_transcoded_bytes); - const auto gpu_upload = statistics(gpu_upload_times.total); - const auto gpu_mipmap = statistics(gpu_mipmap_times.total); - const auto gpu_compression_pass = statistics(gpu_compression_pass_times.total); - const auto gpu_packing_pass = statistics(gpu_packing_pass_times.total); - const auto gpu_encoding = statistics(gpu_encoding_times.total); - const auto gpu_output_transfer = statistics(gpu_output_transfer_times.total); - const auto gpu_compressed_upload = statistics(gpu_compressed_upload_times.total); - const auto gpu_total = statistics(gpu_total_times); - m_preview_results[cpu_preview_index].compression_time_ms = cpu_total.median; - const auto algorithm_name = algorithm == nucleus::utils::ColourTexture::Format::DXT1 ? QStringLiteral("DXT1 / BC1") : QStringLiteral("ETC1 in ETC2"); - const auto cpu_backend_name = cpu_encoder_name(m_cpu_encoder); - - QJsonObject root { - { QStringLiteral("renderer"), gl_string(GL_RENDERER) }, - { QStringLiteral("vendor"), gl_string(GL_VENDOR) }, - { QStringLiteral("version"), gl_string(GL_VERSION) }, - { QStringLiteral("supported"), true }, - { QStringLiteral("algorithm"), algorithm_name }, - { QStringLiteral("cpu_backend"), cpu_backend_name }, - { QStringLiteral("gpu_backend"), backend_name }, - { QStringLiteral("timing_method"), QStringLiteral("wall time; one final glFinish per end-to-end sample") }, - { QStringLiteral("resolution"), int(resolution) }, - { QStringLiteral("batch_size"), batch_size }, - { QStringLiteral("measurement_rounds"), measurement_rounds }, - { QStringLiteral("batches_per_round"), batches_per_round }, - { QStringLiteral("measurement_samples"), sample_count }, - { QStringLiteral("warmup_rounds"), 1 }, - { QStringLiteral("gpu_stage_profile_samples"), sample_count }, - { QStringLiteral("basis_quality"), m_basis_quality }, - { QStringLiteral("basis_effort"), m_basis_effort }, - { QStringLiteral("gpu_encoder"), backend_name }, - { QStringLiteral("gpu_effort"), m_effort }, - { QStringLiteral("mipmaps"), m_mipmaps }, - { QStringLiteral("dataset"), dataset_json() }, - { QStringLiteral("cpu_psnr_db_all_16_images"), cpu_psnr }, - { QStringLiteral("gpu_psnr_db_all_16_images"), gpu_psnr }, - { QStringLiteral("cpu_compression"), to_json(cpu_compression) }, - { QStringLiteral("cpu_source_preparation"), to_json(cpu_source_preparation) }, - { QStringLiteral("cpu_encoding"), to_json(cpu_encoding) }, - { QStringLiteral("cpu_transcoding"), to_json(cpu_transcoding) }, - { QStringLiteral("cpu_intermediate_size"), size_to_json(cpu_intermediate_size) }, - { QStringLiteral("cpu_transcoded_size"), size_to_json(cpu_transcoded_size) }, - { QStringLiteral("cpu_compressed_upload"), to_json(cpu_upload) }, - { QStringLiteral("cpu_end_to_end"), to_json(cpu_total) }, - { QStringLiteral("gpu_scratch_upload"), to_json(gpu_upload) }, - { QStringLiteral("gpu_mipmap_generation"), to_json(gpu_mipmap) }, - { QStringLiteral("gpu_compression_pass"), to_json(gpu_compression_pass) }, - { QStringLiteral("gpu_packing_pass"), to_json(gpu_packing_pass) }, - { QStringLiteral("gpu_encoding"), to_json(gpu_encoding) }, - { QStringLiteral("gpu_output_transfer"), to_json(gpu_output_transfer) }, - { QStringLiteral("gpu_compressed_upload"), to_json(gpu_compressed_upload) }, - { QStringLiteral("gpu_end_to_end"), to_json(gpu_total) }, - { QStringLiteral("cpu_tiles_per_second"), 1000.0 * batch_size / cpu_total.median }, - { QStringLiteral("gpu_tiles_per_second"), 1000.0 * batch_size / gpu_total.median }, - { QStringLiteral("phase_order"), - QStringLiteral("16-image PSNR and preview, one CPU and GPU warmup round over four batches, three CPU and GPU measurement rounds over four batches, GPU stage profiling, asynchronous fence verification") }, - { QStringLiteral("gpu_stage_timing_method"), QStringLiteral("separate profiling pass; each stage glFinish-synchronised") }, - { QStringLiteral("gpu_stage_wall_profile"), - QJsonObject { - { QStringLiteral("scratch_upload"), gpu_upload_times.statistics_json() }, - { QStringLiteral("mipmap_generation"), gpu_mipmap_times.statistics_json() }, - { QStringLiteral("compression_pass"), gpu_compression_pass_times.statistics_json() }, - { QStringLiteral("packing_pass"), gpu_packing_pass_times.statistics_json() }, - { QStringLiteral("encoding_total"), gpu_encoding_times.statistics_json() }, - { QStringLiteral("output_transfer"), gpu_output_transfer_times.statistics_json() }, - { QStringLiteral("compressed_upload"), gpu_compressed_upload_times.statistics_json() }, - { QStringLiteral("raw_samples_ms"), - QJsonObject { - { QStringLiteral("scratch_upload"), gpu_upload_times.raw_json() }, - { QStringLiteral("mipmap_generation"), gpu_mipmap_times.raw_json() }, - { QStringLiteral("compression_pass"), gpu_compression_pass_times.raw_json() }, - { QStringLiteral("packing_pass"), gpu_packing_pass_times.raw_json() }, - { QStringLiteral("encoding_total"), gpu_encoding_times.raw_json() }, - { QStringLiteral("output_transfer"), gpu_output_transfer_times.raw_json() }, - { QStringLiteral("compressed_upload"), gpu_compressed_upload_times.raw_json() }, - } }, - } }, - { QStringLiteral("raw_samples_ms"), - QJsonObject { - { QStringLiteral("cpu_compression"), samples_to_json(cpu_compression_times) }, - { QStringLiteral("cpu_source_preparation"), samples_to_json(cpu_source_preparation_times) }, - { QStringLiteral("cpu_encoding"), samples_to_json(cpu_encoding_times) }, - { QStringLiteral("cpu_transcoding"), samples_to_json(cpu_transcoding_times) }, - { QStringLiteral("cpu_intermediate_bytes"), samples_to_json(cpu_intermediate_bytes) }, - { QStringLiteral("cpu_transcoded_bytes"), samples_to_json(cpu_transcoded_bytes) }, - { QStringLiteral("cpu_compressed_upload"), samples_to_json(cpu_upload_times) }, - { QStringLiteral("cpu_end_to_end"), samples_to_json(cpu_total_times) }, - { QStringLiteral("gpu_end_to_end"), samples_to_json(gpu_total_times) }, - { QStringLiteral("gpu_scratch_upload_stage_profile"), samples_to_json(gpu_upload_times.total) }, - { QStringLiteral("gpu_mipmap_generation_stage_profile"), samples_to_json(gpu_mipmap_times.total) }, - { QStringLiteral("gpu_compression_pass_stage_profile"), samples_to_json(gpu_compression_pass_times.total) }, - { QStringLiteral("gpu_packing_pass_stage_profile"), samples_to_json(gpu_packing_pass_times.total) }, - { QStringLiteral("gpu_encoding_stage_profile"), samples_to_json(gpu_encoding_times.total) }, - { QStringLiteral("gpu_output_transfer_stage_profile"), samples_to_json(gpu_output_transfer_times.total) }, - { QStringLiteral("gpu_compressed_upload_stage_profile"), samples_to_json(gpu_compressed_upload_times.total) }, - } }, - }; - const auto line = [](QString label, Statistics value) { - return QStringLiteral("%1 median %2 ms p95 %3 ms").arg(label, -26).arg(value.median, 8, 'f', 3).arg(value.p95, 8, 'f', 3); - }; - const auto size_line = [](QString label, Statistics value) { - return QStringLiteral("%1 median %2 KiB p95 %3 KiB") - .arg(label, -26) - .arg(value.median / 1024.0, 8, 'f', 1) - .arg(value.p95 / 1024.0, 8, 'f', 1); - }; - const auto cpu_settings = m_cpu_encoder == BenchmarkItem::CpuEncoder::Goofy - ? QStringLiteral("CPU: %1").arg(cpu_backend_name) - : QStringLiteral("CPU: %1, quality %2, effort %3").arg(cpu_backend_name).arg(m_basis_quality).arg(m_basis_effort); - QStringList summary { - QStringLiteral("%1 — %2 × %3, batch %4, 12 samples, GPU effort %5, mipmaps %6") - .arg(algorithm_name) - .arg(resolution) - .arg(resolution) - .arg(batch_size) - .arg(m_effort) - .arg(m_mipmaps ? QStringLiteral("on") : QStringLiteral("off")), - cpu_settings, - backend_name, - gl_string(GL_RENDERER), - QStringLiteral("Quality first: one untimed 16-image pass"), - QStringLiteral("CPU PSNR (all 16 images) %1 dB").arg(cpu_psnr, 0, 'f', 2), - QStringLiteral("GPU PSNR (all 16 images) %1 dB").arg(gpu_psnr, 0, 'f', 2), - QStringLiteral("Timing: one warmup round, then three rounds × four distinct batches"), - QStringLiteral("Timing: one final glFinish per end-to-end sample"), - QString(), - line(QStringLiteral("CPU compression"), cpu_compression), - line(QStringLiteral(" source preparation"), cpu_source_preparation), - line(QStringLiteral(" CPU encoding"), cpu_encoding), - line(QStringLiteral(" CPU transcoding"), cpu_transcoding), - size_line(QStringLiteral(" Intermediate stream"), cpu_intermediate_size), - size_line(QStringLiteral(" GPU blocks"), cpu_transcoded_size), - line(QStringLiteral("CPU compressed upload"), cpu_upload), - line(QStringLiteral("CPU end-to-end"), cpu_total), - QStringLiteral("GPU stages (separate serialised profiling pass)"), - line(QStringLiteral("GPU scratch upload"), gpu_upload), - line(QStringLiteral("GPU mip generation"), gpu_mipmap), - line(QStringLiteral("GPU compression pass"), gpu_compression_pass), - line(QStringLiteral("GPU packing pass"), gpu_packing_pass), - line(QStringLiteral("GPU encoding"), gpu_encoding), - line(QStringLiteral("GPU output transfer"), gpu_output_transfer), - line(QStringLiteral("GPU compressed upload"), gpu_compressed_upload), - line(QStringLiteral("GPU end-to-end"), gpu_total), - QString(), - QStringLiteral("CPU completed throughput %1 tiles/s").arg(1000.0 * batch_size / cpu_total.median, 0, 'f', 1), - QStringLiteral("GPU completed throughput %1 tiles/s").arg(1000.0 * batch_size / gpu_total.median, 0, 'f', 1), - }; - if (!m_gpu_timer->is_supported()) { - root.insert(QStringLiteral("gpu_timer_query"), - QJsonObject { - { QStringLiteral("supported"), false }, - { QStringLiteral("status"), QStringLiteral("unsupported") }, - }); - } - - m_pending_gpu_report = PendingGpuReport { - .root = std::move(root), - .summary = std::move(summary), - .tickets = std::move(gpu_timing_tickets), - .query_results = std::vector>(sample_count), - .query_finished = std::vector(sample_count, false), - }; - if (m_gpu_timer->is_supported()) - Q_ASSERT(m_pending_gpu_report->tickets.size() == sample_count); - - PendingFenceDiagnostic fence_diagnostic; - fence_diagnostic.compressor = std::move(gpu_compressor); - fence_diagnostic.destination = std::move(gpu_destination); - fence_diagnostic.probe_framebuffer = std::make_unique(gl_engine::Framebuffer::DepthFormat::None, - std::vector { gl_engine::Framebuffer::ColourFormat::RGBA8 }, - glm::uvec2(batch_size, 1u)); - fence_diagnostic.probe_shader = std::make_unique(R"( - void main() { - highp vec2 vertices[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); - gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0); - })", - R"( - uniform lowp sampler2DArray texture_sampler; - out lowp vec4 out_color; - void main() { - highp int layer = int(gl_FragCoord.x); - out_color = textureLod(texture_sampler, vec3(0.5, 0.5, float(layer)), 0.0); - })", - gl_engine::ShaderCodeSource::PLAINTEXT); - fence_diagnostic.probe_geometry = gl_engine::helpers::create_screen_quad_geometry(); - fence_diagnostic.source_pool = std::move(all_sources); - fence_diagnostic.layers = std::move(layers); - fence_diagnostic.settings = gpu_settings; - fence_diagnostic.settings.timing_mode = gl_engine::TextureCompressor::TimingMode::SubmissionOnly; - fence_diagnostic.iteration_count = sample_count; - m_pending_gpu_report->fence_diagnostic.emplace(std::move(fence_diagnostic)); - begin_fence_sample(*m_pending_gpu_report->fence_diagnostic); - return {}; - } - - QPointer m_item; - QQuickWindow* m_window = nullptr; - unsigned m_seen_serial = 0; - BenchmarkItem::CpuEncoder m_cpu_encoder = BenchmarkItem::CpuEncoder::Goofy; - int m_basis_quality = 75; - int m_basis_effort = 4; - BenchmarkItem::GpuEncoder m_gpu_encoder = BenchmarkItem::GpuEncoder::FastSplitFused; - int m_effort = 4; - bool m_mipmaps = true; - int m_preview_encoder = 0; - bool m_pending = false; - std::vector m_source_images; - std::vector m_preview_results; - std::array, first_gpu_preview_index + gpu_encoders.size()> m_preview_textures; - std::unique_ptr m_preview_shader; - gl_engine::helpers::ScreenQuadGeometry m_preview_geometry; - std::unique_ptr m_gpu_timer; - std::optional m_pending_gpu_report; -}; - -BenchmarkItem::BenchmarkItem(QQuickItem* parent) - : QQuickFramebufferObject(parent) - , m_network_manager(new QNetworkAccessManager(this)) -{ - setMirrorVertically(true); - downloadBenchmarkData(); -} - -QQuickFramebufferObject::Renderer* BenchmarkItem::createRenderer() const { return new BenchmarkRenderer; } - -BenchmarkItem::CpuEncoder BenchmarkItem::cpuEncoder() const { return m_cpu_encoder; } -void BenchmarkItem::setCpuEncoder(CpuEncoder value) -{ - if (m_cpu_encoder == value) - return; - m_cpu_encoder = value; - emit cpuEncoderChanged(); -} - -int BenchmarkItem::basisQuality() const { return m_basis_quality; } -void BenchmarkItem::setBasisQuality(int value) -{ - value = std::clamp(value, 1, 100); - if (m_basis_quality == value) - return; - m_basis_quality = value; - emit basisQualityChanged(); -} - -int BenchmarkItem::basisEffort() const { return m_basis_effort; } -void BenchmarkItem::setBasisEffort(int value) -{ - value = std::clamp(value, 0, 10); - if (m_basis_effort == value) - return; - m_basis_effort = value; - emit basisEffortChanged(); -} - -BenchmarkItem::GpuEncoder BenchmarkItem::gpuEncoder() const { return m_gpu_encoder; } -void BenchmarkItem::setGpuEncoder(GpuEncoder value) -{ - if (m_gpu_encoder == value) - return; - m_gpu_encoder = value; - emit gpuEncoderChanged(); -} - -int BenchmarkItem::effort() const { return m_effort; } -void BenchmarkItem::setEffort(int value) -{ - value = std::clamp(value, 0, 10); - if (m_effort == value) - return; - m_effort = value; - emit effortChanged(); -} - -bool BenchmarkItem::mipmaps() const { return m_mipmaps; } -void BenchmarkItem::setMipmaps(bool value) -{ - if (m_mipmaps == value) - return; - m_mipmaps = value; - emit mipmapsChanged(); -} - -bool BenchmarkItem::dataReady() const { return m_data_ready; } -QString BenchmarkItem::dataStatus() const { return m_data_status; } -bool BenchmarkItem::running() const { return m_running; } -QString BenchmarkItem::resultText() const { return m_result_text; } -QString BenchmarkItem::resultJson() const { return m_result_json; } -int BenchmarkItem::previewEncoder() const { return m_preview_encoder; } -void BenchmarkItem::setPreviewEncoder(int value) -{ - if (!m_preview_results.empty()) - value = std::clamp(value, 0, int(m_preview_results.size()) - 1); - else - value = 0; - if (m_preview_encoder == value) - return; - m_preview_encoder = value; - emit previewEncoderChanged(); - emit previewDetailsChanged(); - update(); -} - -bool BenchmarkItem::previewReady() const { return !m_preview_results.empty(); } -QStringList BenchmarkItem::previewEncoders() const -{ - QStringList result; - result.reserve(qsizetype(m_preview_results.size())); - for (const auto& preview : m_preview_results) - result.push_back(preview.name); - return result; -} - -QString BenchmarkItem::previewName() const -{ - return previewReady() ? m_preview_results[size_t(m_preview_encoder)].name : QString {}; -} - -double BenchmarkItem::previewPsnr() const -{ - return previewReady() ? m_preview_results[size_t(m_preview_encoder)].psnr : 0.0; -} - -double BenchmarkItem::previewCompressionTime() const -{ - return previewReady() ? m_preview_results[size_t(m_preview_encoder)].compression_time_ms : 0.0; -} - -void BenchmarkItem::downloadBenchmarkData() -{ - m_downloaded_tiles.resize(tile_groups.size() * 4); - m_downloads_remaining = int(m_downloaded_tiles.size()); - for (size_t group_index = 0; group_index < tile_groups.size(); ++group_index) { - for (int y = 0; y < 2; ++y) { - for (int x = 0; x < 2; ++x) { - const auto tile_index = group_index * 4 + size_t(y * 2 + x); - const auto url = tile_url(tile_groups[group_index], x, y); - auto* reply = m_network_manager->get(QNetworkRequest(QUrl(url))); - connect(reply, &QNetworkReply::finished, this, [this, reply, tile_index, url]() { - if (reply->error() == QNetworkReply::NoError) { - const auto image = nucleus::utils::image_loader::rgba8(reply->readAll()); - if (image && image->size() == glm::uvec2(256u)) - m_downloaded_tiles[tile_index] = nucleus::tile::conversion::to_QImage(*image); - } - if (m_downloaded_tiles[tile_index].isNull() && !m_data_status.startsWith(QStringLiteral("Unable"))) { - m_data_status = QStringLiteral("Unable to download benchmark tile: %1").arg(url); - emit dataStatusChanged(); - } - reply->deleteLater(); - --m_downloads_remaining; - if (m_downloads_remaining > 0) { - if (!m_data_status.startsWith(QStringLiteral("Unable"))) { - m_data_status = QStringLiteral("Downloading benchmark imagery… %1/%2") - .arg(int(m_downloaded_tiles.size()) - m_downloads_remaining) - .arg(m_downloaded_tiles.size()); - emit dataStatusChanged(); - } - return; - } - if (std::ranges::any_of(m_downloaded_tiles, [](const QImage& image) { return image.isNull(); })) { - m_result_text = m_data_status; - emit resultTextChanged(); - return; - } - stitchBenchmarkData(); - }); - } - } - } -} - -void BenchmarkItem::stitchBenchmarkData() -{ - m_source_images.clear(); - m_source_images.reserve(tile_groups.size()); - for (size_t group_index = 0; group_index < tile_groups.size(); ++group_index) { - QImage stitched(512, 512, QImage::Format_RGBA8888); - QPainter painter(&stitched); - for (int y = 0; y < 2; ++y) { - for (int x = 0; x < 2; ++x) - painter.drawImage(QPoint(x * 256, y * 256), m_downloaded_tiles[group_index * 4 + size_t(y * 2 + x)]); - } - painter.end(); - m_source_images.push_back(std::move(stitched)); - } - m_downloaded_tiles.clear(); - m_data_ready = true; - m_data_status = QStringLiteral("16 benchmark images ready."); - emit dataReadyChanged(); - emit dataStatusChanged(); - runBenchmark(); -} - -void BenchmarkItem::runBenchmark() -{ - if (m_running || !m_data_ready) - return; - m_running = true; - m_result_text = QStringLiteral("Computing PSNR, then measuring batch size 4…"); - m_result_json.clear(); - m_preview_results.clear(); - m_preview_encoder = 0; - ++m_request_serial; - emit runningChanged(); - emit resultTextChanged(); - emit resultJsonChanged(); - emit previewEncoderChanged(); - emit previewResultsChanged(); - emit previewDetailsChanged(); - update(); -} - -void BenchmarkItem::copyResultJson() -{ - if (!m_result_json.isEmpty()) - QGuiApplication::clipboard()->setText(m_result_json); -} - -void BenchmarkItem::publishResults( - const QString& text, const QString& json, const std::vector& preview_results) -{ - m_result_text = text; - m_result_json = json; - m_preview_results = preview_results; - m_preview_encoder = std::clamp(m_preview_encoder, 0, std::max(0, int(m_preview_results.size()) - 1)); - m_running = false; - emit resultTextChanged(); - emit resultJsonChanged(); - emit previewResultsChanged(); - emit previewDetailsChanged(); - emit runningChanged(); - update(); -} diff --git a/apps/texture_compression_benchmark/BenchmarkItem.h b/apps/texture_compression_benchmark/BenchmarkItem.h deleted file mode 100644 index 224d7097..00000000 --- a/apps/texture_compression_benchmark/BenchmarkItem.h +++ /dev/null @@ -1,122 +0,0 @@ -/***************************************************************************** - * AlpineMaps.org - * Copyright (C) 2026 Adam Celarek - * SPDX-License-Identifier: GPL-3.0-or-later - *****************************************************************************/ - -#pragma once - -#include -#include -#include -#include -#include -#include - -class QNetworkAccessManager; - -class BenchmarkItem : public QQuickFramebufferObject { - Q_OBJECT - QML_ELEMENT - Q_PROPERTY(CpuEncoder cpuEncoder READ cpuEncoder WRITE setCpuEncoder NOTIFY cpuEncoderChanged) - Q_PROPERTY(int basisQuality READ basisQuality WRITE setBasisQuality NOTIFY basisQualityChanged) - Q_PROPERTY(int basisEffort READ basisEffort WRITE setBasisEffort NOTIFY basisEffortChanged) - Q_PROPERTY(GpuEncoder gpuEncoder READ gpuEncoder WRITE setGpuEncoder NOTIFY gpuEncoderChanged) - Q_PROPERTY(int effort READ effort WRITE setEffort NOTIFY effortChanged) - Q_PROPERTY(bool mipmaps READ mipmaps WRITE setMipmaps NOTIFY mipmapsChanged) - Q_PROPERTY(bool dataReady READ dataReady NOTIFY dataReadyChanged) - Q_PROPERTY(QString dataStatus READ dataStatus NOTIFY dataStatusChanged) - Q_PROPERTY(bool running READ running NOTIFY runningChanged) - Q_PROPERTY(QString resultText READ resultText NOTIFY resultTextChanged) - Q_PROPERTY(QString resultJson READ resultJson NOTIFY resultJsonChanged) - Q_PROPERTY(int previewEncoder READ previewEncoder WRITE setPreviewEncoder NOTIFY previewEncoderChanged) - Q_PROPERTY(bool previewReady READ previewReady NOTIFY previewResultsChanged) - Q_PROPERTY(QStringList previewEncoders READ previewEncoders NOTIFY previewResultsChanged) - Q_PROPERTY(QString previewName READ previewName NOTIFY previewDetailsChanged) - Q_PROPERTY(double previewPsnr READ previewPsnr NOTIFY previewDetailsChanged) - Q_PROPERTY(double previewCompressionTime READ previewCompressionTime NOTIFY previewDetailsChanged) - -public: - enum class CpuEncoder { Goofy, BasisEtc1s, BasisUastcLdr4x4, BasisXuastcLdr4x4 }; - Q_ENUM(CpuEncoder) - enum class GpuEncoder { Search, FastRange, FastSplit, FastSplitFused, FastSplitBounds }; - Q_ENUM(GpuEncoder) - - struct PreviewResult { - QString name; - double psnr = 0.0; - double compression_time_ms = 0.0; - }; - - explicit BenchmarkItem(QQuickItem* parent = nullptr); - Renderer* createRenderer() const override; - - [[nodiscard]] CpuEncoder cpuEncoder() const; - void setCpuEncoder(CpuEncoder value); - [[nodiscard]] int basisQuality() const; - void setBasisQuality(int value); - [[nodiscard]] int basisEffort() const; - void setBasisEffort(int value); - [[nodiscard]] GpuEncoder gpuEncoder() const; - void setGpuEncoder(GpuEncoder value); - [[nodiscard]] int effort() const; - void setEffort(int value); - [[nodiscard]] bool mipmaps() const; - void setMipmaps(bool value); - [[nodiscard]] bool dataReady() const; - [[nodiscard]] QString dataStatus() const; - [[nodiscard]] bool running() const; - [[nodiscard]] QString resultText() const; - [[nodiscard]] QString resultJson() const; - [[nodiscard]] int previewEncoder() const; - void setPreviewEncoder(int value); - [[nodiscard]] bool previewReady() const; - [[nodiscard]] QStringList previewEncoders() const; - [[nodiscard]] QString previewName() const; - [[nodiscard]] double previewPsnr() const; - [[nodiscard]] double previewCompressionTime() const; - - Q_INVOKABLE void runBenchmark(); - Q_INVOKABLE void copyResultJson(); - -signals: - void cpuEncoderChanged(); - void basisQualityChanged(); - void basisEffortChanged(); - void gpuEncoderChanged(); - void effortChanged(); - void mipmapsChanged(); - void dataReadyChanged(); - void dataStatusChanged(); - void runningChanged(); - void resultTextChanged(); - void resultJsonChanged(); - void previewEncoderChanged(); - void previewResultsChanged(); - void previewDetailsChanged(); - -private: - friend class BenchmarkRenderer; - void downloadBenchmarkData(); - void stitchBenchmarkData(); - void publishResults(const QString& text, const QString& json, const std::vector& preview_results); - - CpuEncoder m_cpu_encoder = CpuEncoder::Goofy; - int m_basis_quality = 75; - int m_basis_effort = 4; - GpuEncoder m_gpu_encoder = GpuEncoder::FastSplitFused; - int m_effort = 4; - bool m_mipmaps = true; - bool m_data_ready = false; - bool m_running = false; - unsigned m_request_serial = 0; - int m_downloads_remaining = 0; - QNetworkAccessManager* m_network_manager = nullptr; - std::vector m_downloaded_tiles; - std::vector m_source_images; - QString m_data_status = QStringLiteral("Downloading benchmark imagery…"); - QString m_result_text = QStringLiteral("Waiting for benchmark imagery."); - QString m_result_json; - int m_preview_encoder = 0; - std::vector m_preview_results; -}; diff --git a/apps/texture_compression_benchmark/CMakeLists.txt b/apps/texture_compression_benchmark/CMakeLists.txt index 029070ec..314ca93b 100644 --- a/apps/texture_compression_benchmark/CMakeLists.txt +++ b/apps/texture_compression_benchmark/CMakeLists.txt @@ -4,68 +4,39 @@ # SPDX-License-Identifier: GPL-3.0-or-later ############################################################################# -project(texture-compression-benchmark LANGUAGES CXX) +project(texture-compression-preview LANGUAGES CXX) -alp_add_git_repository(basisu - URL https://github.com/BinomialLLC/basis_universal.git - COMMITISH 9bebe16726b3a61c8c213eeee3b7cffb462ef34e - DO_NOT_ADD_SUBPROJECT) -set(BASISU_EXAMPLES OFF CACHE BOOL "" FORCE) -set(BASISU_SSE OFF CACHE BOOL "" FORCE) -set(BASISU_ZSTD OFF CACHE BOOL "" FORCE) -set(BASISU_OPENCL OFF CACHE BOOL "" FORCE) -set(BASISU_DISABLE_ANDROID_ASTC_DECOMP ON CACHE BOOL "" FORCE) -add_subdirectory("${basisu_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/basisu" EXCLUDE_FROM_ALL) -if (EMSCRIPTEN AND ALP_ENABLE_THREADING) - target_compile_options(basisu_encoder PRIVATE -pthread) -endif() - -add_library(alp_basisu_texture_compression STATIC - ${CMAKE_SOURCE_DIR}/nucleus/utils/BasisUniversalTextureCompression.h - ${CMAKE_SOURCE_DIR}/nucleus/utils/BasisUniversalTextureCompression.cpp -) -target_include_directories(alp_basisu_texture_compression SYSTEM PRIVATE "${basisu_SOURCE_DIR}") -target_compile_definitions(alp_basisu_texture_compression PRIVATE - BASISD_SUPPORT_KTX2_ZSTD=0 - BASISU_SUPPORT_SSE=0 -) -target_link_libraries(alp_basisu_texture_compression PUBLIC nucleus PRIVATE basisu_encoder) -if (UNIX AND NOT ANDROID AND NOT EMSCRIPTEN) - target_link_libraries(alp_basisu_texture_compression PRIVATE m pthread) -endif() -alp_configure_target(alp_basisu_texture_compression) - -qt_add_executable(texture_compression_benchmark +qt_add_executable(texture_compression_preview main.cpp - BenchmarkItem.h - BenchmarkItem.cpp + TexturePreviewItem.h + TexturePreviewItem.cpp ) -qt_add_qml_module(texture_compression_benchmark - URI TextureCompressionBenchmark +qt_add_qml_module(texture_compression_preview + URI TextureCompressionPreview VERSION 1.0 RESOURCE_PREFIX /qt/qml QML_FILES Main.qml ) -qt_add_resources(texture_compression_benchmark "fonts" +qt_add_resources(texture_compression_preview "fonts" BASE "${alpineapp_fonts_SOURCE_DIR}" PREFIX "/fonts" FILES "${alpineapp_fonts_SOURCE_DIR}/Roboto/Roboto-Regular.ttf" ) -target_link_libraries(texture_compression_benchmark PUBLIC alp_basisu_texture_compression gl_engine Qt::Network Qt::Quick Qt::QuickControls2) -alp_configure_target(texture_compression_benchmark) +target_link_libraries(texture_compression_preview PUBLIC gl_engine Qt::Network Qt::Quick Qt::QuickControls2) +alp_configure_target(texture_compression_preview) if (ANDROID) - add_android_openssl_libraries(texture_compression_benchmark) - set_target_properties(texture_compression_benchmark PROPERTIES + add_android_openssl_libraries(texture_compression_preview) + set_target_properties(texture_compression_preview PROPERTIES QT_ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android" - QT_ANDROID_PACKAGE_NAME "org.alpinemaps.texturecompressionbenchmark" + QT_ANDROID_PACKAGE_NAME "org.alpinemaps.texturecompressionpreview" QT_ANDROID_VERSION_NAME "1.0" QT_ANDROID_VERSION_CODE 1 ) - install(TARGETS texture_compression_benchmark + install(TARGETS texture_compression_preview LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) endif() @@ -73,15 +44,15 @@ endif() if (EMSCRIPTEN) install( FILES - "$/texture_compression_benchmark.js" - "$/texture_compression_benchmark.wasm" - "$/texture_compression_benchmark.html" - "$/qtloader.js" - DESTINATION "${ALP_WWW_INSTALL_DIR}/texture_compression_benchmark" + "$/texture_compression_preview.js" + "$/texture_compression_preview.wasm" + "$/texture_compression_preview.html" + "$/qtloader.js" + DESTINATION "${ALP_WWW_INSTALL_DIR}/texture_compression_preview" ) install( - FILES "$/texture_compression_benchmark.worker.js" - DESTINATION "${ALP_WWW_INSTALL_DIR}/texture_compression_benchmark" + FILES "$/texture_compression_preview.worker.js" + DESTINATION "${ALP_WWW_INSTALL_DIR}/texture_compression_preview" OPTIONAL ) endif() diff --git a/apps/texture_compression_benchmark/Main.qml b/apps/texture_compression_benchmark/Main.qml index f5ffd001..4e1beef8 100644 --- a/apps/texture_compression_benchmark/Main.qml +++ b/apps/texture_compression_benchmark/Main.qml @@ -1,7 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts -import TextureCompressionBenchmark +import TextureCompressionPreview ApplicationWindow { id: root @@ -9,337 +9,94 @@ ApplicationWindow { width: 900 height: 900 minimumWidth: 360 - minimumHeight: 640 + minimumHeight: 480 visible: true - title: qsTr("Texture Compression Benchmark") + title: qsTr("Texture Compression Preview") LayoutMirroring.enabled: Qt.application.layoutDirection === Qt.RightToLeft LayoutMirroring.childrenInherit: true - BenchmarkItem { - id: benchmark - - readonly property bool showingPreview: previewDialogLoader.status === Loader.Ready - && previewDialogLoader.item.opened - - parent: showingPreview ? previewDialogLoader.item.previewHost : root.contentItem - x: 0 - y: 0 - z: showingPreview ? 0 : -1 - width: showingPreview ? parent.width : 1 - height: showingPreview ? parent.height : 1 - } - - ScrollView { - id: scrollView + ColumnLayout { anchors.fill: parent + anchors.margins: 20 + spacing: 12 + + Label { + Layout.fillWidth: true + text: qsTr("Texture compression preview") + font.pointSize: 20 + font.weight: Font.Medium + wrapMode: Text.Wrap + } - ColumnLayout { - width: scrollView.availableWidth - spacing: 16 - - Label { - Layout.fillWidth: true - Layout.leftMargin: 20 - Layout.rightMargin: 20 - Layout.topMargin: 20 - text: qsTr("CPU versus GPU texture compression") - font.pointSize: 20 - font.weight: Font.Medium - wrapMode: Text.Wrap - } - - Label { - Layout.fillWidth: true - Layout.leftMargin: 20 - Layout.rightMargin: 20 - text: qsTr("Compares direct CPU compression, Basis Universal encoding paths, and GPU compression of varied 512×512 ortho imagery in batches of four. Quality is computed once over all 16 images; timing covers three rounds of four distinct batches. WebGL requires ETC or sRGB S3TC compressed textures.") - wrapMode: Text.Wrap - } - - GroupBox { - Layout.fillWidth: true - Layout.leftMargin: 20 - Layout.rightMargin: 20 - title: qsTr("Configuration") - - ColumnLayout { - anchors.fill: parent - spacing: 12 - - RowLayout { - Layout.fillWidth: true - - Label { - Layout.fillWidth: true - text: qsTr("CPU encoder") - } - - ComboBox { - Layout.preferredWidth: 260 - model: [ - qsTr("Goofy direct (baseline)"), - qsTr("BasisU ETC1S"), - qsTr("BasisU UASTC LDR 4×4"), - qsTr("BasisU XUASTC LDR 4×4") - ] - currentIndex: benchmark.cpuEncoder - enabled: !benchmark.running - onActivated: benchmark.cpuEncoder = currentIndex - } - } - - Label { - Layout.fillWidth: true - text: qsTr("BasisU quality: %1").arg(benchmark.basisQuality) - enabled: benchmark.cpuEncoder !== BenchmarkItem.Goofy - } + Label { + Layout.fillWidth: true + text: preview.status + wrapMode: Text.Wrap + } - Slider { - Layout.fillWidth: true - from: 1 - to: 100 - stepSize: 1 - value: benchmark.basisQuality - enabled: !benchmark.running && benchmark.cpuEncoder !== BenchmarkItem.Goofy - onMoved: benchmark.basisQuality = Math.round(value) - } + ComboBox { + Layout.fillWidth: true + model: preview.previewEncoders + currentIndex: preview.previewEncoder + enabled: preview.ready + onActivated: preview.previewEncoder = currentIndex + } - Label { - Layout.fillWidth: true - text: qsTr("BasisU effort: %1").arg(benchmark.basisEffort) - enabled: benchmark.cpuEncoder !== BenchmarkItem.Goofy - } + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: 300 - Slider { - Layout.fillWidth: true - from: 0 - to: 10 - stepSize: 1 - value: benchmark.basisEffort - enabled: !benchmark.running && benchmark.cpuEncoder !== BenchmarkItem.Goofy - onMoved: benchmark.basisEffort = Math.round(value) - } + Flickable { + id: previewViewport - Label { - Layout.fillWidth: true - text: qsTr("GPU effort: %1").arg(benchmark.effort) - enabled: benchmark.gpuEncoder === BenchmarkItem.Search - } + anchors.fill: parent + clip: true + boundsBehavior: Flickable.StopAtBounds + contentWidth: Math.max(width, previewHost.width * previewHost.scale) + contentHeight: Math.max(height, previewHost.height * previewHost.scale) - Slider { - id: effortSlider - Layout.fillWidth: true - from: 0 - to: 10 - stepSize: 1 - value: benchmark.effort - enabled: !benchmark.running && benchmark.gpuEncoder === BenchmarkItem.Search - onMoved: benchmark.effort = Math.round(value) - } + Item { + id: previewHost - RowLayout { - Layout.fillWidth: true + readonly property real fittedSize: Math.min(previewViewport.width, previewViewport.height) - Label { - Layout.fillWidth: true - text: qsTr("GPU encoder") - } + x: (previewViewport.contentWidth - width) / 2 + y: (previewViewport.contentHeight - height) / 2 + width: fittedSize + height: width - ComboBox { - Layout.preferredWidth: 260 - model: [ - qsTr("Search (reference)"), - qsTr("Fast range (Goofy-inspired)"), - qsTr("Fast split (two sub-blocks)"), - qsTr("Fast split fused"), - qsTr("Fast split bounds") - ] - currentIndex: benchmark.gpuEncoder - enabled: !benchmark.running - onActivated: benchmark.gpuEncoder = currentIndex - } + TexturePreviewItem { + id: preview + anchors.fill: parent } - CheckBox { - Layout.fillWidth: true - text: qsTr("Generate and compress mipmaps") - checked: benchmark.mipmaps - enabled: !benchmark.running - onToggled: benchmark.mipmaps = checked + PinchHandler { + target: previewHost + rotationAxis.enabled: false + xAxis.enabled: false + yAxis.enabled: false + scaleAxis.minimum: 1 + scaleAxis.maximum: 8 } } } - RowLayout { - Layout.fillWidth: true - Layout.leftMargin: 20 - Layout.rightMargin: 20 - - Button { - text: qsTr("Run benchmark") - enabled: benchmark.dataReady && !benchmark.running - highlighted: true - onClicked: benchmark.runBenchmark() - } - - BusyIndicator { - Layout.preferredWidth: 44 - Layout.preferredHeight: 44 - running: benchmark.running - visible: benchmark.running - } - - Label { - Layout.fillWidth: true - text: benchmark.running - ? qsTr("Computing PSNR, BasisU encoding/transcoding, and batch timings; the display may pause to avoid contaminating GPU measurements.") - : benchmark.dataStatus - wrapMode: Text.Wrap - } - } - - GroupBox { - Layout.fillWidth: true - Layout.leftMargin: 20 - Layout.rightMargin: 20 - Layout.bottomMargin: 20 - title: qsTr("Results") - - ColumnLayout { - anchors.fill: parent - - TextArea { - Layout.fillWidth: true - Layout.minimumHeight: 300 - text: benchmark.resultText - readOnly: true - selectByMouse: true - wrapMode: TextEdit.Wrap - } - - RowLayout { - Layout.fillWidth: true - - Button { - text: qsTr("Preview encoders") - enabled: !benchmark.running && benchmark.previewReady - onClicked: { - if (previewDialogLoader.status === Loader.Ready) - previewDialogLoader.item.open() - else - previewDialogLoader.active = true - } - } - - Button { - text: qsTr("Copy JSON") - enabled: !benchmark.running && benchmark.resultJson.length > 0 - onClicked: benchmark.copyResultJson() - } - } - } + BusyIndicator { + anchors.centerIn: parent + running: preview.loading + visible: running } } - } - - Loader { - id: previewDialogLoader - - active: false - asynchronous: true - onLoaded: { - if (status === Loader.Ready) - item.open() - } - - sourceComponent: Component { - Dialog { - id: previewDialog - - property alias previewHost: previewHost - - parent: Overlay.overlay - anchors.centerIn: parent - width: Math.min(root.width - 40, 820) - height: Math.min(root.height - 40, 880) - modal: true - title: qsTr("Compressed tile preview") - standardButtons: Dialog.Close - - contentItem: ColumnLayout { - spacing: 8 - ComboBox { - Layout.fillWidth: true - model: benchmark.previewEncoders - currentIndex: benchmark.previewEncoder - onActivated: benchmark.previewEncoder = currentIndex - } - - Item { - id: previewContainer - - Layout.fillWidth: true - Layout.fillHeight: true - Layout.minimumHeight: 240 - - Flickable { - id: previewViewport - - anchors.fill: parent - clip: true - boundsBehavior: Flickable.StopAtBounds - contentWidth: Math.max(width, previewHost.width * previewHost.scale) - contentHeight: Math.max(height, previewHost.height * previewHost.scale) - - Item { - id: previewHost - - readonly property real fittedSize: Math.min(previewViewport.width, previewViewport.height) - - x: (previewViewport.contentWidth - width) / 2 - y: (previewViewport.contentHeight - height) / 2 - width: fittedSize - height: width - - PinchHandler { - target: previewHost - rotationAxis.enabled: false - xAxis.enabled: false - yAxis.enabled: false - scaleAxis.minimum: 1 - scaleAxis.maximum: 8 - } - } - } - - Rectangle { - anchors.left: parent.left - anchors.top: parent.top - anchors.margins: 8 - implicitWidth: previewDetails.implicitWidth + 24 - implicitHeight: previewDetails.implicitHeight + 24 - color: Qt.rgba(1, 1, 1, 0.8) - radius: 4 - - Label { - id: previewDetails - - readonly property bool showingReference: benchmark.previewCompressionTime < 0 - - anchors.fill: parent - anchors.margins: 12 - text: showingReference - ? qsTr("%1\nPSNR: ∞\nCompression: N/A").arg(benchmark.previewName) - : qsTr("%1\nPSNR: %2 dB\nCompleted compression: %3 ms") - .arg(benchmark.previewName) - .arg(benchmark.previewPsnr.toFixed(2)) - .arg(benchmark.previewCompressionTime.toFixed(3)) - } - } - } - } - } + Label { + Layout.fillWidth: true + text: preview.ready + ? (Number.isFinite(preview.previewPsnr) + ? qsTr("%1 — PSNR: %2 dB").arg(preview.previewName).arg(preview.previewPsnr.toFixed(2)) + : qsTr("%1 — PSNR: ∞").arg(preview.previewName)) + : "" + wrapMode: Text.Wrap } } } diff --git a/apps/texture_compression_benchmark/TexturePreviewItem.cpp b/apps/texture_compression_benchmark/TexturePreviewItem.cpp new file mode 100644 index 00000000..ab899fbb --- /dev/null +++ b/apps/texture_compression_benchmark/TexturePreviewItem.cpp @@ -0,0 +1,458 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2026 Adam Celarek + * SPDX-License-Identifier: GPL-3.0-or-later + *****************************************************************************/ + +#include "TexturePreviewItem.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace { +using Raster = radix::Raster; + +struct TileGroup { + int zoom; + int y; + int x; +}; + +constexpr std::array tile_groups { { + { 17, 45448, 71496 }, + { 16, 22832, 35144 }, + { 16, 23030, 35578 }, + { 13, 2852, 4476 }, + { 14, 5702, 8808 }, + { 15, 11574, 17670 }, + { 16, 23030, 35078 }, + { 15, 11460, 17622 }, + { 14, 5752, 8656 }, + { 16, 23084, 34746 }, + { 14, 5684, 8926 }, + { 15, 11418, 17692 }, + { 16, 22956, 34570 }, + { 15, 11358, 17904 }, + { 16, 22910, 35770 }, + { 14, 5656, 8938 }, +} }; + +QString tileUrl(const TileGroup& group, int x_offset, int y_offset) +{ + return QStringLiteral("https://gataki.cg.tuwien.ac.at/raw/basemap/tiles/%1/%2/%3.jpeg") + .arg(group.zoom) + .arg(group.y + y_offset) + .arg(group.x + x_offset); +} + +double srgbToLinear(uint8_t value) +{ + const auto normalised = double(value) / 255.0; + if (normalised <= 0.04045) + return normalised / 12.92; + return std::pow((normalised + 0.055) / 1.055, 2.4); +} + +double linearPsnr(std::span reconstructed, std::span sources) +{ + Q_ASSERT(reconstructed.size() == sources.size()); + double squared_error = 0.0; + uint64_t channel_count = 0; + for (size_t i = 0; i < sources.size(); ++i) { + for (int y = 0; y < reconstructed[i].height(); ++y) { + for (int x = 0; x < reconstructed[i].width(); ++x) { + const auto actual = reconstructed[i].pixel(x, y); + const auto expected = sources[i].pixel({ x, y }); + const std::array actual_channels { + qRed(actual) / 255.0, + qGreen(actual) / 255.0, + qBlue(actual) / 255.0, + }; + const std::array expected_channels { + srgbToLinear(expected.x), + srgbToLinear(expected.y), + srgbToLinear(expected.z), + }; + for (size_t channel = 0; channel < actual_channels.size(); ++channel) { + const auto difference = actual_channels[channel] - expected_channels[channel]; + squared_error += difference * difference; + } + } + } + channel_count += uint64_t(reconstructed[i].width()) * uint64_t(reconstructed[i].height()) * 3; + } + const auto mse = squared_error / double(channel_count); + return mse == 0.0 ? std::numeric_limits::infinity() : 10.0 * std::log10(1.0 / mse); +} + +QImage reconstruct(gl_engine::Texture& texture, unsigned resolution, unsigned layer) +{ + gl_engine::Framebuffer framebuffer( + gl_engine::Framebuffer::DepthFormat::None, { gl_engine::Framebuffer::ColourFormat::RGBA8 }, { resolution, resolution }); + framebuffer.bind(); + gl_engine::ShaderProgram shader(R"( + out highp vec2 texcoords; + void main() { + vec2 vertices[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); + gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0); + texcoords = 0.5 * gl_Position.xy + vec2(0.5); + })", + R"( + uniform lowp sampler2DArray texture_sampler; + uniform highp float texture_layer; + in highp vec2 texcoords; + out lowp vec4 out_color; + void main() { + out_color = textureLod(texture_sampler, vec3(texcoords.x, 1.0 - texcoords.y, texture_layer), 0.0); + })", + gl_engine::ShaderCodeSource::PLAINTEXT); + shader.bind(); + texture.bind(0); + shader.set_uniform("texture_sampler", 0); + shader.set_uniform("texture_layer", float(layer)); + gl_engine::helpers::create_screen_quad_geometry().draw(); + auto result = framebuffer.read_colour_attachment(0); + gl_engine::Framebuffer::unbind(); + return result; +} + +constexpr std::array gpu_encoders { + gl_engine::TextureCompressor::Encoder::Search, + gl_engine::TextureCompressor::Encoder::FastRange, + gl_engine::TextureCompressor::Encoder::FastSplit, + gl_engine::TextureCompressor::Encoder::FastSplitFused, + gl_engine::TextureCompressor::Encoder::FastSplitBounds, +}; + +QString gpuEncoderName(gl_engine::TextureCompressor::Encoder encoder) +{ + switch (encoder) { + case gl_engine::TextureCompressor::Encoder::Search: + return QStringLiteral("GPU Search (reference)"); + case gl_engine::TextureCompressor::Encoder::FastRange: + return QStringLiteral("GPU Fast range"); + case gl_engine::TextureCompressor::Encoder::FastSplit: + return QStringLiteral("GPU Fast split"); + case gl_engine::TextureCompressor::Encoder::FastSplitFused: + return QStringLiteral("GPU Fast split fused"); + case gl_engine::TextureCompressor::Encoder::FastSplitBounds: + return QStringLiteral("GPU Fast split bounds"); + } + return {}; +} + +constexpr size_t uncompressed_preview_index = 0; +constexpr size_t goofy_preview_index = 1; +constexpr size_t first_gpu_preview_index = 2; +constexpr size_t preview_count = first_gpu_preview_index + gpu_encoders.size(); +} // namespace + +class TexturePreviewRenderer final : public QQuickFramebufferObject::Renderer { +public: + void synchronize(QQuickFramebufferObject* item) override + { + auto* preview_item = static_cast(item); + m_item = preview_item; + m_window = preview_item->window(); + m_preview_encoder = preview_item->m_preview_encoder; + if (preview_item->m_request_serial == m_seen_serial) + return; + m_seen_serial = preview_item->m_request_serial; + m_source_images = preview_item->m_source_images; + m_pending = true; + } + + void render() override + { + m_window->beginExternalCommands(); + if (m_pending) { + m_pending = false; + const auto error = generateTextures(); + QPointer item = m_item; + const auto results = m_preview_results; + QMetaObject::invokeMethod(m_item, [item, error, results]() { + if (item) + item->publishResults(error, results); + }); + } + drawPreview(); + m_window->endExternalCommands(); + } + + QOpenGLFramebufferObject* createFramebufferObject(const QSize& size) override + { + QOpenGLFramebufferObjectFormat format; + format.setAttachment(QOpenGLFramebufferObject::NoAttachment); + return new QOpenGLFramebufferObject(size.expandedTo(QSize(1, 1)), format); + } + +private: + QString generateTextures() + { + constexpr unsigned resolution = 512; + constexpr unsigned effort = 4; + if (m_source_images.size() != tile_groups.size()) + return QStringLiteral("Preview imagery is incomplete."); + if (!gl_engine::TextureCompressor::is_supported()) + return QStringLiteral("GPU compression is unavailable on this device."); + + std::vector sources; + sources.reserve(m_source_images.size()); + for (const auto& image : m_source_images) + sources.push_back(nucleus::tile::conversion::to_rgba8raster(image)); + + std::vector layers(sources.size()); + std::iota(layers.begin(), layers.end(), 0u); + const auto algorithm = gl_engine::Texture::compression_algorithm(); + const auto filter = gl_engine::Texture::Filter::MipMapLinear; + m_preview_results.clear(); + m_preview_results.reserve(preview_count); + + auto create_texture = [&](gl_engine::Texture::Format format) { + auto texture = std::make_unique(gl_engine::Texture::Target::_2dArray, format); + texture->setParams(format == gl_engine::Texture::Format::CompressedRGBA8 ? filter : gl_engine::Texture::Filter::Linear, + gl_engine::Texture::Filter::Linear); + texture->allocate_array(resolution, resolution, unsigned(sources.size())); + return texture; + }; + auto psnr = [&](gl_engine::Texture& texture) { + std::vector reconstructed; + reconstructed.reserve(sources.size()); + for (unsigned layer = 0; layer < sources.size(); ++layer) + reconstructed.push_back(reconstruct(texture, resolution, layer)); + return linearPsnr(reconstructed, sources); + }; + + m_preview_textures[uncompressed_preview_index] = create_texture(gl_engine::Texture::Format::SRGBA8); + for (size_t layer = 0; layer < sources.size(); ++layer) + m_preview_textures[uncompressed_preview_index]->upload(sources[layer], unsigned(layer)); + m_preview_results.push_back({ QStringLiteral("Uncompressed reference"), std::numeric_limits::infinity() }); + + m_preview_textures[goofy_preview_index] = create_texture(gl_engine::Texture::Format::CompressedRGBA8); + for (size_t layer = 0; layer < sources.size(); ++layer) { + const auto compressed = nucleus::utils::generate_mipmapped_colour_texture(sources[layer], algorithm); + m_preview_textures[goofy_preview_index]->upload(compressed, unsigned(layer)); + } + m_preview_results.push_back({ QStringLiteral("Goofy CPU reference"), psnr(*m_preview_textures[goofy_preview_index]) }); + + for (size_t i = 0; i < gpu_encoders.size(); ++i) { + auto& texture = m_preview_textures[first_gpu_preview_index + i]; + texture = create_texture(gl_engine::Texture::Format::CompressedRGBA8); + gl_engine::TextureCompressor compressor(resolution, resolution, unsigned(sources.size())); + static_cast(compressor.compress(sources, + *texture, + layers, + { + .algorithm = algorithm, + .effort = effort, + .encoder = gpu_encoders[i], + .generate_mipmaps = true, + })); + m_preview_results.push_back({ gpuEncoderName(gpu_encoders[i]), psnr(*texture) }); + } + return {}; + } + + void drawPreview() + { + if (m_preview_encoder < 0 || size_t(m_preview_encoder) >= m_preview_textures.size() + || !m_preview_textures[size_t(m_preview_encoder)]) + return; + if (!m_preview_shader) { + m_preview_shader = std::make_unique(R"( + out highp vec2 texcoords; + void main() { + highp vec2 vertices[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); + gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0); + texcoords = 0.5 * gl_Position.xy + vec2(0.5); + })", + R"( + uniform lowp sampler2DArray texture_sampler; + in highp vec2 texcoords; + out lowp vec4 out_color; + highp vec3 linear_to_srgb(highp vec3 linear) { + return mix(12.92 * linear, + 1.055 * pow(linear, vec3(1.0 / 2.4)) - 0.055, + step(vec3(0.0031308), linear)); + } + void main() { + highp vec2 grid_position = texcoords * 4.0; + highp ivec2 cell = min(ivec2(grid_position), ivec2(3)); + highp float layer = float((3 - cell.y) * 4 + cell.x); + highp vec2 tile_coordinates = fract(grid_position); + highp vec4 linear_color = textureLod(texture_sampler, + vec3(tile_coordinates.x, 1.0 - tile_coordinates.y, layer), 0.0); + out_color = vec4(linear_to_srgb(linear_color.rgb), linear_color.a); + })", + gl_engine::ShaderCodeSource::PLAINTEXT); + m_preview_geometry = gl_engine::helpers::create_screen_quad_geometry(); + } + + auto* f = QOpenGLContext::currentContext()->extraFunctions(); + framebufferObject()->bind(); + f->glViewport(0, 0, framebufferObject()->width(), framebufferObject()->height()); + f->glDisable(GL_BLEND); + f->glDisable(GL_CULL_FACE); + f->glDisable(GL_DEPTH_TEST); + f->glDisable(GL_SCISSOR_TEST); + m_preview_shader->bind(); + m_preview_textures[size_t(m_preview_encoder)]->bind(0); + m_preview_shader->set_uniform("texture_sampler", 0); + m_preview_geometry.draw(); + m_preview_shader->release(); + } + + QPointer m_item; + QQuickWindow* m_window = nullptr; + unsigned m_seen_serial = 0; + int m_preview_encoder = 0; + bool m_pending = false; + std::vector m_source_images; + std::vector m_preview_results; + std::array, preview_count> m_preview_textures; + std::unique_ptr m_preview_shader; + gl_engine::helpers::ScreenQuadGeometry m_preview_geometry; +}; + +TexturePreviewItem::TexturePreviewItem(QQuickItem* parent) + : QQuickFramebufferObject(parent) + , m_network_manager(new QNetworkAccessManager(this)) +{ + setMirrorVertically(true); + downloadImages(); +} + +QQuickFramebufferObject::Renderer* TexturePreviewItem::createRenderer() const { return new TexturePreviewRenderer; } + +QString TexturePreviewItem::status() const { return m_status; } +bool TexturePreviewItem::loading() const { return m_loading; } +bool TexturePreviewItem::ready() const { return !m_preview_results.empty(); } +int TexturePreviewItem::previewEncoder() const { return m_preview_encoder; } + +void TexturePreviewItem::setPreviewEncoder(int value) +{ + value = m_preview_results.empty() ? 0 : std::clamp(value, 0, int(m_preview_results.size()) - 1); + if (m_preview_encoder == value) + return; + m_preview_encoder = value; + emit previewEncoderChanged(); + emit previewDetailsChanged(); + update(); +} + +QStringList TexturePreviewItem::previewEncoders() const +{ + QStringList result; + result.reserve(qsizetype(m_preview_results.size())); + for (const auto& preview : m_preview_results) + result.push_back(preview.name); + return result; +} + +QString TexturePreviewItem::previewName() const +{ + return ready() ? m_preview_results[size_t(m_preview_encoder)].name : QString {}; +} + +double TexturePreviewItem::previewPsnr() const +{ + return ready() ? m_preview_results[size_t(m_preview_encoder)].psnr : 0.0; +} + +void TexturePreviewItem::downloadImages() +{ + m_downloaded_tiles.resize(tile_groups.size() * 4); + m_downloads_remaining = int(m_downloaded_tiles.size()); + for (size_t group_index = 0; group_index < tile_groups.size(); ++group_index) { + for (int y = 0; y < 2; ++y) { + for (int x = 0; x < 2; ++x) { + const auto tile_index = group_index * 4 + size_t(y * 2 + x); + const auto url = tileUrl(tile_groups[group_index], x, y); + auto* reply = m_network_manager->get(QNetworkRequest(QUrl(url))); + connect(reply, &QNetworkReply::finished, this, [this, reply, tile_index, url]() { + if (reply->error() == QNetworkReply::NoError) { + const auto image = nucleus::utils::image_loader::rgba8(reply->readAll()); + if (image && image->size() == glm::uvec2(256u)) + m_downloaded_tiles[tile_index] = nucleus::tile::conversion::to_QImage(*image); + } + if (m_downloaded_tiles[tile_index].isNull()) + m_status = QStringLiteral("Unable to download preview tile: %1").arg(url); + reply->deleteLater(); + --m_downloads_remaining; + if (m_downloads_remaining > 0) { + if (!m_status.startsWith(QStringLiteral("Unable"))) { + m_status = QStringLiteral("Downloading preview imagery… %1/%2") + .arg(int(m_downloaded_tiles.size()) - m_downloads_remaining) + .arg(m_downloaded_tiles.size()); + } + emit statusChanged(); + return; + } + if (std::ranges::any_of(m_downloaded_tiles, [](const QImage& image) { return image.isNull(); })) { + m_loading = false; + emit statusChanged(); + return; + } + stitchImages(); + }); + } + } + } +} + +void TexturePreviewItem::stitchImages() +{ + m_source_images.clear(); + m_source_images.reserve(tile_groups.size()); + for (size_t group_index = 0; group_index < tile_groups.size(); ++group_index) { + QImage stitched(512, 512, QImage::Format_RGBA8888); + QPainter painter(&stitched); + for (int y = 0; y < 2; ++y) { + for (int x = 0; x < 2; ++x) + painter.drawImage(QPoint(x * 256, y * 256), m_downloaded_tiles[group_index * 4 + size_t(y * 2 + x)]); + } + m_source_images.push_back(std::move(stitched)); + } + m_downloaded_tiles.clear(); + m_status = QStringLiteral("Generating compressed texture arrays…"); + ++m_request_serial; + emit statusChanged(); + update(); +} + +void TexturePreviewItem::publishResults(const QString& error, const std::vector& results) +{ + m_preview_results = results; + m_preview_encoder = std::clamp(m_preview_encoder, 0, std::max(0, int(m_preview_results.size()) - 1)); + m_status = error.isEmpty() ? QStringLiteral("Texture previews ready.") : error; + m_loading = false; + emit statusChanged(); + emit previewResultsChanged(); + emit previewDetailsChanged(); + update(); +} diff --git a/apps/texture_compression_benchmark/TexturePreviewItem.h b/apps/texture_compression_benchmark/TexturePreviewItem.h new file mode 100644 index 00000000..701eedd2 --- /dev/null +++ b/apps/texture_compression_benchmark/TexturePreviewItem.h @@ -0,0 +1,68 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2026 Adam Celarek + * SPDX-License-Identifier: GPL-3.0-or-later + *****************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +class QNetworkAccessManager; + +class TexturePreviewItem : public QQuickFramebufferObject { + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(QString status READ status NOTIFY statusChanged) + Q_PROPERTY(bool loading READ loading NOTIFY statusChanged) + Q_PROPERTY(bool ready READ ready NOTIFY previewResultsChanged) + Q_PROPERTY(int previewEncoder READ previewEncoder WRITE setPreviewEncoder NOTIFY previewEncoderChanged) + Q_PROPERTY(QStringList previewEncoders READ previewEncoders NOTIFY previewResultsChanged) + Q_PROPERTY(QString previewName READ previewName NOTIFY previewDetailsChanged) + Q_PROPERTY(double previewPsnr READ previewPsnr NOTIFY previewDetailsChanged) + +public: + struct PreviewResult { + QString name; + double psnr = 0.0; + }; + + explicit TexturePreviewItem(QQuickItem* parent = nullptr); + Renderer* createRenderer() const override; + + [[nodiscard]] QString status() const; + [[nodiscard]] bool loading() const; + [[nodiscard]] bool ready() const; + [[nodiscard]] int previewEncoder() const; + void setPreviewEncoder(int value); + [[nodiscard]] QStringList previewEncoders() const; + [[nodiscard]] QString previewName() const; + [[nodiscard]] double previewPsnr() const; + +signals: + void statusChanged(); + void previewEncoderChanged(); + void previewResultsChanged(); + void previewDetailsChanged(); + +private: + friend class TexturePreviewRenderer; + void downloadImages(); + void stitchImages(); + void publishResults(const QString& error, const std::vector& results); + + unsigned m_request_serial = 0; + int m_downloads_remaining = 0; + QNetworkAccessManager* m_network_manager = nullptr; + std::vector m_downloaded_tiles; + std::vector m_source_images; + QString m_status = QStringLiteral("Downloading preview imagery…"); + bool m_loading = true; + int m_preview_encoder = 0; + std::vector m_preview_results; +}; diff --git a/apps/texture_compression_benchmark/android/AndroidManifest.xml b/apps/texture_compression_benchmark/android/AndroidManifest.xml index c0808204..d0043555 100644 --- a/apps/texture_compression_benchmark/android/AndroidManifest.xml +++ b/apps/texture_compression_benchmark/android/AndroidManifest.xml @@ -1,11 +1,11 @@ - + - + - + diff --git a/apps/texture_compression_benchmark/main.cpp b/apps/texture_compression_benchmark/main.cpp index 8c2ea468..76298190 100644 --- a/apps/texture_compression_benchmark/main.cpp +++ b/apps/texture_compression_benchmark/main.cpp @@ -28,13 +28,13 @@ int main(int argc, char** argv) QGuiApplication application(argc, argv); QCoreApplication::setOrganizationName(QStringLiteral("AlpineMaps.org")); - QCoreApplication::setApplicationName(QStringLiteral("TextureCompressionBenchmark")); - QGuiApplication::setApplicationDisplayName(QStringLiteral("Texture Compression Benchmark")); + QCoreApplication::setApplicationName(QStringLiteral("TextureCompressionPreview")); + QGuiApplication::setApplicationDisplayName(QStringLiteral("Texture Compression Preview")); QFontDatabase::addApplicationFont(QStringLiteral(":/fonts/Roboto/Roboto-Regular.ttf")); application.setFont(QFont(QStringLiteral("Roboto"), 12, QFont::Normal)); QQmlApplicationEngine engine; - engine.loadFromModule("TextureCompressionBenchmark", "Main"); + engine.loadFromModule("TextureCompressionPreview", "Main"); if (engine.rootObjects().isEmpty()) return -1; return application.exec(); diff --git a/gl_engine/Texture.cpp b/gl_engine/Texture.cpp index 43a8a0df..0ad9d036 100644 --- a/gl_engine/Texture.cpp +++ b/gl_engine/Texture.cpp @@ -25,11 +25,7 @@ #include #include #include -#include #include -#include -#include -#include #ifdef __EMSCRIPTEN__ #include #endif @@ -386,293 +382,6 @@ float gl_engine::Texture::max_anisotropy() #endif } -namespace { -template gl_engine::TextureCompressor::StageTiming measure_gl(Callable&& callable, bool finish) -{ - const auto start = std::chrono::steady_clock::now(); - std::forward(callable)(); - const auto submitted = std::chrono::steady_clock::now(); - if (!finish) { - return { - std::chrono::duration(submitted - start).count(), - 0.0, - }; - } - QOpenGLContext::currentContext()->extraFunctions()->glFinish(); - const auto completed = std::chrono::steady_clock::now(); - return { - std::chrono::duration(submitted - start).count(), - std::chrono::duration(completed - submitted).count(), - }; -} -} - -double gl_engine::TextureCompressor::GpuTimings::total_ms() const -{ - return scratch_upload_ms + mipmap_generation_ms + compression_pass_ms + packing_pass_ms + output_transfer_ms - + compressed_upload_ms; -} - -struct gl_engine::TextureCompressor::GpuTimer::Impl { - static constexpr GLenum time_elapsed = 0x88BF; // GL_TIME_ELAPSED_EXT - static constexpr GLenum gpu_disjoint = 0x8FBB; // GL_GPU_DISJOINT_EXT - static constexpr size_t stage_count = 6; - - struct Sample { - std::array queries {}; - std::array used {}; - }; - - bool supported = false; - bool uses_extension_functions = false; - uint64_t next_ticket = 1; - uint64_t active_ticket = 0; - std::optional active_stage; - std::unordered_map samples; - -#if defined(__ANDROID__) - using GenQueries = void (*)(GLsizei, GLuint*); - using DeleteQueries = void (*)(GLsizei, const GLuint*); - using BeginQuery = void (*)(GLenum, GLuint); - using EndQuery = void (*)(GLenum); - using GetQueryObjectuiv = void (*)(GLuint, GLenum, GLuint*); - using GetQueryObjectui64v = void (*)(GLuint, GLenum, GLuint64*); - GenQueries gen_queries = nullptr; - DeleteQueries delete_queries = nullptr; - BeginQuery begin_query = nullptr; - EndQuery end_query = nullptr; - GetQueryObjectuiv get_query_object_uiv = nullptr; - GetQueryObjectui64v get_query_object_ui64v = nullptr; -#endif - - Impl() - { - auto* context = QOpenGLContext::currentContext(); - if (!context) - return; -#if defined(__EMSCRIPTEN__) - const auto webgl_context = emscripten_webgl_get_current_context(); - supported = webgl_context && emscripten_webgl_enable_extension(webgl_context, "EXT_disjoint_timer_query_webgl2"); -#elif defined(__ANDROID__) - supported = context->hasExtension(QByteArrayLiteral("GL_EXT_disjoint_timer_query")); - if (!supported) - return; - gen_queries = reinterpret_cast(context->getProcAddress("glGenQueriesEXT")); - delete_queries = reinterpret_cast(context->getProcAddress("glDeleteQueriesEXT")); - begin_query = reinterpret_cast(context->getProcAddress("glBeginQueryEXT")); - end_query = reinterpret_cast(context->getProcAddress("glEndQueryEXT")); - get_query_object_uiv = reinterpret_cast(context->getProcAddress("glGetQueryObjectuivEXT")); - get_query_object_ui64v = reinterpret_cast(context->getProcAddress("glGetQueryObjectui64vEXT")); - supported = gen_queries && delete_queries && begin_query && end_query && get_query_object_uiv && get_query_object_ui64v; - uses_extension_functions = supported; -#else - const auto format = context->format(); - supported = format.majorVersion() > 3 || (format.majorVersion() == 3 && format.minorVersion() >= 3) - || context->hasExtension(QByteArrayLiteral("GL_ARB_timer_query")); -#endif - } - - void gen_query(GLuint* query) - { -#if defined(__EMSCRIPTEN__) - glGenQueries(1, query); - return; -#endif -#if defined(__ANDROID__) - if (uses_extension_functions) { - gen_queries(1, query); - return; - } -#endif - QOpenGLContext::currentContext()->extraFunctions()->glGenQueries(1, query); - } - - void delete_query(GLuint query) - { - if (!query) - return; -#if defined(__EMSCRIPTEN__) - glDeleteQueries(1, &query); - return; -#endif -#if defined(__ANDROID__) - if (uses_extension_functions) { - delete_queries(1, &query); - return; - } -#endif - QOpenGLContext::currentContext()->extraFunctions()->glDeleteQueries(1, &query); - } - - void begin(GLuint query) - { -#if defined(__EMSCRIPTEN__) - glBeginQuery(time_elapsed, query); - return; -#endif -#if defined(__ANDROID__) - if (uses_extension_functions) { - begin_query(time_elapsed, query); - return; - } -#endif - QOpenGLContext::currentContext()->extraFunctions()->glBeginQuery(time_elapsed, query); - } - - void end() - { -#if defined(__EMSCRIPTEN__) - glEndQuery(time_elapsed); - return; -#endif -#if defined(__ANDROID__) - if (uses_extension_functions) { - end_query(time_elapsed); - return; - } -#endif - QOpenGLContext::currentContext()->extraFunctions()->glEndQuery(time_elapsed); - } - - void get_query_uiv(GLuint query, GLenum parameter, GLuint* value) - { -#if defined(__EMSCRIPTEN__) - glGetQueryObjectuiv(query, parameter, value); - return; -#endif -#if defined(__ANDROID__) - if (uses_extension_functions) { - get_query_object_uiv(query, parameter, value); - return; - } -#endif - QOpenGLContext::currentContext()->extraFunctions()->glGetQueryObjectuiv(query, parameter, value); - } - - void get_query_result(GLuint query, GLuint64* value) - { -#if defined(__EMSCRIPTEN__) - GLuint result = 0; - glGetQueryObjectuiv(query, GL_QUERY_RESULT, &result); - *value = result; -#elif defined(__ANDROID__) - get_query_object_ui64v(query, GL_QUERY_RESULT, value); -#else - GLuint result = 0; - QOpenGLContext::currentContext()->extraFunctions()->glGetQueryObjectuiv(query, GL_QUERY_RESULT, &result); - *value = result; -#endif - } - - void delete_sample(Sample& sample) - { - for (const auto query : sample.queries) - delete_query(query); - } -}; - -gl_engine::TextureCompressor::GpuTimer::GpuTimer() - : m(std::make_unique()) -{ -} - -gl_engine::TextureCompressor::GpuTimer::~GpuTimer() -{ - if (!QOpenGLContext::currentContext()) - return; - for (auto& [ticket, sample] : m->samples) { - static_cast(ticket); - m->delete_sample(sample); - } -} - -bool gl_engine::TextureCompressor::GpuTimer::is_supported() const { return m->supported; } - -uint64_t gl_engine::TextureCompressor::GpuTimer::begin_sample() -{ - if (!m->supported) - return 0; - Q_ASSERT(m->active_ticket == 0); - m->active_ticket = m->next_ticket++; - m->samples.emplace(m->active_ticket, Impl::Sample {}); - return m->active_ticket; -} - -void gl_engine::TextureCompressor::GpuTimer::begin_stage(Stage stage) -{ - if (!m->active_ticket) - return; - Q_ASSERT(!m->active_stage.has_value()); - auto& sample = m->samples.at(m->active_ticket); - const auto index = size_t(stage); - Q_ASSERT(!sample.used[index]); - m->gen_query(&sample.queries[index]); - sample.used[index] = true; - m->begin(sample.queries[index]); - m->active_stage = stage; -} - -void gl_engine::TextureCompressor::GpuTimer::end_stage() -{ - if (!m->active_ticket) - return; - Q_ASSERT(m->active_stage.has_value()); - m->end(); - m->active_stage.reset(); -} - -void gl_engine::TextureCompressor::GpuTimer::end_sample() -{ - if (!m->active_ticket) - return; - Q_ASSERT(!m->active_stage.has_value()); - m->active_ticket = 0; -} - -gl_engine::TextureCompressor::GpuTimer::PollStatus gl_engine::TextureCompressor::GpuTimer::poll( - uint64_t ticket, GpuTimings& timings) -{ - Q_ASSERT(ticket != 0); - const auto iterator = m->samples.find(ticket); - Q_ASSERT(iterator != m->samples.end()); - GLint disjoint = GL_FALSE; -#if defined(__EMSCRIPTEN__) || defined(__ANDROID__) - QOpenGLContext::currentContext()->extraFunctions()->glGetIntegerv(Impl::gpu_disjoint, &disjoint); -#endif - if (disjoint) { - m->delete_sample(iterator->second); - m->samples.erase(iterator); - return PollStatus::Disjoint; - } - - for (size_t index = 0; index < Impl::stage_count; ++index) { - if (!iterator->second.used[index]) - continue; - GLuint available = GL_FALSE; - m->get_query_uiv(iterator->second.queries[index], GL_QUERY_RESULT_AVAILABLE, &available); - if (!available) - return PollStatus::Pending; - } - - std::array milliseconds {}; - for (size_t index = 0; index < Impl::stage_count; ++index) { - if (!iterator->second.used[index]) - continue; - GLuint64 nanoseconds = 0; - m->get_query_result(iterator->second.queries[index], &nanoseconds); - milliseconds[index] = double(nanoseconds) / 1'000'000.0; - } - timings.scratch_upload_ms = milliseconds[size_t(Stage::ScratchUpload)]; - timings.mipmap_generation_ms = milliseconds[size_t(Stage::MipmapGeneration)]; - timings.compression_pass_ms = milliseconds[size_t(Stage::CompressionPass)]; - timings.packing_pass_ms = milliseconds[size_t(Stage::PackingPass)]; - timings.output_transfer_ms = milliseconds[size_t(Stage::OutputTransfer)]; - timings.compressed_upload_ms = milliseconds[size_t(Stage::CompressedUpload)]; - m->delete_sample(iterator->second); - m->samples.erase(iterator); - return PollStatus::Ready; -} - struct gl_engine::TextureCompressor::Impl { static constexpr unsigned max_shader_mip_levels = 16; @@ -901,23 +610,8 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: auto* f = QOpenGLContext::currentContext()->extraFunctions(); Result result; result.mip_levels = settings.generate_mipmaps ? mip_level_count(m->width, m->height) : 1; - const bool finish_stages = settings.timing_mode == TimingMode::IndividualStages; - const bool finish_total = settings.timing_mode == TimingMode::EndToEnd; - const auto total_start = std::chrono::steady_clock::now(); - auto* gpu_timer = settings.gpu_timer && settings.gpu_timer->is_supported() ? settings.gpu_timer : nullptr; - if (gpu_timer) - result.gpu_timing_ticket = gpu_timer->begin_sample(); - const auto measure_stage = [&](GpuTimer::Stage stage, auto&& callable) { - return measure_gl([&]() { - if (gpu_timer) - gpu_timer->begin_stage(stage); - std::forward(callable)(); - if (gpu_timer) - gpu_timer->end_stage(); - }, finish_stages); - }; - - result.timings.scratch_upload = measure_stage(GpuTimer::Stage::ScratchUpload, [&]() { + + { f->glBindTexture(GL_TEXTURE_2D_ARRAY, m->scratch_texture); f->glPixelStorei(GL_UNPACK_ALIGNMENT, 1); for (size_t layer = 0; layer < textures.size(); ++layer) { @@ -933,12 +627,10 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: GL_UNSIGNED_BYTE, textures[layer].bytes().data()); } - }); + } if (settings.generate_mipmaps) { - result.timings.mipmap_generation = measure_stage(GpuTimer::Stage::MipmapGeneration, [&]() { - f->glBindTexture(GL_TEXTURE_2D_ARRAY, m->scratch_texture); - f->glGenerateMipmap(GL_TEXTURE_2D_ARRAY); - }); + f->glBindTexture(GL_TEXTURE_2D_ARRAY, m->scratch_texture); + f->glGenerateMipmap(GL_TEXTURE_2D_ARRAY); } std::vector level_offsets; @@ -969,7 +661,7 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: GLboolean depth_enabled = GL_FALSE; GLboolean scissor_enabled = GL_FALSE; - result.timings.compression_pass = measure_stage(GpuTimer::Stage::CompressionPass, [&]() { + { auto* program = m->dxt1_fragment_program.get(); if (settings.algorithm == nucleus::utils::ColourTexture::Format::ETC1) { if (settings.encoder == Encoder::FastRange) @@ -1014,9 +706,9 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: f->glBindTexture(GL_TEXTURE_2D_ARRAY, m->scratch_texture); f->glBindVertexArray(m->vertex_array); f->glDrawArrays(GL_TRIANGLES, 0, 3); - }); + } - result.timings.packing_pass = measure_stage(GpuTimer::Stage::PackingPass, [&]() { + { f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m->packing_framebuffer); f->glViewport(0, 0, m->output_atlas_width, m->output_atlas_height); m->packing_program->bind(); @@ -1041,13 +733,9 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: f->glColorMask(previous_colour_mask[0], previous_colour_mask[1], previous_colour_mask[2], previous_colour_mask[3]); f->glViewport(previous_viewport[0], previous_viewport[1], previous_viewport[2], previous_viewport[3]); f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, GLuint(previous_draw_framebuffer)); - }); - result.timings.encoding.submission_ms - = result.timings.compression_pass.submission_ms + result.timings.packing_pass.submission_ms; - result.timings.encoding.completion_wait_ms - = result.timings.compression_pass.completion_wait_ms + result.timings.packing_pass.completion_wait_ms; + } - result.timings.output_transfer = measure_stage(GpuTimer::Stage::OutputTransfer, [&]() { + { GLint previous_read_framebuffer = 0; GLint previous_read_buffer = 0; GLint previous_pack_alignment = 0; @@ -1063,9 +751,9 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: f->glPixelStorei(GL_PACK_ALIGNMENT, previous_pack_alignment); f->glBindFramebuffer(GL_READ_FRAMEBUFFER, GLuint(previous_read_framebuffer)); f->glReadBuffer(GLenum(previous_read_buffer)); - }); + } - result.timings.compressed_upload = measure_stage(GpuTimer::Stage::CompressedUpload, [&]() { + { f->glBindTexture(GL_TEXTURE_2D_ARRAY, destination.m_id); f->glBindBuffer(GL_PIXEL_UNPACK_BUFFER, m->encoded_buffer); const auto format = Texture::compressed_texture_format(); @@ -1089,12 +777,7 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: } } f->glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); - }); - if (gpu_timer) - gpu_timer->end_sample(); + } f->glActiveTexture(GL_TEXTURE0); - if (finish_total) - f->glFinish(); - result.timings.total_ms = std::chrono::duration(std::chrono::steady_clock::now() - total_start).count(); return result; } diff --git a/gl_engine/Texture.h b/gl_engine/Texture.h index c1626681..68a22be4 100644 --- a/gl_engine/Texture.h +++ b/gl_engine/Texture.h @@ -21,7 +21,6 @@ #include #include #include -#include #include #include #include @@ -92,81 +91,18 @@ class Texture { class TextureCompressor { public: - enum class TimingMode { EndToEnd, IndividualStages, SubmissionOnly }; enum class Encoder { Search, FastRange, FastSplit, FastSplitFused, FastSplitBounds }; - struct GpuTimings { - double scratch_upload_ms = 0.0; - double mipmap_generation_ms = 0.0; - double compression_pass_ms = 0.0; - double packing_pass_ms = 0.0; - double output_transfer_ms = 0.0; - double compressed_upload_ms = 0.0; - - [[nodiscard]] double total_ms() const; - }; - - class GpuTimer { - public: - enum class PollStatus { Pending, Ready, Disjoint }; - - GpuTimer(); - ~GpuTimer(); - GpuTimer(const GpuTimer&) = delete; - GpuTimer(GpuTimer&&) = delete; - GpuTimer& operator=(const GpuTimer&) = delete; - GpuTimer& operator=(GpuTimer&&) = delete; - - [[nodiscard]] bool is_supported() const; - [[nodiscard]] PollStatus poll(uint64_t ticket, GpuTimings& timings); - - private: - friend class TextureCompressor; - enum class Stage { ScratchUpload, MipmapGeneration, CompressionPass, PackingPass, OutputTransfer, CompressedUpload }; - - [[nodiscard]] uint64_t begin_sample(); - void begin_stage(Stage stage); - void end_stage(); - void end_sample(); - - struct Impl; - std::unique_ptr m; - }; - struct Settings { nucleus::utils::ColourTexture::Format algorithm = nucleus::utils::ColourTexture::Format::DXT1; unsigned effort = 0; Encoder encoder = Encoder::Search; bool generate_mipmaps = true; - TimingMode timing_mode = TimingMode::EndToEnd; - GpuTimer* gpu_timer = nullptr; - }; - - struct StageTiming { - double submission_ms = 0.0; - double completion_wait_ms = 0.0; - - [[nodiscard]] double total_ms() const { return submission_ms + completion_wait_ms; } - }; - - struct Timings { - // EndToEnd includes one final completion wait. IndividualStages includes a completion - // wait per stage. SubmissionOnly does not wait for GPU completion. - StageTiming scratch_upload; - StageTiming mipmap_generation; - StageTiming compression_pass; - StageTiming packing_pass; - StageTiming encoding; - StageTiming output_transfer; - StageTiming compressed_upload; - double total_ms = 0.0; }; struct Result { - Timings timings; size_t encoded_bytes = 0; unsigned mip_levels = 0; - uint64_t gpu_timing_ticket = 0; }; TextureCompressor(unsigned width, unsigned height, unsigned max_batch_size); diff --git a/nucleus/utils/BasisUniversalTextureCompression.cpp b/nucleus/utils/BasisUniversalTextureCompression.cpp deleted file mode 100644 index 0be5a6ed..00000000 --- a/nucleus/utils/BasisUniversalTextureCompression.cpp +++ /dev/null @@ -1,175 +0,0 @@ -/***************************************************************************** - * AlpineMaps.org - * Copyright (C) 2026 Adam Celarek - * SPDX-License-Identifier: GPL-3.0-or-later - *****************************************************************************/ - -#include "BasisUniversalTextureCompression.h" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -namespace { - -using Clock = std::chrono::steady_clock; - -double elapsed_ms(Clock::time_point start) -{ - return std::chrono::duration(Clock::now() - start).count(); -} - -basist::basis_tex_format basis_format(nucleus::utils::BasisUniversalFormat format) -{ - using Format = nucleus::utils::BasisUniversalFormat; - switch (format) { - case Format::ETC1S: - return basist::basis_tex_format::cETC1S; - case Format::UASTC_LDR_4x4: - return basist::basis_tex_format::cUASTC_LDR_4x4; - case Format::XUASTC_LDR_4x4: - return basist::basis_tex_format::cXUASTC_LDR_4x4; - } - return basist::basis_tex_format::cETC1S; -} - -basist::transcoder_texture_format transcoder_format(nucleus::utils::ColourTexture::Format format) -{ - using Format = nucleus::utils::ColourTexture::Format; - switch (format) { - case Format::DXT1: - return basist::transcoder_texture_format::cTFBC1_RGB; - case Format::ETC1: - return basist::transcoder_texture_format::cTFETC1_RGB; - case Format::Uncompressed_RGBA: - break; - } - return basist::transcoder_texture_format::cTFRGBA32; -} - -std::vector> mip_levels( - const radix::Raster& source, bool generate_mipmaps) -{ - if (!generate_mipmaps) - return { source }; - auto levels = radix::raster::generate_mipmap(source); - return levels ? std::move(*levels) : std::vector> {}; -} - -basisu::vector basis_images(const std::vector>& levels) -{ - static_assert(sizeof(basisu::color_rgba) == sizeof(glm::u8vec4)); - basisu::vector result; - result.reserve(levels.size()); - for (const auto& level : levels) { - basisu::image image(level.width(), level.height()); - std::memcpy(image.get_ptr(), level.bytes().data(), level.size_in_bytes()); - result.push_back(std::move(image)); - } - return result; -} - -void initialise_basis_universal() -{ - static std::once_flag flag; - std::call_once(flag, [] { basisu::basisu_encoder_init(); }); -} - -} // namespace - -const char* nucleus::utils::basis_universal_format_name(BasisUniversalFormat format) -{ - switch (format) { - case BasisUniversalFormat::ETC1S: - return "BasisU ETC1S"; - case BasisUniversalFormat::UASTC_LDR_4x4: - return "BasisU UASTC LDR 4x4"; - case BasisUniversalFormat::XUASTC_LDR_4x4: - return "BasisU XUASTC LDR 4x4"; - } - return "BasisU unknown"; -} - -std::expected -nucleus::utils::compress_with_basis_universal( - const radix::Raster& source, const BasisUniversalCompressionSettings& settings) -{ - if (source.buffer().empty()) - return std::unexpected("Cannot compress an empty image"); - if (settings.target_format == ColourTexture::Format::Uncompressed_RGBA) - return std::unexpected("Basis Universal target must be BC1 or ETC1"); - if (source.width() > int(basist::BASISU_MAX_SUPPORTED_TEXTURE_DIMENSION) - || source.height() > int(basist::BASISU_MAX_SUPPORTED_TEXTURE_DIMENSION)) { - return std::unexpected("Image exceeds Basis Universal's maximum dimensions"); - } - - initialise_basis_universal(); - BasisUniversalCompressionResult result; - - const auto preparation_start = Clock::now(); - const auto levels = mip_levels(source, settings.generate_mipmaps); - if (levels.empty()) - return std::unexpected("Basis Universal requires power-of-two mipmap input"); - auto images = basis_images(levels); - result.timings.source_preparation_ms = elapsed_ms(preparation_start); - - const auto encoding_start = Clock::now(); - size_t encoded_size = 0; - const auto flags = uint32_t(basisu::cFlagSRGB); - void* encoded = basisu::basis_compress2(basis_format(settings.format), - images, - flags, - std::clamp(settings.quality, 1, 100), - std::clamp(settings.effort, 0, 10), - &encoded_size); - result.timings.encoding_ms = elapsed_ms(encoding_start); - if (!encoded) - return std::unexpected("Basis Universal encoding failed"); - const auto encoded_deleter = [](void* data) { basisu::basis_free_data(data); }; - std::unique_ptr encoded_owner(encoded, encoded_deleter); - result.intermediate_bytes = encoded_size; - if (encoded_size > std::numeric_limits::max()) - return std::unexpected("Basis Universal output is too large to transcode"); - - const auto transcoding_start = Clock::now(); - basist::basisu_transcoder transcoder; - const auto encoded_size_u32 = uint32_t(encoded_size); - if (!transcoder.validate_header(encoded, encoded_size_u32)) - return std::unexpected("Basis Universal produced an invalid header"); - if (transcoder.get_total_images(encoded, encoded_size_u32) != 1) - return std::unexpected("Basis Universal produced an unexpected image count"); - if (!transcoder.start_transcoding(encoded, encoded_size_u32)) - return std::unexpected("Basis Universal transcoder initialisation failed"); - - const auto level_count = transcoder.get_total_image_levels(encoded, encoded_size_u32, 0); - if (level_count != levels.size()) - return std::unexpected("Basis Universal produced an unexpected mip level count"); - result.texture.reserve(level_count); - for (uint32_t level_index = 0; level_index < level_count; ++level_index) { - basist::basisu_image_level_info info; - if (!transcoder.get_image_level_info(encoded, encoded_size_u32, info, 0, level_index)) - return std::unexpected("Unable to inspect a Basis Universal mip level"); - const auto block_count = ((info.m_orig_width + 3u) / 4u) * ((info.m_orig_height + 3u) / 4u); - std::vector blocks(size_t(block_count) * 8u); - if (!transcoder.transcode_image_level(encoded, - encoded_size_u32, - 0, - level_index, - blocks.data(), - block_count, - transcoder_format(settings.target_format))) { - return std::unexpected("Basis Universal transcoding failed"); - } - result.transcoded_bytes += blocks.size(); - result.texture.emplace_back(std::move(blocks), info.m_orig_width, info.m_orig_height, settings.target_format); - } - result.timings.transcoding_ms = elapsed_ms(transcoding_start); - return result; -} diff --git a/nucleus/utils/BasisUniversalTextureCompression.h b/nucleus/utils/BasisUniversalTextureCompression.h deleted file mode 100644 index 680008e3..00000000 --- a/nucleus/utils/BasisUniversalTextureCompression.h +++ /dev/null @@ -1,46 +0,0 @@ -/***************************************************************************** - * AlpineMaps.org - * Copyright (C) 2026 Adam Celarek - * SPDX-License-Identifier: GPL-3.0-or-later - *****************************************************************************/ - -#pragma once - -#include "ColourTexture.h" - -#include -#include -#include - -namespace nucleus::utils { - -enum class BasisUniversalFormat { ETC1S, UASTC_LDR_4x4, XUASTC_LDR_4x4 }; - -struct BasisUniversalCompressionSettings { - BasisUniversalFormat format = BasisUniversalFormat::ETC1S; - ColourTexture::Format target_format = ColourTexture::Format::DXT1; - int quality = 75; - int effort = 4; - bool generate_mipmaps = true; -}; - -struct BasisUniversalCompressionTimings { - double source_preparation_ms = 0.0; - double encoding_ms = 0.0; - double transcoding_ms = 0.0; - - [[nodiscard]] double total_ms() const { return source_preparation_ms + encoding_ms + transcoding_ms; } -}; - -struct BasisUniversalCompressionResult { - MipmappedColourTexture texture; - BasisUniversalCompressionTimings timings; - size_t intermediate_bytes = 0; - size_t transcoded_bytes = 0; -}; - -[[nodiscard]] const char* basis_universal_format_name(BasisUniversalFormat format); -[[nodiscard]] std::expected compress_with_basis_universal( - const radix::Raster& source, const BasisUniversalCompressionSettings& settings); - -} // namespace nucleus::utils diff --git a/unittests/gl_engine/texture.cpp b/unittests/gl_engine/texture.cpp index 0d444e0e..900e193d 100644 --- a/unittests/gl_engine/texture.cpp +++ b/unittests/gl_engine/texture.cpp @@ -718,40 +718,6 @@ TEST_CASE("gl texture GPU compression quality") } CHECK(result.encoded_bytes == expected_size * sources.size()); CHECK(result.mip_levels == 7); - CHECK(result.timings.total_ms > 0.0); - - const auto profiled_result = compressor.compress(sources, - destination, - destination_layers, - { .algorithm = gl_engine::Texture::compression_algorithm(), - .effort = 4, - .generate_mipmaps = true, - .timing_mode = gl_engine::TextureCompressor::TimingMode::IndividualStages }); - CHECK(profiled_result.timings.scratch_upload.total_ms() > 0.0); - CHECK(profiled_result.timings.mipmap_generation.total_ms() > 0.0); - CHECK(profiled_result.timings.compression_pass.total_ms() > 0.0); - CHECK(profiled_result.timings.packing_pass.total_ms() > 0.0); - CHECK(profiled_result.timings.encoding.total_ms() > 0.0); - CHECK(profiled_result.timings.output_transfer.total_ms() > 0.0); - CHECK(profiled_result.timings.compressed_upload.total_ms() > 0.0); - CHECK(profiled_result.timings.total_ms > 0.0); - CHECK(f->glGetError() == GL_NO_ERROR); - - const auto submitted_result = compressor.compress(sources, - destination, - destination_layers, - { .algorithm = gl_engine::Texture::compression_algorithm(), - .effort = 4, - .generate_mipmaps = true, - .timing_mode = gl_engine::TextureCompressor::TimingMode::SubmissionOnly }); - CHECK(submitted_result.timings.scratch_upload.completion_wait_ms == 0.0); - CHECK(submitted_result.timings.mipmap_generation.completion_wait_ms == 0.0); - CHECK(submitted_result.timings.compression_pass.completion_wait_ms == 0.0); - CHECK(submitted_result.timings.packing_pass.completion_wait_ms == 0.0); - CHECK(submitted_result.timings.output_transfer.completion_wait_ms == 0.0); - CHECK(submitted_result.timings.compressed_upload.completion_wait_ms == 0.0); - f->glFinish(); - CHECK(f->glGetError() == GL_NO_ERROR); Framebuffer framebuffer(Framebuffer::DepthFormat::None, { Framebuffer::ColourFormat::RGBA8 }, { resolution, resolution }); framebuffer.bind(); diff --git a/unittests/nucleus/CMakeLists.txt b/unittests/nucleus/CMakeLists.txt index 9362ac34..9600a4b0 100644 --- a/unittests/nucleus/CMakeLists.txt +++ b/unittests/nucleus/CMakeLists.txt @@ -91,10 +91,6 @@ if (ALP_ENABLE_AVALANCHE_WARNING_LAYER) ) endif() -if (TARGET alp_basisu_texture_compression) - target_sources(unittests_nucleus PRIVATE basis_universal_texture_compression.cpp) - target_link_libraries(unittests_nucleus PUBLIC alp_basisu_texture_compression) -endif() target_link_libraries(unittests_nucleus PUBLIC nucleus Catch2::Catch2 Qt::Test Qt::Gui) target_compile_definitions(unittests_nucleus PUBLIC "ALP_TEST_DATA_DIR=\":/test_data/\"") diff --git a/unittests/nucleus/basis_universal_texture_compression.cpp b/unittests/nucleus/basis_universal_texture_compression.cpp deleted file mode 100644 index 8bdf5c23..00000000 --- a/unittests/nucleus/basis_universal_texture_compression.cpp +++ /dev/null @@ -1,51 +0,0 @@ -/***************************************************************************** - * AlpineMaps.org - * Copyright (C) 2026 Adam Celarek - * SPDX-License-Identifier: GPL-3.0-or-later - *****************************************************************************/ - -#include - -#include - -#include - -TEST_CASE("nucleus/basis_universal_texture_compression: transcodes all LDR paths") -{ - using nucleus::utils::BasisUniversalCompressionSettings; - using nucleus::utils::BasisUniversalFormat; - using nucleus::utils::ColourTexture; - - radix::Raster source(glm::uvec2(16), glm::u8vec4(0, 0, 0, 255)); - for (unsigned y = 0; y < source.height(); ++y) { - for (unsigned x = 0; x < source.width(); ++x) - source.pixel({ x, y }) = glm::u8vec4(x * 16, y * 16, (x + y) * 8, 255); - } - - constexpr std::array formats { - BasisUniversalFormat::ETC1S, - BasisUniversalFormat::UASTC_LDR_4x4, - BasisUniversalFormat::XUASTC_LDR_4x4, - }; - constexpr std::array targets { ColourTexture::Format::DXT1, ColourTexture::Format::ETC1 }; - for (const auto format : formats) { - for (const auto target : targets) { - INFO(nucleus::utils::basis_universal_format_name(format)); - const auto result = nucleus::utils::compress_with_basis_universal(source, - BasisUniversalCompressionSettings { - .format = format, - .target_format = target, - .quality = 50, - .effort = 0, - .generate_mipmaps = true, - }); - REQUIRE(result); - CHECK(result->texture.size() == 5); - CHECK(result->texture.front().width() == 16); - CHECK(result->texture.front().height() == 16); - CHECK(result->texture.front().format() == target); - CHECK(result->intermediate_bytes > 0); - CHECK(result->transcoded_bytes == 184); - } - } -} From 66f4e25324370e64113044e937fd43084fe71ba3 Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Fri, 21 Aug 2026 11:30:01 +0200 Subject: [PATCH 22/38] Add texture preview algorithm controls --- apps/texture_compression_benchmark/Main.qml | 80 +++++++++++++++--- .../TexturePreviewItem.cpp | 83 ++++++++++++------- .../TexturePreviewItem.h | 3 + 3 files changed, 124 insertions(+), 42 deletions(-) diff --git a/apps/texture_compression_benchmark/Main.qml b/apps/texture_compression_benchmark/Main.qml index 4e1beef8..182f577f 100644 --- a/apps/texture_compression_benchmark/Main.qml +++ b/apps/texture_compression_benchmark/Main.qml @@ -34,12 +34,26 @@ ApplicationWindow { wrapMode: Text.Wrap } - ComboBox { + GridLayout { Layout.fillWidth: true - model: preview.previewEncoders - currentIndex: preview.previewEncoder - enabled: preview.ready - onActivated: preview.previewEncoder = currentIndex + columns: width >= 760 ? 9 : width >= 500 ? 5 : 3 + columnSpacing: 6 + rowSpacing: 6 + + Repeater { + model: preview.previewEncoders + + Button { + required property int index + required property string modelData + + Layout.fillWidth: true + text: modelData + enabled: preview.ready + highlighted: preview.previewEncoder === index + onClicked: preview.previewEncoder = index + } + } } Item { @@ -89,14 +103,56 @@ ApplicationWindow { } } - Label { + RowLayout { Layout.fillWidth: true - text: preview.ready - ? (Number.isFinite(preview.previewPsnr) - ? qsTr("%1 — PSNR: %2 dB").arg(preview.previewName).arg(preview.previewPsnr.toFixed(2)) - : qsTr("%1 — PSNR: ∞").arg(preview.previewName)) - : "" - wrapMode: Text.Wrap + + Label { + Layout.fillWidth: true + text: preview.ready + ? (Number.isFinite(preview.previewPsnr) + ? qsTr("%1 — PSNR: %2 dB").arg(preview.previewName).arg(preview.previewPsnr.toFixed(2)) + : qsTr("%1 — PSNR: ∞").arg(preview.previewName)) + : "" + wrapMode: Text.Wrap + } + + Button { + text: qsTr("Description") + enabled: preview.ready + onClicked: { + if (descriptionDialogLoader.status === Loader.Ready) + descriptionDialogLoader.item.open() + else + descriptionDialogLoader.active = true + } + } + } + } + + Loader { + id: descriptionDialogLoader + + active: false + asynchronous: true + onLoaded: { + if (status === Loader.Ready) + item.open() + } + + sourceComponent: Component { + Dialog { + parent: Overlay.overlay + anchors.centerIn: parent + width: Math.min(root.width - 40, 560) + modal: true + title: preview.previewName + standardButtons: Dialog.Close + + contentItem: Label { + text: preview.previewDescription + wrapMode: Text.Wrap + } + } } } } diff --git a/apps/texture_compression_benchmark/TexturePreviewItem.cpp b/apps/texture_compression_benchmark/TexturePreviewItem.cpp index ab899fbb..5adba037 100644 --- a/apps/texture_compression_benchmark/TexturePreviewItem.cpp +++ b/apps/texture_compression_benchmark/TexturePreviewItem.cpp @@ -141,35 +141,48 @@ QImage reconstruct(gl_engine::Texture& texture, unsigned resolution, unsigned la return result; } -constexpr std::array gpu_encoders { - gl_engine::TextureCompressor::Encoder::Search, - gl_engine::TextureCompressor::Encoder::FastRange, - gl_engine::TextureCompressor::Encoder::FastSplit, - gl_engine::TextureCompressor::Encoder::FastSplitFused, - gl_engine::TextureCompressor::Encoder::FastSplitBounds, +struct GpuPreview { + const char* name; + const char* description; + gl_engine::TextureCompressor::Encoder encoder; + unsigned effort; }; -QString gpuEncoderName(gl_engine::TextureCompressor::Encoder encoder) -{ - switch (encoder) { - case gl_engine::TextureCompressor::Encoder::Search: - return QStringLiteral("GPU Search (reference)"); - case gl_engine::TextureCompressor::Encoder::FastRange: - return QStringLiteral("GPU Fast range"); - case gl_engine::TextureCompressor::Encoder::FastSplit: - return QStringLiteral("GPU Fast split"); - case gl_engine::TextureCompressor::Encoder::FastSplitFused: - return QStringLiteral("GPU Fast split fused"); - case gl_engine::TextureCompressor::Encoder::FastSplitBounds: - return QStringLiteral("GPU Fast split bounds"); - } - return {}; -} +constexpr std::array gpu_previews { { + { "Search 0", + "Tests the average block colour with every ETC1 modifier table and keeps the lowest-error result.", + gl_engine::TextureCompressor::Encoder::Search, + 0 }, + { "search 4", + "Tests five base colours around the block average with every ETC1 modifier table and keeps the lowest-error result.", + gl_engine::TextureCompressor::Encoder::Search, + 4 }, + { "search 10", + "Tests eleven base colours around the block average with every ETC1 modifier table and keeps the lowest-error result.", + gl_engine::TextureCompressor::Encoder::Search, + 10 }, + { "range", + "Derives one ETC1 base colour, modifier table, and pixel indices from the whole block's colour and brightness range.", + gl_engine::TextureCompressor::Encoder::FastRange, + 0 }, + { "split", + "Encodes vertical and horizontal two-sub-block layouts separately, then keeps the layout with the lower reconstruction error.", + gl_engine::TextureCompressor::Encoder::FastSplit, + 0 }, + { "split fused", + "Evaluates both two-sub-block layouts like split, but gathers their statistics and indices in shared shader loops.", + gl_engine::TextureCompressor::Encoder::FastSplitFused, + 0 }, + { "split bounds", + "Uses the sub-block colour bounds to choose an orientation first, then encodes only the selected layout.", + gl_engine::TextureCompressor::Encoder::FastSplitBounds, + 0 }, +} }; constexpr size_t uncompressed_preview_index = 0; constexpr size_t goofy_preview_index = 1; constexpr size_t first_gpu_preview_index = 2; -constexpr size_t preview_count = first_gpu_preview_index + gpu_encoders.size(); +constexpr size_t preview_count = first_gpu_preview_index + gpu_previews.size(); } // namespace class TexturePreviewRenderer final : public QQuickFramebufferObject::Renderer { @@ -215,7 +228,6 @@ class TexturePreviewRenderer final : public QQuickFramebufferObject::Renderer { QString generateTextures() { constexpr unsigned resolution = 512; - constexpr unsigned effort = 4; if (m_source_images.size() != tile_groups.size()) return QStringLiteral("Preview imagery is incomplete."); if (!gl_engine::TextureCompressor::is_supported()) @@ -251,16 +263,21 @@ class TexturePreviewRenderer final : public QQuickFramebufferObject::Renderer { m_preview_textures[uncompressed_preview_index] = create_texture(gl_engine::Texture::Format::SRGBA8); for (size_t layer = 0; layer < sources.size(); ++layer) m_preview_textures[uncompressed_preview_index]->upload(sources[layer], unsigned(layer)); - m_preview_results.push_back({ QStringLiteral("Uncompressed reference"), std::numeric_limits::infinity() }); + m_preview_results.push_back({ QStringLiteral("Ref"), + QStringLiteral("The original uncompressed texture array used as the visual and PSNR reference."), + std::numeric_limits::infinity() }); m_preview_textures[goofy_preview_index] = create_texture(gl_engine::Texture::Format::CompressedRGBA8); for (size_t layer = 0; layer < sources.size(); ++layer) { const auto compressed = nucleus::utils::generate_mipmapped_colour_texture(sources[layer], algorithm); m_preview_textures[goofy_preview_index]->upload(compressed, unsigned(layer)); } - m_preview_results.push_back({ QStringLiteral("Goofy CPU reference"), psnr(*m_preview_textures[goofy_preview_index]) }); + m_preview_results.push_back({ QStringLiteral("Goofy"), + QStringLiteral("CPU reference compressed by Goofy into the device's active ETC1 or DXT1 block format."), + psnr(*m_preview_textures[goofy_preview_index]) }); - for (size_t i = 0; i < gpu_encoders.size(); ++i) { + for (size_t i = 0; i < gpu_previews.size(); ++i) { + const auto& preview = gpu_previews[i]; auto& texture = m_preview_textures[first_gpu_preview_index + i]; texture = create_texture(gl_engine::Texture::Format::CompressedRGBA8); gl_engine::TextureCompressor compressor(resolution, resolution, unsigned(sources.size())); @@ -269,11 +286,12 @@ class TexturePreviewRenderer final : public QQuickFramebufferObject::Renderer { layers, { .algorithm = algorithm, - .effort = effort, - .encoder = gpu_encoders[i], + .effort = preview.effort, + .encoder = preview.encoder, .generate_mipmaps = true, })); - m_preview_results.push_back({ gpuEncoderName(gpu_encoders[i]), psnr(*texture) }); + m_preview_results.push_back( + { QString::fromLatin1(preview.name), QString::fromLatin1(preview.description), psnr(*texture) }); } return {}; } @@ -379,6 +397,11 @@ QString TexturePreviewItem::previewName() const return ready() ? m_preview_results[size_t(m_preview_encoder)].name : QString {}; } +QString TexturePreviewItem::previewDescription() const +{ + return ready() ? m_preview_results[size_t(m_preview_encoder)].description : QString {}; +} + double TexturePreviewItem::previewPsnr() const { return ready() ? m_preview_results[size_t(m_preview_encoder)].psnr : 0.0; diff --git a/apps/texture_compression_benchmark/TexturePreviewItem.h b/apps/texture_compression_benchmark/TexturePreviewItem.h index 701eedd2..e71aceeb 100644 --- a/apps/texture_compression_benchmark/TexturePreviewItem.h +++ b/apps/texture_compression_benchmark/TexturePreviewItem.h @@ -24,11 +24,13 @@ class TexturePreviewItem : public QQuickFramebufferObject { Q_PROPERTY(int previewEncoder READ previewEncoder WRITE setPreviewEncoder NOTIFY previewEncoderChanged) Q_PROPERTY(QStringList previewEncoders READ previewEncoders NOTIFY previewResultsChanged) Q_PROPERTY(QString previewName READ previewName NOTIFY previewDetailsChanged) + Q_PROPERTY(QString previewDescription READ previewDescription NOTIFY previewDetailsChanged) Q_PROPERTY(double previewPsnr READ previewPsnr NOTIFY previewDetailsChanged) public: struct PreviewResult { QString name; + QString description; double psnr = 0.0; }; @@ -42,6 +44,7 @@ class TexturePreviewItem : public QQuickFramebufferObject { void setPreviewEncoder(int value); [[nodiscard]] QStringList previewEncoders() const; [[nodiscard]] QString previewName() const; + [[nodiscard]] QString previewDescription() const; [[nodiscard]] double previewPsnr() const; signals: From baa20424426523634b7532682c0ad64a9109cced Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Fri, 21 Aug 2026 13:26:51 +0200 Subject: [PATCH 23/38] Add GPU texture compression benchmark --- .../CMakeLists.txt | 1 + .../TextureCompressionData.h | 47 ++ .../TexturePreviewItem.cpp | 37 +- gl_engine/Texture.cpp | 10 +- gl_engine/Texture.h | 2 +- gl_engine/shaders/texture_compress.vert | 10 +- unittests/CMakeLists.txt | 1 + .../CMakeLists.txt | 38 ++ .../texture_compression_benchmark/main.cpp | 453 ++++++++++++++++++ 9 files changed, 563 insertions(+), 36 deletions(-) create mode 100644 apps/texture_compression_benchmark/TextureCompressionData.h create mode 100644 unittests/texture_compression_benchmark/CMakeLists.txt create mode 100644 unittests/texture_compression_benchmark/main.cpp diff --git a/apps/texture_compression_benchmark/CMakeLists.txt b/apps/texture_compression_benchmark/CMakeLists.txt index 314ca93b..d06bf3fd 100644 --- a/apps/texture_compression_benchmark/CMakeLists.txt +++ b/apps/texture_compression_benchmark/CMakeLists.txt @@ -8,6 +8,7 @@ project(texture-compression-preview LANGUAGES CXX) qt_add_executable(texture_compression_preview main.cpp + TextureCompressionData.h TexturePreviewItem.h TexturePreviewItem.cpp ) diff --git a/apps/texture_compression_benchmark/TextureCompressionData.h b/apps/texture_compression_benchmark/TextureCompressionData.h new file mode 100644 index 00000000..7b9acc38 --- /dev/null +++ b/apps/texture_compression_benchmark/TextureCompressionData.h @@ -0,0 +1,47 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2026 Adam Celarek + * SPDX-License-Identifier: GPL-3.0-or-later + *****************************************************************************/ + +#pragma once + +#include +#include + +namespace texture_compression_data { + +struct TileGroup { + int zoom; + int y; + int x; +}; + +constexpr std::array tile_groups { { + { 17, 45448, 71496 }, + { 16, 22832, 35144 }, + { 16, 23030, 35578 }, + { 13, 2852, 4476 }, + { 14, 5702, 8808 }, + { 15, 11574, 17670 }, + { 16, 23030, 35078 }, + { 15, 11460, 17622 }, + { 14, 5752, 8656 }, + { 16, 23084, 34746 }, + { 14, 5684, 8926 }, + { 15, 11418, 17692 }, + { 16, 22956, 34570 }, + { 15, 11358, 17904 }, + { 16, 22910, 35770 }, + { 14, 5656, 8938 }, +} }; + +inline QString tile_url(const TileGroup& group, int x_offset, int y_offset) +{ + return QStringLiteral("https://gataki.cg.tuwien.ac.at/raw/basemap/tiles/%1/%2/%3.jpeg") + .arg(group.zoom) + .arg(group.y + y_offset) + .arg(group.x + x_offset); +} + +} // namespace texture_compression_data diff --git a/apps/texture_compression_benchmark/TexturePreviewItem.cpp b/apps/texture_compression_benchmark/TexturePreviewItem.cpp index 5adba037..eeca9f50 100644 --- a/apps/texture_compression_benchmark/TexturePreviewItem.cpp +++ b/apps/texture_compression_benchmark/TexturePreviewItem.cpp @@ -5,6 +5,7 @@ *****************************************************************************/ #include "TexturePreviewItem.h" +#include "TextureCompressionData.h" #include #include @@ -37,38 +38,8 @@ namespace { using Raster = radix::Raster; -struct TileGroup { - int zoom; - int y; - int x; -}; - -constexpr std::array tile_groups { { - { 17, 45448, 71496 }, - { 16, 22832, 35144 }, - { 16, 23030, 35578 }, - { 13, 2852, 4476 }, - { 14, 5702, 8808 }, - { 15, 11574, 17670 }, - { 16, 23030, 35078 }, - { 15, 11460, 17622 }, - { 14, 5752, 8656 }, - { 16, 23084, 34746 }, - { 14, 5684, 8926 }, - { 15, 11418, 17692 }, - { 16, 22956, 34570 }, - { 15, 11358, 17904 }, - { 16, 22910, 35770 }, - { 14, 5656, 8938 }, -} }; - -QString tileUrl(const TileGroup& group, int x_offset, int y_offset) -{ - return QStringLiteral("https://gataki.cg.tuwien.ac.at/raw/basemap/tiles/%1/%2/%3.jpeg") - .arg(group.zoom) - .arg(group.y + y_offset) - .arg(group.x + x_offset); -} +using texture_compression_data::tile_groups; +using texture_compression_data::tile_url; double srgbToLinear(uint8_t value) { @@ -415,7 +386,7 @@ void TexturePreviewItem::downloadImages() for (int y = 0; y < 2; ++y) { for (int x = 0; x < 2; ++x) { const auto tile_index = group_index * 4 + size_t(y * 2 + x); - const auto url = tileUrl(tile_groups[group_index], x, y); + const auto url = tile_url(tile_groups[group_index], x, y); auto* reply = m_network_manager->get(QNetworkRequest(QUrl(url))); connect(reply, &QNetworkReply::finished, this, [this, reply, tile_index, url]() { if (reply->error() == QNetworkReply::NoError) { diff --git a/gl_engine/Texture.cpp b/gl_engine/Texture.cpp index 0ad9d036..aa0f1c33 100644 --- a/gl_engine/Texture.cpp +++ b/gl_engine/Texture.cpp @@ -406,6 +406,7 @@ struct gl_engine::TextureCompressor::Impl { std::unique_ptr etc1_fast_split_fragment_program; std::unique_ptr etc1_fast_split_fused_fragment_program; std::unique_ptr etc1_fast_split_bounds_fragment_program; + std::unique_ptr checksum_fragment_program; std::unique_ptr packing_program; Impl(unsigned texture_width, unsigned texture_height, unsigned maximum_batch_size) @@ -504,6 +505,10 @@ struct gl_engine::TextureCompressor::Impl { "texture_compress.vert", ShaderCodeSource::FILE, std::vector { QStringLiteral("#define ALP_COMPRESS_ETC1"), QStringLiteral("#define ALP_COMPRESS_ETC1_SPLIT_BOUNDS") }); + checksum_fragment_program = std::make_unique("texture_compress_raster.vert", + "texture_compress.vert", + ShaderCodeSource::FILE, + std::vector { QStringLiteral("#define ALP_COMPRESS_CHECKSUM") }); packing_program = std::make_unique( "texture_compress_raster.vert", "texture_compress_pack.frag", ShaderCodeSource::FILE); @@ -517,6 +522,7 @@ struct gl_engine::TextureCompressor::Impl { etc1_fast_split_fragment_program.reset(); etc1_fast_split_fused_fragment_program.reset(); etc1_fast_split_bounds_fragment_program.reset(); + checksum_fragment_program.reset(); packing_program.reset(); if (!QOpenGLContext::currentContext()) return; @@ -663,7 +669,9 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: { auto* program = m->dxt1_fragment_program.get(); - if (settings.algorithm == nucleus::utils::ColourTexture::Format::ETC1) { + if (settings.encoder == Encoder::Checksum) { + program = m->checksum_fragment_program.get(); + } else if (settings.algorithm == nucleus::utils::ColourTexture::Format::ETC1) { if (settings.encoder == Encoder::FastRange) program = m->etc1_fast_fragment_program.get(); else if (settings.encoder == Encoder::FastSplit) diff --git a/gl_engine/Texture.h b/gl_engine/Texture.h index 68a22be4..8593425f 100644 --- a/gl_engine/Texture.h +++ b/gl_engine/Texture.h @@ -91,7 +91,7 @@ class Texture { class TextureCompressor { public: - enum class Encoder { Search, FastRange, FastSplit, FastSplitFused, FastSplitBounds }; + enum class Encoder { Search, FastRange, FastSplit, FastSplitFused, FastSplitBounds, Checksum }; struct Settings { nucleus::utils::ColourTexture::Format algorithm = nucleus::utils::ColourTexture::Format::DXT1; diff --git a/gl_engine/shaders/texture_compress.vert b/gl_engine/shaders/texture_compress.vert index 92acf307..2d1a1494 100644 --- a/gl_engine/shaders/texture_compress.vert +++ b/gl_engine/shaders/texture_compress.vert @@ -584,7 +584,15 @@ highp uvec2 compress_block(highp ivec2 block, } } -#ifdef ALP_COMPRESS_ETC1 +#ifdef ALP_COMPRESS_CHECKSUM + highp uvec2 checksum = uvec2(0u); + for (int i = 0; i < 16; ++i) { + highp uint packed_value = pixels[i].r | pixels[i].g << 8u | pixels[i].b << 16u; + checksum.x = checksum.x * 33u ^ packed_value; + checksum.y = checksum.y + packed_value * uint(i + 1); + } + return checksum; +#elif defined(ALP_COMPRESS_ETC1) #ifdef ALP_COMPRESS_ETC1_SPLIT_BOUNDS return encode_etc1_fast_split_bounds(pixels); #elif defined(ALP_COMPRESS_ETC1_SPLIT_FUSED) diff --git a/unittests/CMakeLists.txt b/unittests/CMakeLists.txt index 83f9766a..bc5a86d8 100644 --- a/unittests/CMakeLists.txt +++ b/unittests/CMakeLists.txt @@ -25,6 +25,7 @@ add_subdirectory(nucleus) if (TARGET gl_engine) add_subdirectory(gl_engine) + add_subdirectory(texture_compression_benchmark) endif() if (TARGET webgpu_engine) diff --git a/unittests/texture_compression_benchmark/CMakeLists.txt b/unittests/texture_compression_benchmark/CMakeLists.txt new file mode 100644 index 00000000..3c53cd13 --- /dev/null +++ b/unittests/texture_compression_benchmark/CMakeLists.txt @@ -0,0 +1,38 @@ +############################################################################# +# AlpineMaps.org +# Copyright (C) 2026 Adam Celarek +# SPDX-License-Identifier: GPL-3.0-or-later +############################################################################# + +project(alpine-renderer-texture-compression-benchmark LANGUAGES CXX) + +qt_add_executable(texture_compression_benchmark + main.cpp + ${CMAKE_SOURCE_DIR}/apps/texture_compression_benchmark/TextureCompressionData.h +) + +target_include_directories(texture_compression_benchmark PRIVATE + ${CMAKE_SOURCE_DIR}/apps/texture_compression_benchmark +) +target_link_libraries(texture_compression_benchmark PUBLIC gl_engine Qt::Network Qt::OpenGL) +alp_configure_target(texture_compression_benchmark) + +if (ANDROID) + add_android_openssl_libraries(texture_compression_benchmark) +endif() + +if (EMSCRIPTEN) + install( + FILES + "$/texture_compression_benchmark.js" + "$/texture_compression_benchmark.wasm" + "$/texture_compression_benchmark.html" + "$/qtloader.js" + DESTINATION "${ALP_WWW_INSTALL_DIR}/texture_compression_benchmark" + ) + install( + FILES "$/texture_compression_benchmark.worker.js" + DESTINATION "${ALP_WWW_INSTALL_DIR}/texture_compression_benchmark" + OPTIONAL + ) +endif() diff --git a/unittests/texture_compression_benchmark/main.cpp b/unittests/texture_compression_benchmark/main.cpp new file mode 100644 index 00000000..cf894e78 --- /dev/null +++ b/unittests/texture_compression_benchmark/main.cpp @@ -0,0 +1,453 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2026 Adam Celarek + * SPDX-License-Identifier: GPL-3.0-or-later + *****************************************************************************/ + +#include "TextureCompressionData.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace { +using Clock = std::chrono::steady_clock; +using Raster = radix::Raster; +using Format = nucleus::utils::ColourTexture::Format; +using Encoder = gl_engine::TextureCompressor::Encoder; + +constexpr unsigned resolution = 512; +constexpr unsigned batch_size = 4; +constexpr unsigned framebuffer_size = 32; +constexpr int warmup_batches = 2; +constexpr int measured_batches = 10; +constexpr int repetitions = 20; +constexpr uint32_t random_seed = 0x4a17c0deu; + +enum class Operation { SamplingOnly, Compression }; + +struct Workload { + std::array source_indices {}; + uint32_t sampling_seed = 0; +}; + +struct Algorithm { + std::string name; + Operation operation = Operation::Compression; + gl_engine::TextureCompressor::Settings settings; + bool checksum = false; + std::vector samples; +}; + +struct Statistics { + double mean = 0.0; + double mean_standard_deviation = 0.0; + size_t sample_count = 0; +}; + +Statistics mean_statistics(std::span samples) +{ + if (samples.empty()) + return {}; + Q_ASSERT(samples.size() == size_t(repetitions * measured_batches)); + std::array repetition_means {}; + for (int repetition = 0; repetition < repetitions; ++repetition) { + const auto begin = samples.begin() + repetition * measured_batches; + repetition_means[size_t(repetition)] + = std::accumulate(begin, begin + measured_batches, 0.0) / double(measured_batches); + } + + const auto mean = std::accumulate(repetition_means.begin(), repetition_means.end(), 0.0) + / double(repetition_means.size()); + double squared_deviations = 0.0; + for (const auto repetition_mean : repetition_means) { + const auto deviation = repetition_mean - mean; + squared_deviations += deviation * deviation; + } + const auto repetition_variance = squared_deviations / double(repetition_means.size() - 1); + const auto mean_standard_deviation = std::sqrt(repetition_variance / double(repetition_means.size())); + return { mean, mean_standard_deviation, samples.size() }; +} + +std::string gl_string(GLenum name) +{ + const auto* value = QOpenGLContext::currentContext()->functions()->glGetString(name); + return value ? reinterpret_cast(value) : "unavailable"; +} + +const char* format_name(Format format) +{ + switch (format) { + case Format::DXT1: + return "DXT1"; + case Format::ETC1: + return "ETC1"; + case Format::Uncompressed_RGBA: + return "uncompressed"; + } + return "unknown"; +} + +std::vector supported_algorithms(Format format) +{ + const auto settings = [format](Encoder encoder) { + return gl_engine::TextureCompressor::Settings { + .algorithm = format, + .effort = 0, + .encoder = encoder, + .generate_mipmaps = true, + }; + }; + + std::vector result; + result.push_back({ "sampling only", Operation::SamplingOnly, settings(Encoder::Checksum) }); + result.push_back({ "checksum", Operation::Compression, settings(Encoder::Checksum), true }); + if (format == Format::DXT1) { + result.push_back({ "DXT1", Operation::Compression, settings(Encoder::FastRange) }); + } else if (format == Format::ETC1) { + result.push_back({ "ETC1 fast range", Operation::Compression, settings(Encoder::FastRange) }); + result.push_back({ "ETC1 fast split", Operation::Compression, settings(Encoder::FastSplit) }); + result.push_back({ "ETC1 fast split fused", Operation::Compression, settings(Encoder::FastSplitFused) }); + result.push_back({ "ETC1 fast split bounds", Operation::Compression, settings(Encoder::FastSplitBounds) }); + } + return result; +} + +class BenchmarkWindow final : public QOpenGLWindow { +public: + BenchmarkWindow() + : m_downloaded_tiles(texture_compression_data::tile_groups.size() * 4) + { + resize(int(framebuffer_size), int(framebuffer_size)); + } + +protected: + void initializeGL() override + { + if (!gl_engine::TextureCompressor::is_supported()) { + fail(QStringLiteral("GPU texture compression is not supported by this context.")); + return; + } + download_data(); + } + + void paintGL() override + { + if (!m_data_ready || m_benchmark_started) + return; + m_benchmark_started = true; + const auto successful = run_benchmark(); + QTimer::singleShot(0, qApp, [successful]() { QCoreApplication::exit(successful ? EXIT_SUCCESS : EXIT_FAILURE); }); + } + +private: + void download_data() + { + m_downloads_remaining = int(m_downloaded_tiles.size()); + qInfo().noquote() << QStringLiteral("Downloading %1 source tiles once...").arg(m_downloaded_tiles.size()); + for (size_t group_index = 0; group_index < texture_compression_data::tile_groups.size(); ++group_index) { + for (int y = 0; y < 2; ++y) { + for (int x = 0; x < 2; ++x) { + const auto tile_index = group_index * 4 + size_t(y * 2 + x); + const auto url = texture_compression_data::tile_url( + texture_compression_data::tile_groups[group_index], x, y); + auto* reply = m_network_manager.get(QNetworkRequest(QUrl(url))); + connect(reply, &QNetworkReply::finished, this, [this, reply, tile_index, url]() { + if (reply->error() == QNetworkReply::NoError) { + const auto image = nucleus::utils::image_loader::rgba8(reply->readAll()); + if (image && image->size() == glm::uvec2(256u)) + m_downloaded_tiles[tile_index] = nucleus::tile::conversion::to_QImage(*image); + } + if (m_downloaded_tiles[tile_index].isNull() && m_download_error.isEmpty()) + m_download_error = QStringLiteral("Unable to download benchmark tile: %1").arg(url); + reply->deleteLater(); + if (--m_downloads_remaining == 0) + finish_downloads(); + }); + } + } + } + } + + void finish_downloads() + { + if (!m_download_error.isEmpty()) { + fail(m_download_error); + return; + } + + m_sources.clear(); + m_sources.reserve(texture_compression_data::tile_groups.size()); + for (size_t group_index = 0; group_index < texture_compression_data::tile_groups.size(); ++group_index) { + QImage stitched(int(resolution), int(resolution), QImage::Format_RGBA8888); + QPainter painter(&stitched); + for (int y = 0; y < 2; ++y) { + for (int x = 0; x < 2; ++x) { + painter.drawImage(QPoint(x * 256, y * 256), + m_downloaded_tiles[group_index * 4 + size_t(y * 2 + x)]); + } + } + painter.end(); + m_sources.push_back(nucleus::tile::conversion::to_rgba8raster(stitched)); + } + m_downloaded_tiles.clear(); + m_data_ready = true; + qInfo().noquote() << QStringLiteral("Prepared %1 stitched 512x512 textures.").arg(m_sources.size()); + update(); + } + + bool run_benchmark() + { + const auto format = gl_engine::Texture::compression_algorithm(); + auto algorithms = supported_algorithms(format); + if (algorithms.size() <= 2) { + qInfo().noquote() << QStringLiteral("No supported GPU compression algorithm was found."); + return false; + } + + gl_engine::Texture destination(gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); + destination.setParams(gl_engine::Texture::Filter::MipMapLinear, gl_engine::Texture::Filter::Linear); + destination.allocate_array(resolution, resolution, batch_size); + gl_engine::TextureCompressor compressor(resolution, resolution, batch_size); + gl_engine::Framebuffer framebuffer(gl_engine::Framebuffer::DepthFormat::None, + { gl_engine::Framebuffer::ColourFormat::RGBA8 }, + { framebuffer_size, framebuffer_size }); + gl_engine::ShaderProgram sampling_shader(R"( + void main() { + highp vec2 vertices[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); + gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0); + })", + R"( + uniform lowp sampler2DArray texture_sampler; + uniform highp uint sampling_seed; + layout(location = 0) out lowp vec4 out_color; + + highp uint hash(highp uint value) { + value ^= value >> 16u; + value *= 0x7feb352du; + value ^= value >> 15u; + value *= 0x846ca68bu; + return value ^ (value >> 16u); + } + + void main() { + highp uvec2 pixel = uvec2(gl_FragCoord.xy); + highp uint pixel_index = pixel.y * 32u + pixel.x; + highp uint random_value = hash(sampling_seed ^ pixel_index); + highp float x = float(random_value & 0xffffu) / 65535.0; + random_value = hash(random_value); + highp float y = float(random_value & 0xffffu) / 65535.0; + random_value = hash(random_value); + highp float layer = float(random_value % 4u); + random_value = hash(random_value); + highp float level = float(random_value % 10u); + lowp vec4 sampled = textureLod(texture_sampler, vec3(x, y, layer), level); + bool write_pixel = (random_value & 1u) != 0u || pixel_index == 0u; + if (!write_pixel) + discard; + out_color = sampled; + })", + gl_engine::ShaderCodeSource::PLAINTEXT); + auto sampling_geometry = gl_engine::helpers::create_screen_quad_geometry(); + const std::array destination_layers { 0, 1, 2, 3 }; + + std::mt19937 random_engine(random_seed); + std::array, repetitions> workloads; + for (auto& repetition : workloads) { + for (auto& workload : repetition) { + std::array indices; + std::iota(indices.begin(), indices.end(), 0); + std::ranges::shuffle(indices, random_engine); + std::ranges::copy_n(indices.begin(), batch_size, workload.source_indices.begin()); + workload.sampling_seed = random_engine(); + } + } + + const auto run_batch = [&](const Algorithm& algorithm, const Workload& workload) { + std::vector selected_sources; + if (algorithm.operation == Operation::Compression) { + selected_sources.reserve(batch_size); + for (const auto source_index : workload.source_indices) + selected_sources.push_back(m_sources[source_index]); + } + + const auto start = Clock::now(); + if (algorithm.operation == Operation::Compression) { + static_cast(compressor.compress( + selected_sources, destination, destination_layers, algorithm.settings)); + } + + auto* functions = QOpenGLContext::currentContext()->extraFunctions(); + framebuffer.bind(); + functions->glViewport(0, 0, framebuffer_size, framebuffer_size); + functions->glDisable(GL_BLEND); + functions->glDisable(GL_CULL_FACE); + functions->glDisable(GL_DEPTH_TEST); + functions->glDisable(GL_SCISSOR_TEST); + functions->glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + sampling_shader.bind(); + destination.bind(0); + sampling_shader.set_uniform("texture_sampler", 0); + sampling_shader.set_uniform("sampling_seed", workload.sampling_seed); + sampling_geometry.draw(); + sampling_shader.release(); + const auto pixel = framebuffer.read_colour_attachment_pixel(0, { -1.0, -1.0 }); + const auto end = Clock::now(); + m_pixel_checksum += uint64_t(pixel.x) + 3u * uint64_t(pixel.y) + 5u * uint64_t(pixel.z) + 7u * uint64_t(pixel.w); + return std::chrono::duration(end - start).count(); + }; + + // Give the sampling-only baseline valid compressed data even if it is first in the random order. + std::vector initial_sources(m_sources.begin(), m_sources.begin() + batch_size); + static_cast(compressor.compress( + initial_sources, destination, destination_layers, algorithms[1].settings)); + + qInfo().noquote() << QStringLiteral("\nTexture compression benchmark\n" + "GL vendor: %1\n" + "GL renderer: %2\n" + "GL version: %3\n" + "Compression format: %4\n" + "Random seed: 0x%5\n" + "Batch: 4 x 512x512 base-level textures, with mipmaps\n" + "Schedule: 20 repetitions, 2 warm-up + 10 measured batches per algorithm\n" + "Timer: steady-clock wall time through dependent one-pixel framebuffer readback\n") + .arg(QString::fromStdString(gl_string(GL_VENDOR)), + QString::fromStdString(gl_string(GL_RENDERER)), + QString::fromStdString(gl_string(GL_VERSION)), + QString::fromLatin1(format_name(format)), + QString::number(random_seed, 16)); + + std::vector algorithm_order(algorithms.size()); + std::iota(algorithm_order.begin(), algorithm_order.end(), 0); + for (int repetition = 0; repetition < repetitions; ++repetition) { + std::ranges::shuffle(algorithm_order, random_engine); + for (const auto algorithm_index : algorithm_order) { + auto& algorithm = algorithms[algorithm_index]; + for (int batch = 0; batch < warmup_batches; ++batch) + static_cast(run_batch(algorithm, workloads[size_t(repetition)][size_t(batch)])); + for (int batch = 0; batch < measured_batches; ++batch) { + algorithm.samples.push_back(run_batch( + algorithm, workloads[size_t(repetition)][size_t(warmup_batches + batch)])); + } + } + qInfo().noquote() + << QStringLiteral("Completed repetition %1/%2").arg(repetition + 1).arg(repetitions); + } + + const auto sampling_iterator = std::ranges::find_if( + algorithms, [](const Algorithm& algorithm) { return algorithm.operation == Operation::SamplingOnly; }); + const auto checksum_iterator = std::ranges::find_if( + algorithms, [](const Algorithm& algorithm) { return algorithm.checksum; }); + if (sampling_iterator == algorithms.end() || checksum_iterator == algorithms.end()) + return false; + + std::ostringstream report; + report << "\nAll values are milliseconds per batch. Mean SD is estimated from 20 repetition means " + "(10 batches each): sample SD / sqrt(20).\n" + << std::left << std::setw(29) << "algorithm" + << std::right << std::setw(12) << "raw mean" << std::setw(14) << "raw mean SD" + << std::setw(8) << "n" << std::setw(17) << "minus sample" + << std::setw(18) << "adjusted mean SD" << std::setw(17) << "encoding only" + << std::setw(18) << "encoding mean SD" << '\n'; + + for (const auto& algorithm : algorithms) { + std::vector sampling_subtracted; + std::vector encoding_only; + sampling_subtracted.reserve(algorithm.samples.size()); + encoding_only.reserve(algorithm.samples.size()); + for (size_t i = 0; i < algorithm.samples.size(); ++i) { + sampling_subtracted.push_back(algorithm.samples[i] - sampling_iterator->samples[i]); + if (algorithm.operation != Operation::SamplingOnly) + encoding_only.push_back(algorithm.samples[i] - checksum_iterator->samples[i]); + } + const auto raw = mean_statistics(algorithm.samples); + const auto adjusted = mean_statistics(sampling_subtracted); + const auto encoding = mean_statistics(encoding_only); + report << std::left << std::setw(29) << algorithm.name << std::right << std::fixed << std::setprecision(3) + << std::setw(12) << raw.mean << std::setw(14) << raw.mean_standard_deviation + << std::setw(8) << raw.sample_count << std::setw(17) << adjusted.mean + << std::setw(18) << adjusted.mean_standard_deviation; + if (encoding_only.empty()) { + report << std::setw(17) << "n/a" << std::setw(18) << "n/a"; + } else { + report << std::setw(17) << encoding.mean << std::setw(18) << encoding.mean_standard_deviation; + } + report << '\n'; + } + report << "Readback checksum: " << m_pixel_checksum; + for (const auto& line : QString::fromStdString(report.str()).split('\n')) + qInfo().noquote() << line; + return true; + } + + void fail(const QString& message) + { + qInfo().noquote() << message; + QTimer::singleShot(0, qApp, []() { QCoreApplication::exit(EXIT_FAILURE); }); + } + + QNetworkAccessManager m_network_manager; + std::vector m_downloaded_tiles; + std::vector m_sources; + QString m_download_error; + int m_downloads_remaining = 0; + bool m_data_ready = false; + bool m_benchmark_started = false; + uint64_t m_pixel_checksum = 0; +}; + +} // namespace + +int main(int argc, char* argv[]) +{ + QGuiApplication application(argc, argv); + QCoreApplication::setApplicationName(QStringLiteral("TextureCompressionBenchmark")); + QCoreApplication::setOrganizationName(QStringLiteral("AlpineMaps.org")); + + QSurfaceFormat format; + format.setDepthBufferSize(24); + format.setOption(QSurfaceFormat::DebugContext); + if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGL) { + format.setVersion(3, 3); + format.setProfile(QSurfaceFormat::CoreProfile); + } else { + format.setVersion(3, 0); + } + QSurfaceFormat::setDefaultFormat(format); + + BenchmarkWindow window; + window.show(); + return application.exec(); +} From 4f1f12b038c2b8b707ef4bd154d586a692529e0a Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Fri, 21 Aug 2026 21:18:51 +0200 Subject: [PATCH 24/38] Increase compression benchmark repetitions --- .../texture_compression_benchmark/main.cpp | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/unittests/texture_compression_benchmark/main.cpp b/unittests/texture_compression_benchmark/main.cpp index cf894e78..aadf964a 100644 --- a/unittests/texture_compression_benchmark/main.cpp +++ b/unittests/texture_compression_benchmark/main.cpp @@ -53,7 +53,7 @@ constexpr unsigned batch_size = 4; constexpr unsigned framebuffer_size = 32; constexpr int warmup_batches = 2; constexpr int measured_batches = 10; -constexpr int repetitions = 20; +constexpr int repetitions = 200; constexpr uint32_t random_seed = 0x4a17c0deu; enum class Operation { SamplingOnly, Compression }; @@ -341,13 +341,16 @@ class BenchmarkWindow final : public QOpenGLWindow { "Compression format: %4\n" "Random seed: 0x%5\n" "Batch: 4 x 512x512 base-level textures, with mipmaps\n" - "Schedule: 20 repetitions, 2 warm-up + 10 measured batches per algorithm\n" + "Schedule: %6 repetitions, %7 warm-up + %8 measured batches per algorithm\n" "Timer: steady-clock wall time through dependent one-pixel framebuffer readback\n") - .arg(QString::fromStdString(gl_string(GL_VENDOR)), - QString::fromStdString(gl_string(GL_RENDERER)), - QString::fromStdString(gl_string(GL_VERSION)), - QString::fromLatin1(format_name(format)), - QString::number(random_seed, 16)); + .arg(QString::fromStdString(gl_string(GL_VENDOR))) + .arg(QString::fromStdString(gl_string(GL_RENDERER))) + .arg(QString::fromStdString(gl_string(GL_VERSION))) + .arg(QString::fromLatin1(format_name(format))) + .arg(QString::number(random_seed, 16)) + .arg(repetitions) + .arg(warmup_batches) + .arg(measured_batches); std::vector algorithm_order(algorithms.size()); std::iota(algorithm_order.begin(), algorithm_order.end(), 0); @@ -374,8 +377,9 @@ class BenchmarkWindow final : public QOpenGLWindow { return false; std::ostringstream report; - report << "\nAll values are milliseconds per batch. Mean SD is estimated from 20 repetition means " - "(10 batches each): sample SD / sqrt(20).\n" + report << "\nAll values are milliseconds per batch. Mean SD is estimated from " << repetitions + << " repetition means (" << measured_batches << " batches each): sample SD / sqrt(" + << repetitions << ").\n" << std::left << std::setw(29) << "algorithm" << std::right << std::setw(12) << "raw mean" << std::setw(14) << "raw mean SD" << std::setw(8) << "n" << std::setw(17) << "minus sample" From 3c84b95baa87d5f80a6e7ad0748b39f6feba97fd Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Fri, 21 Aug 2026 23:42:38 +0200 Subject: [PATCH 25/38] Add texture compression quality metrics --- gl_engine/ShaderProgram.cpp | 5 +- .../texture_compression_benchmark/main.cpp | 254 +++++++++++++++++- 2 files changed, 257 insertions(+), 2 deletions(-) diff --git a/gl_engine/ShaderProgram.cpp b/gl_engine/ShaderProgram.cpp index ec4e792d..243768d0 100644 --- a/gl_engine/ShaderProgram.cpp +++ b/gl_engine/ShaderProgram.cpp @@ -249,7 +249,10 @@ void ShaderProgram::set_uniform(const std::string& name, int value) void ShaderProgram::set_uniform(const std::string& name, unsigned value) { - set_uniform_template(name, value); + if (!m_cached_uniforms.contains(name)) + m_cached_uniforms[name] = m_q_shader_program->uniformLocation(name.c_str()); + + QOpenGLContext::currentContext()->extraFunctions()->glUniform1ui(m_cached_uniforms.at(name), value); } void ShaderProgram::set_uniform(const std::string& name, float value) diff --git a/unittests/texture_compression_benchmark/main.cpp b/unittests/texture_compression_benchmark/main.cpp index aadf964a..d90c9bd2 100644 --- a/unittests/texture_compression_benchmark/main.cpp +++ b/unittests/texture_compression_benchmark/main.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -55,6 +56,8 @@ constexpr int warmup_batches = 2; constexpr int measured_batches = 10; constexpr int repetitions = 200; constexpr uint32_t random_seed = 0x4a17c0deu; +constexpr int ssim_window_size = 11; +constexpr int ssim_window_radius = ssim_window_size / 2; enum class Operation { SamplingOnly, Compression }; @@ -63,12 +66,31 @@ struct Workload { uint32_t sampling_seed = 0; }; +struct QualityMetrics { + double psnr = 0.0; + double ssim = 0.0; +}; + struct Algorithm { std::string name; Operation operation = Operation::Compression; gl_engine::TextureCompressor::Settings settings; bool checksum = false; std::vector samples; + std::optional quality; +}; + +struct QualityReference { + std::vector ssim_luma; + std::vector ssim_mean; + std::vector ssim_second_moment; +}; + +struct QualityAccumulator { + double squared_error = 0.0; + uint64_t channel_count = 0; + double ssim_sum = 0.0; + uint64_t ssim_count = 0; }; struct Statistics { @@ -101,6 +123,158 @@ Statistics mean_statistics(std::span samples) return { mean, mean_standard_deviation, samples.size() }; } +double srgb_to_linear(uint8_t value) +{ + const auto normalised = double(value) / 255.0; + if (normalised <= 0.04045) + return normalised / 12.92; + return std::pow((normalised + 0.055) / 1.055, 2.4); +} + +double linear_to_srgb(double value) +{ + if (value <= 0.0031308) + return 12.92 * value; + return 1.055 * std::pow(value, 1.0 / 2.4) - 0.055; +} + +std::array ssim_kernel() +{ + constexpr double sigma = 1.5; + std::array kernel {}; + double sum = 0.0; + for (int i = -ssim_window_radius; i <= ssim_window_radius; ++i) { + const auto value = std::exp(-double(i * i) / (2.0 * sigma * sigma)); + kernel[size_t(i + ssim_window_radius)] = value; + sum += value; + } + for (auto& value : kernel) + value /= sum; + return kernel; +} + +std::vector gaussian_filter_valid(std::span input, int width, int height) +{ + Q_ASSERT(width >= ssim_window_size && height >= ssim_window_size); + Q_ASSERT(input.size() == size_t(width * height)); + static const auto kernel = ssim_kernel(); + const auto horizontal_width = width - 2 * ssim_window_radius; + const auto output_height = height - 2 * ssim_window_radius; + std::vector horizontal(size_t(horizontal_width * height)); + for (int y = 0; y < height; ++y) { + for (int x = ssim_window_radius; x < width - ssim_window_radius; ++x) { + double value = 0.0; + for (int offset = -ssim_window_radius; offset <= ssim_window_radius; ++offset) + value += kernel[size_t(offset + ssim_window_radius)] * input[size_t(y * width + x + offset)]; + horizontal[size_t(y * horizontal_width + x - ssim_window_radius)] = value; + } + } + + std::vector output(size_t(horizontal_width * output_height)); + for (int y = ssim_window_radius; y < height - ssim_window_radius; ++y) { + for (int x = 0; x < horizontal_width; ++x) { + double value = 0.0; + for (int offset = -ssim_window_radius; offset <= ssim_window_radius; ++offset) { + value += kernel[size_t(offset + ssim_window_radius)] + * horizontal[size_t((y + offset) * horizontal_width + x)]; + } + output[size_t((y - ssim_window_radius) * horizontal_width + x)] = value; + } + } + return output; +} + +QualityReference make_quality_reference(const Raster& source) +{ + QualityReference result; + result.ssim_luma.resize(size_t(source.width() * source.height())); + std::vector squared_luma(result.ssim_luma.size()); + for (unsigned y = 0; y < source.height(); ++y) { + for (unsigned x = 0; x < source.width(); ++x) { + const auto pixel = source.pixel({ x, y }); + const auto luma = 0.2126 * double(pixel.x) / 255.0 + + 0.7152 * double(pixel.y) / 255.0 + + 0.0722 * double(pixel.z) / 255.0; + const auto index = size_t(y * source.width() + x); + result.ssim_luma[index] = luma; + squared_luma[index] = luma * luma; + } + } + result.ssim_mean = gaussian_filter_valid(result.ssim_luma, int(source.width()), int(source.height())); + result.ssim_second_moment = gaussian_filter_valid(squared_luma, int(source.width()), int(source.height())); + return result; +} + +void accumulate_quality(QualityAccumulator& accumulator, + const QImage& reconstructed, + const Raster& source, + const QualityReference& reference) +{ + Q_ASSERT(reconstructed.size() == QSize(int(source.width()), int(source.height()))); + std::vector reconstructed_luma(size_t(source.width() * source.height())); + std::vector squared_luma(reconstructed_luma.size()); + std::vector cross_luma(reconstructed_luma.size()); + for (unsigned y = 0; y < source.height(); ++y) { + for (unsigned x = 0; x < source.width(); ++x) { + const auto actual = reconstructed.pixel(int(x), int(y)); + const auto expected = source.pixel({ x, y }); + const std::array actual_linear { + qRed(actual) / 255.0, + qGreen(actual) / 255.0, + qBlue(actual) / 255.0, + }; + const std::array expected_linear { + srgb_to_linear(expected.x), + srgb_to_linear(expected.y), + srgb_to_linear(expected.z), + }; + for (size_t channel = 0; channel < actual_linear.size(); ++channel) { + const auto difference = actual_linear[channel] - expected_linear[channel]; + accumulator.squared_error += difference * difference; + } + accumulator.channel_count += actual_linear.size(); + + const auto luma = 0.2126 * linear_to_srgb(actual_linear[0]) + + 0.7152 * linear_to_srgb(actual_linear[1]) + + 0.0722 * linear_to_srgb(actual_linear[2]); + const auto index = size_t(y * source.width() + x); + reconstructed_luma[index] = luma; + squared_luma[index] = luma * luma; + cross_luma[index] = luma * reference.ssim_luma[index]; + } + } + + const auto actual_mean = gaussian_filter_valid(reconstructed_luma, int(source.width()), int(source.height())); + const auto actual_second_moment = gaussian_filter_valid(squared_luma, int(source.width()), int(source.height())); + const auto cross_moment = gaussian_filter_valid(cross_luma, int(source.width()), int(source.height())); + Q_ASSERT(actual_mean.size() == reference.ssim_mean.size()); + constexpr double c1 = 0.01 * 0.01; + constexpr double c2 = 0.03 * 0.03; + for (size_t i = 0; i < actual_mean.size(); ++i) { + const auto reference_variance + = std::max(0.0, reference.ssim_second_moment[i] - reference.ssim_mean[i] * reference.ssim_mean[i]); + const auto actual_variance + = std::max(0.0, actual_second_moment[i] - actual_mean[i] * actual_mean[i]); + const auto covariance = cross_moment[i] - reference.ssim_mean[i] * actual_mean[i]; + const auto luminance = 2.0 * reference.ssim_mean[i] * actual_mean[i] + c1; + const auto contrast_structure = 2.0 * covariance + c2; + const auto denominator = (reference.ssim_mean[i] * reference.ssim_mean[i] + actual_mean[i] * actual_mean[i] + c1) + * (reference_variance + actual_variance + c2); + accumulator.ssim_sum += luminance * contrast_structure / denominator; + } + accumulator.ssim_count += actual_mean.size(); +} + +QualityMetrics quality_metrics(const QualityAccumulator& accumulator) +{ + Q_ASSERT(accumulator.channel_count > 0 && accumulator.ssim_count > 0); + const auto mse = accumulator.squared_error / double(accumulator.channel_count); + return { + mse == 0.0 ? std::numeric_limits::infinity() : 10.0 * std::log10(1.0 / mse), + accumulator.ssim_sum / double(accumulator.ssim_count), + }; +} + std::string gl_string(GLenum name) { const auto* value = QOpenGLContext::currentContext()->functions()->glGetString(name); @@ -369,6 +543,74 @@ class BenchmarkWindow final : public QOpenGLWindow { << QStringLiteral("Completed repetition %1/%2").arg(repetition + 1).arg(repetitions); } + qInfo().noquote() << QStringLiteral("Computing untimed 512x512 quality metrics..."); + std::vector quality_references; + quality_references.reserve(m_sources.size()); + for (const auto& source : m_sources) + quality_references.push_back(make_quality_reference(source)); + + gl_engine::Framebuffer quality_framebuffer(gl_engine::Framebuffer::DepthFormat::None, + { gl_engine::Framebuffer::ColourFormat::RGBA8 }, + { resolution, resolution }); + gl_engine::ShaderProgram quality_shader(R"( + out highp vec2 texcoords; + void main() { + highp vec2 vertices[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); + gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0); + texcoords = 0.5 * gl_Position.xy + vec2(0.5); + })", + R"( + uniform lowp sampler2DArray texture_sampler; + uniform highp float texture_layer; + in highp vec2 texcoords; + out lowp vec4 out_color; + void main() { + out_color = textureLod(texture_sampler, vec3(texcoords.x, 1.0 - texcoords.y, texture_layer), 0.0); + })", + gl_engine::ShaderCodeSource::PLAINTEXT); + + const auto reconstruct = [&](unsigned layer) { + auto* functions = QOpenGLContext::currentContext()->extraFunctions(); + quality_framebuffer.bind(); + functions->glViewport(0, 0, resolution, resolution); + functions->glDisable(GL_BLEND); + functions->glDisable(GL_CULL_FACE); + functions->glDisable(GL_DEPTH_TEST); + functions->glDisable(GL_SCISSOR_TEST); + functions->glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + quality_shader.bind(); + destination.bind(0); + quality_shader.set_uniform("texture_sampler", 0); + quality_shader.set_uniform("texture_layer", float(layer)); + sampling_geometry.draw(); + quality_shader.release(); + return quality_framebuffer.read_colour_attachment(0); + }; + + for (auto& algorithm : algorithms) { + if (algorithm.operation != Operation::Compression || algorithm.checksum) + continue; + QualityAccumulator accumulator; + for (size_t source_offset = 0; source_offset < m_sources.size(); source_offset += batch_size) { + Q_ASSERT(source_offset + batch_size <= m_sources.size()); + std::vector selected_sources; + selected_sources.reserve(batch_size); + for (size_t layer = 0; layer < batch_size; ++layer) + selected_sources.push_back(m_sources[source_offset + layer]); + static_cast(compressor.compress( + selected_sources, destination, destination_layers, algorithm.settings)); + for (unsigned layer = 0; layer < batch_size; ++layer) { + accumulate_quality(accumulator, + reconstruct(layer), + selected_sources[layer], + quality_references[source_offset + layer]); + } + } + algorithm.quality = quality_metrics(accumulator); + qInfo().noquote() << QStringLiteral("Completed quality metrics for %1").arg(QString::fromStdString(algorithm.name)); + } + gl_engine::Framebuffer::unbind(); + const auto sampling_iterator = std::ranges::find_if( algorithms, [](const Algorithm& algorithm) { return algorithm.operation == Operation::SamplingOnly; }); const auto checksum_iterator = std::ranges::find_if( @@ -380,11 +622,14 @@ class BenchmarkWindow final : public QOpenGLWindow { report << "\nAll values are milliseconds per batch. Mean SD is estimated from " << repetitions << " repetition means (" << measured_batches << " batches each): sample SD / sqrt(" << repetitions << ").\n" + << "Quality is an untimed pass over all " << m_sources.size() + << " source textures at mip level 0 (512x512). PSNR uses linear RGB; SSIM uses sRGB luma.\n" << std::left << std::setw(29) << "algorithm" << std::right << std::setw(12) << "raw mean" << std::setw(14) << "raw mean SD" << std::setw(8) << "n" << std::setw(17) << "minus sample" << std::setw(18) << "adjusted mean SD" << std::setw(17) << "encoding only" - << std::setw(18) << "encoding mean SD" << '\n'; + << std::setw(18) << "encoding mean SD" << std::setw(13) << "PSNR (dB)" + << std::setw(11) << "SSIM" << '\n'; for (const auto& algorithm : algorithms) { std::vector sampling_subtracted; @@ -408,6 +653,13 @@ class BenchmarkWindow final : public QOpenGLWindow { } else { report << std::setw(17) << encoding.mean << std::setw(18) << encoding.mean_standard_deviation; } + if (algorithm.quality) { + report << std::setw(13) << std::setprecision(3) << algorithm.quality->psnr + << std::setw(11) << std::setprecision(6) << algorithm.quality->ssim + << std::setprecision(3); + } else { + report << std::setw(13) << "n/a" << std::setw(11) << "n/a"; + } report << '\n'; } report << "Readback checksum: " << m_pixel_checksum; From 4824e78b1ea8adb0e7aa8364153a91c57b58c6cf Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Sat, 22 Aug 2026 00:03:17 +0200 Subject: [PATCH 26/38] Remove redundant texture compression encoders --- .../TexturePreviewItem.cpp | 12 +- gl_engine/Texture.cpp | 18 +-- gl_engine/Texture.h | 2 +- gl_engine/shaders/texture_compress.vert | 141 ------------------ .../texture_compression_benchmark/main.cpp | 4 +- 5 files changed, 5 insertions(+), 172 deletions(-) diff --git a/apps/texture_compression_benchmark/TexturePreviewItem.cpp b/apps/texture_compression_benchmark/TexturePreviewItem.cpp index eeca9f50..91bc15bc 100644 --- a/apps/texture_compression_benchmark/TexturePreviewItem.cpp +++ b/apps/texture_compression_benchmark/TexturePreviewItem.cpp @@ -119,7 +119,7 @@ struct GpuPreview { unsigned effort; }; -constexpr std::array gpu_previews { { +constexpr std::array gpu_previews { { { "Search 0", "Tests the average block colour with every ETC1 modifier table and keeps the lowest-error result.", gl_engine::TextureCompressor::Encoder::Search, @@ -132,16 +132,8 @@ constexpr std::array gpu_previews { { "Tests eleven base colours around the block average with every ETC1 modifier table and keeps the lowest-error result.", gl_engine::TextureCompressor::Encoder::Search, 10 }, - { "range", - "Derives one ETC1 base colour, modifier table, and pixel indices from the whole block's colour and brightness range.", - gl_engine::TextureCompressor::Encoder::FastRange, - 0 }, - { "split", - "Encodes vertical and horizontal two-sub-block layouts separately, then keeps the layout with the lower reconstruction error.", - gl_engine::TextureCompressor::Encoder::FastSplit, - 0 }, { "split fused", - "Evaluates both two-sub-block layouts like split, but gathers their statistics and indices in shared shader loops.", + "Evaluates vertical and horizontal two-sub-block layouts while gathering their statistics and indices in shared shader loops.", gl_engine::TextureCompressor::Encoder::FastSplitFused, 0 }, { "split bounds", diff --git a/gl_engine/Texture.cpp b/gl_engine/Texture.cpp index aa0f1c33..56c7d79c 100644 --- a/gl_engine/Texture.cpp +++ b/gl_engine/Texture.cpp @@ -402,8 +402,6 @@ struct gl_engine::TextureCompressor::Impl { GLuint packing_renderbuffer = 0; std::unique_ptr dxt1_fragment_program; std::unique_ptr etc1_fragment_program; - std::unique_ptr etc1_fast_fragment_program; - std::unique_ptr etc1_fast_split_fragment_program; std::unique_ptr etc1_fast_split_fused_fragment_program; std::unique_ptr etc1_fast_split_bounds_fragment_program; std::unique_ptr checksum_fragment_program; @@ -489,14 +487,6 @@ struct gl_engine::TextureCompressor::Impl { "texture_compress.vert", ShaderCodeSource::FILE, std::vector { QStringLiteral("#define ALP_COMPRESS_ETC1") }); - etc1_fast_fragment_program = std::make_unique("texture_compress_raster.vert", - "texture_compress.vert", - ShaderCodeSource::FILE, - std::vector { QStringLiteral("#define ALP_COMPRESS_ETC1"), QStringLiteral("#define ALP_COMPRESS_ETC1_FAST") }); - etc1_fast_split_fragment_program = std::make_unique("texture_compress_raster.vert", - "texture_compress.vert", - ShaderCodeSource::FILE, - std::vector { QStringLiteral("#define ALP_COMPRESS_ETC1"), QStringLiteral("#define ALP_COMPRESS_ETC1_SPLIT") }); etc1_fast_split_fused_fragment_program = std::make_unique("texture_compress_raster.vert", "texture_compress.vert", ShaderCodeSource::FILE, @@ -518,8 +508,6 @@ struct gl_engine::TextureCompressor::Impl { { dxt1_fragment_program.reset(); etc1_fragment_program.reset(); - etc1_fast_fragment_program.reset(); - etc1_fast_split_fragment_program.reset(); etc1_fast_split_fused_fragment_program.reset(); etc1_fast_split_bounds_fragment_program.reset(); checksum_fragment_program.reset(); @@ -672,11 +660,7 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: if (settings.encoder == Encoder::Checksum) { program = m->checksum_fragment_program.get(); } else if (settings.algorithm == nucleus::utils::ColourTexture::Format::ETC1) { - if (settings.encoder == Encoder::FastRange) - program = m->etc1_fast_fragment_program.get(); - else if (settings.encoder == Encoder::FastSplit) - program = m->etc1_fast_split_fragment_program.get(); - else if (settings.encoder == Encoder::FastSplitFused) + if (settings.encoder == Encoder::FastSplitFused) program = m->etc1_fast_split_fused_fragment_program.get(); else if (settings.encoder == Encoder::FastSplitBounds) program = m->etc1_fast_split_bounds_fragment_program.get(); diff --git a/gl_engine/Texture.h b/gl_engine/Texture.h index 8593425f..204d784a 100644 --- a/gl_engine/Texture.h +++ b/gl_engine/Texture.h @@ -91,7 +91,7 @@ class Texture { class TextureCompressor { public: - enum class Encoder { Search, FastRange, FastSplit, FastSplitFused, FastSplitBounds, Checksum }; + enum class Encoder { Search, Dxt1, FastSplitFused, FastSplitBounds, Checksum }; struct Settings { nucleus::utils::ColourTexture::Format algorithm = nucleus::utils::ColourTexture::Format::DXT1; diff --git a/gl_engine/shaders/texture_compress.vert b/gl_engine/shaders/texture_compress.vert index 2d1a1494..79dcef79 100644 --- a/gl_engine/shaders/texture_compress.vert +++ b/gl_engine/shaders/texture_compress.vert @@ -132,47 +132,6 @@ highp int table_for_range(highp int range) return 7; } -highp uvec2 encode_etc1_fast(highp uvec3 pixels[16]) -{ - highp uvec3 minimum_colour = uvec3(255u); - highp uvec3 maximum_colour = uvec3(0u); - highp uvec3 sum = uvec3(0u); - for (int i = 0; i < 16; ++i) { - minimum_colour = min(minimum_colour, pixels[i]); - maximum_colour = max(maximum_colour, pixels[i]); - sum += pixels[i]; - } - - highp int minimum_brightness = brightness(minimum_colour); - highp int maximum_brightness = brightness(maximum_colour); - highp int range = max(8, maximum_brightness - minimum_brightness); - highp int middle = (minimum_brightness + maximum_brightness + 1) / 2; - highp ivec3 average = ivec3((sum + 8u) / 16u); - highp int correction = middle - brightness(uvec3(average)); - highp ivec3 adjusted = clamp(average + ivec3(correction), ivec3(0), ivec3(255)); - highp uvec3 base5 = (uvec3(adjusted) * 31u + 127u) / 255u; - - highp int table = table_for_range(range); - highp int threshold = (range * 3 + 4) / 8; - highp uint indices = 0u; - for (int y = 0; y < 4; ++y) { - for (int x = 0; x < 4; ++x) { - highp int pixel_index = y * 4 + x; - highp int delta = brightness(pixels[pixel_index]) - middle; - highp uint selected = uint(abs(delta) >= threshold); - if (delta < 0) - selected += 2u; - highp uint bit_position = uint(x * 4 + y); - indices |= (selected & 1u) << bit_position; - indices |= (selected >> 1u) << (bit_position + 16u); - } - } - - highp uint control = uint(table) << 5u | uint(table) << 2u | 2u; - highp uint header = base5.r << 3u | base5.g << 11u | base5.b << 19u | control << 24u; - return uvec2(header, byte_swap(indices)); -} - struct FastEtc1Subblock { highp ivec3 colour; highp int table; @@ -180,102 +139,6 @@ struct FastEtc1Subblock { highp int threshold; }; -struct FastEtc1Block { - highp uvec2 encoded; - highp uint error; -}; - -FastEtc1Subblock fast_subblock_parameters(highp uvec3 pixels[16], bool flip, highp int subblock) -{ - highp uvec3 minimum_colour = uvec3(255u); - highp uvec3 maximum_colour = uvec3(0u); - highp uvec3 sum = uvec3(0u); - for (int i = 0; i < 16; ++i) { - highp int x = i & 3; - highp int y = i >> 2; - bool belongs_to_first = flip ? y < 2 : x < 2; - if (belongs_to_first != (subblock == 0)) - continue; - minimum_colour = min(minimum_colour, pixels[i]); - maximum_colour = max(maximum_colour, pixels[i]); - sum += pixels[i]; - } - - highp int minimum_brightness = brightness(minimum_colour); - highp int maximum_brightness = brightness(maximum_colour); - highp int range = max(8, maximum_brightness - minimum_brightness); - highp int middle = (minimum_brightness + maximum_brightness + 1) / 2; - highp ivec3 average = ivec3((sum + 4u) / 8u); - highp int correction = middle - brightness(uvec3(average)); - highp ivec3 colour = clamp(average + ivec3(correction), ivec3(0), ivec3(255)); - return FastEtc1Subblock(colour, table_for_range(range), middle, (range * 3 + 4) / 8); -} - -FastEtc1Block encode_etc1_split_orientation(highp uvec3 pixels[16], bool flip) -{ - FastEtc1Subblock first = fast_subblock_parameters(pixels, flip, 0); - FastEtc1Subblock second = fast_subblock_parameters(pixels, flip, 1); - highp uvec3 first_base5 = (uvec3(first.colour) * 31u + 127u) / 255u; - highp uvec3 second_base5 = (uvec3(second.colour) * 31u + 127u) / 255u; - highp ivec3 base_delta = ivec3(second_base5) - ivec3(first_base5); - bool differential = all(greaterThanEqual(base_delta, ivec3(-4))) && all(lessThanEqual(base_delta, ivec3(3))); - - highp uint header; - highp ivec3 first_decoded; - highp ivec3 second_decoded; - if (differential) { - highp uvec3 delta3 = uvec3(base_delta) & 7u; - header = first_base5.r << 3u | delta3.r - | first_base5.g << 11u | delta3.g << 8u - | first_base5.b << 19u | delta3.b << 16u; - first_decoded = ivec3((first_base5 << 3u) | (first_base5 >> 2u)); - second_decoded = ivec3((second_base5 << 3u) | (second_base5 >> 2u)); - } else { - highp uvec3 first_base4 = (uvec3(first.colour) * 15u + 127u) / 255u; - highp uvec3 second_base4 = (uvec3(second.colour) * 15u + 127u) / 255u; - header = first_base4.r << 4u | second_base4.r - | first_base4.g << 12u | second_base4.g << 8u - | first_base4.b << 20u | second_base4.b << 16u; - first_decoded = ivec3((first_base4 << 4u) | first_base4); - second_decoded = ivec3((second_base4 << 4u) | second_base4); - } - - highp uint indices = 0u; - highp uint total_error = 0u; - for (int y = 0; y < 4; ++y) { - for (int x = 0; x < 4; ++x) { - highp int pixel_index = y * 4 + x; - bool use_second = flip ? y >= 2 : x >= 2; - highp int middle = use_second ? second.middle : first.middle; - highp int threshold = use_second ? second.threshold : first.threshold; - highp int table = use_second ? second.table : first.table; - highp ivec3 decoded = use_second ? second_decoded : first_decoded; - highp int brightness_delta = brightness(pixels[pixel_index]) - middle; - highp uint selected = uint(abs(brightness_delta) >= threshold); - if (brightness_delta < 0) - selected += 2u; - - highp ivec3 reconstructed = clamp(decoded + ivec3(modifier(table, int(selected))), ivec3(0), ivec3(255)); - highp ivec3 colour_delta = ivec3(pixels[pixel_index]) - reconstructed; - total_error += uint(colour_delta.x * colour_delta.x + colour_delta.y * colour_delta.y + colour_delta.z * colour_delta.z); - highp uint bit_position = uint(x * 4 + y); - indices |= (selected & 1u) << bit_position; - indices |= (selected >> 1u) << (bit_position + 16u); - } - } - - highp uint control = uint(first.table) << 5u | uint(second.table) << 2u - | (differential ? 2u : 0u) | (flip ? 1u : 0u); - return FastEtc1Block(uvec2(header | control << 24u, byte_swap(indices)), total_error); -} - -highp uvec2 encode_etc1_fast_split(highp uvec3 pixels[16]) -{ - FastEtc1Block vertical = encode_etc1_split_orientation(pixels, false); - FastEtc1Block horizontal = encode_etc1_split_orientation(pixels, true); - return horizontal.error < vertical.error ? horizontal.encoded : vertical.encoded; -} - FastEtc1Subblock fast_subblock_from_statistics(highp uvec3 minimum_colour, highp uvec3 maximum_colour, highp uvec3 sum) @@ -597,10 +460,6 @@ highp uvec2 compress_block(highp ivec2 block, return encode_etc1_fast_split_bounds(pixels); #elif defined(ALP_COMPRESS_ETC1_SPLIT_FUSED) return encode_etc1_fast_split_fused(pixels); -#elif defined(ALP_COMPRESS_ETC1_SPLIT) - return encode_etc1_fast_split(pixels); -#elif defined(ALP_COMPRESS_ETC1_FAST) - return encode_etc1_fast(pixels); #else return encode_etc1(pixels); #endif diff --git a/unittests/texture_compression_benchmark/main.cpp b/unittests/texture_compression_benchmark/main.cpp index d90c9bd2..9410cae7 100644 --- a/unittests/texture_compression_benchmark/main.cpp +++ b/unittests/texture_compression_benchmark/main.cpp @@ -309,10 +309,8 @@ std::vector supported_algorithms(Format format) result.push_back({ "sampling only", Operation::SamplingOnly, settings(Encoder::Checksum) }); result.push_back({ "checksum", Operation::Compression, settings(Encoder::Checksum), true }); if (format == Format::DXT1) { - result.push_back({ "DXT1", Operation::Compression, settings(Encoder::FastRange) }); + result.push_back({ "DXT1", Operation::Compression, settings(Encoder::Dxt1) }); } else if (format == Format::ETC1) { - result.push_back({ "ETC1 fast range", Operation::Compression, settings(Encoder::FastRange) }); - result.push_back({ "ETC1 fast split", Operation::Compression, settings(Encoder::FastSplit) }); result.push_back({ "ETC1 fast split fused", Operation::Compression, settings(Encoder::FastSplitFused) }); result.push_back({ "ETC1 fast split bounds", Operation::Compression, settings(Encoder::FastSplitBounds) }); } From 4a7608f7d7d9c845b6fb20deac9495ce2c9f8247 Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Sat, 22 Aug 2026 06:53:44 +0200 Subject: [PATCH 27/38] Add ETC1 modifier selection variants --- .../TexturePreviewItem.cpp | 34 ++++- gl_engine/Texture.cpp | 80 +++++++++--- gl_engine/Texture.h | 16 ++- gl_engine/shaders/texture_compress.vert | 118 ++++++++++++++++-- .../texture_compression_benchmark/main.cpp | 18 +++ 5 files changed, 234 insertions(+), 32 deletions(-) diff --git a/apps/texture_compression_benchmark/TexturePreviewItem.cpp b/apps/texture_compression_benchmark/TexturePreviewItem.cpp index 91bc15bc..2307ce1e 100644 --- a/apps/texture_compression_benchmark/TexturePreviewItem.cpp +++ b/apps/texture_compression_benchmark/TexturePreviewItem.cpp @@ -119,7 +119,7 @@ struct GpuPreview { unsigned effort; }; -constexpr std::array gpu_previews { { +constexpr std::array gpu_previews { { { "Search 0", "Tests the average block colour with every ETC1 modifier table and keeps the lowest-error result.", gl_engine::TextureCompressor::Encoder::Search, @@ -136,10 +136,42 @@ constexpr std::array gpu_previews { { "Evaluates vertical and horizontal two-sub-block layouts while gathering their statistics and indices in shared shader loops.", gl_engine::TextureCompressor::Encoder::FastSplitFused, 0 }, + { "split fused projection", + "Selects modifiers from the RGB projection relative to each decoded base colour.", + gl_engine::TextureCompressor::Encoder::FastSplitFusedProjection, + 0 }, + { "split fused projection + clamp", + "Uses RGB projection, with exact palette evaluation when a modifier would clamp a colour channel.", + gl_engine::TextureCompressor::Encoder::FastSplitFusedProjectionClamped, + 0 }, + { "split fused exact", + "Evaluates all four colours in the selected modifier table for every pixel.", + gl_engine::TextureCompressor::Encoder::FastSplitFusedExact, + 0 }, + { "split fused exact + tables", + "Searches all modifier tables and evaluates all four reconstructed colours for every pixel.", + gl_engine::TextureCompressor::Encoder::FastSplitFusedExactTableSearch, + 0 }, { "split bounds", "Uses the sub-block colour bounds to choose an orientation first, then encodes only the selected layout.", gl_engine::TextureCompressor::Encoder::FastSplitBounds, 0 }, + { "split bounds projection", + "Chooses the orientation from colour bounds, then selects modifiers from decoded-base RGB projection.", + gl_engine::TextureCompressor::Encoder::FastSplitBoundsProjection, + 0 }, + { "split bounds projection + clamp", + "Uses RGB projection after bounds selection, with exact evaluation when a modifier would clamp.", + gl_engine::TextureCompressor::Encoder::FastSplitBoundsProjectionClamped, + 0 }, + { "split bounds exact", + "Chooses the orientation from colour bounds, then evaluates all four table colours per pixel.", + gl_engine::TextureCompressor::Encoder::FastSplitBoundsExact, + 0 }, + { "split bounds exact + tables", + "Chooses the orientation from colour bounds, then searches every modifier table with exact pixel evaluation.", + gl_engine::TextureCompressor::Encoder::FastSplitBoundsExactTableSearch, + 0 }, } }; constexpr size_t uncompressed_preview_index = 0; diff --git a/gl_engine/Texture.cpp b/gl_engine/Texture.cpp index 56c7d79c..72eb9e7e 100644 --- a/gl_engine/Texture.cpp +++ b/gl_engine/Texture.cpp @@ -385,6 +385,46 @@ float gl_engine::Texture::max_anisotropy() struct gl_engine::TextureCompressor::Impl { static constexpr unsigned max_shader_mip_levels = 16; + struct Etc1FastProgram { + Encoder encoder = Encoder::FastSplitFused; + std::unique_ptr program; + }; + + struct Etc1FastProgramDefinition { + Encoder encoder; + const char* algorithm_define; + const char* index_define; + }; + + static constexpr std::array etc1_fast_program_definitions { + Etc1FastProgramDefinition { Encoder::FastSplitFused, "#define ALP_COMPRESS_ETC1_SPLIT_FUSED", nullptr }, + Etc1FastProgramDefinition { Encoder::FastSplitFusedProjection, + "#define ALP_COMPRESS_ETC1_SPLIT_FUSED", + "#define ALP_ETC1_INDEX_PROJECTION" }, + Etc1FastProgramDefinition { Encoder::FastSplitFusedProjectionClamped, + "#define ALP_COMPRESS_ETC1_SPLIT_FUSED", + "#define ALP_ETC1_INDEX_PROJECTION_CLAMPED" }, + Etc1FastProgramDefinition { Encoder::FastSplitFusedExact, + "#define ALP_COMPRESS_ETC1_SPLIT_FUSED", + "#define ALP_ETC1_INDEX_EXACT" }, + Etc1FastProgramDefinition { Encoder::FastSplitFusedExactTableSearch, + "#define ALP_COMPRESS_ETC1_SPLIT_FUSED", + "#define ALP_ETC1_INDEX_EXACT_TABLE_SEARCH" }, + Etc1FastProgramDefinition { Encoder::FastSplitBounds, "#define ALP_COMPRESS_ETC1_SPLIT_BOUNDS", nullptr }, + Etc1FastProgramDefinition { Encoder::FastSplitBoundsProjection, + "#define ALP_COMPRESS_ETC1_SPLIT_BOUNDS", + "#define ALP_ETC1_INDEX_PROJECTION" }, + Etc1FastProgramDefinition { Encoder::FastSplitBoundsProjectionClamped, + "#define ALP_COMPRESS_ETC1_SPLIT_BOUNDS", + "#define ALP_ETC1_INDEX_PROJECTION_CLAMPED" }, + Etc1FastProgramDefinition { Encoder::FastSplitBoundsExact, + "#define ALP_COMPRESS_ETC1_SPLIT_BOUNDS", + "#define ALP_ETC1_INDEX_EXACT" }, + Etc1FastProgramDefinition { Encoder::FastSplitBoundsExactTableSearch, + "#define ALP_COMPRESS_ETC1_SPLIT_BOUNDS", + "#define ALP_ETC1_INDEX_EXACT_TABLE_SEARCH" }, + }; + unsigned width = 0; unsigned height = 0; unsigned max_batch_size = 0; @@ -402,8 +442,7 @@ struct gl_engine::TextureCompressor::Impl { GLuint packing_renderbuffer = 0; std::unique_ptr dxt1_fragment_program; std::unique_ptr etc1_fragment_program; - std::unique_ptr etc1_fast_split_fused_fragment_program; - std::unique_ptr etc1_fast_split_bounds_fragment_program; + std::array etc1_fast_programs; std::unique_ptr checksum_fragment_program; std::unique_ptr packing_program; @@ -487,14 +526,20 @@ struct gl_engine::TextureCompressor::Impl { "texture_compress.vert", ShaderCodeSource::FILE, std::vector { QStringLiteral("#define ALP_COMPRESS_ETC1") }); - etc1_fast_split_fused_fragment_program = std::make_unique("texture_compress_raster.vert", - "texture_compress.vert", - ShaderCodeSource::FILE, - std::vector { QStringLiteral("#define ALP_COMPRESS_ETC1"), QStringLiteral("#define ALP_COMPRESS_ETC1_SPLIT_FUSED") }); - etc1_fast_split_bounds_fragment_program = std::make_unique("texture_compress_raster.vert", - "texture_compress.vert", - ShaderCodeSource::FILE, - std::vector { QStringLiteral("#define ALP_COMPRESS_ETC1"), QStringLiteral("#define ALP_COMPRESS_ETC1_SPLIT_BOUNDS") }); + for (size_t i = 0; i < etc1_fast_program_definitions.size(); ++i) { + const auto& definition = etc1_fast_program_definitions[i]; + std::vector defines { + QStringLiteral("#define ALP_COMPRESS_ETC1"), + QString::fromLatin1(definition.algorithm_define), + }; + if (definition.index_define) + defines.push_back(QString::fromLatin1(definition.index_define)); + etc1_fast_programs[i] = { + definition.encoder, + std::make_unique( + "texture_compress_raster.vert", "texture_compress.vert", ShaderCodeSource::FILE, defines), + }; + } checksum_fragment_program = std::make_unique("texture_compress_raster.vert", "texture_compress.vert", ShaderCodeSource::FILE, @@ -508,8 +553,8 @@ struct gl_engine::TextureCompressor::Impl { { dxt1_fragment_program.reset(); etc1_fragment_program.reset(); - etc1_fast_split_fused_fragment_program.reset(); - etc1_fast_split_bounds_fragment_program.reset(); + for (auto& fast_program : etc1_fast_programs) + fast_program.program.reset(); checksum_fragment_program.reset(); packing_program.reset(); if (!QOpenGLContext::currentContext()) @@ -660,12 +705,11 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: if (settings.encoder == Encoder::Checksum) { program = m->checksum_fragment_program.get(); } else if (settings.algorithm == nucleus::utils::ColourTexture::Format::ETC1) { - if (settings.encoder == Encoder::FastSplitFused) - program = m->etc1_fast_split_fused_fragment_program.get(); - else if (settings.encoder == Encoder::FastSplitBounds) - program = m->etc1_fast_split_bounds_fragment_program.get(); - else - program = m->etc1_fragment_program.get(); + program = m->etc1_fragment_program.get(); + const auto fast_program = std::ranges::find( + m->etc1_fast_programs, settings.encoder, &Impl::Etc1FastProgram::encoder); + if (fast_program != m->etc1_fast_programs.end()) + program = fast_program->program.get(); } f->glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previous_draw_framebuffer); f->glGetIntegerv(GL_VIEWPORT, previous_viewport); diff --git a/gl_engine/Texture.h b/gl_engine/Texture.h index 204d784a..c26138d9 100644 --- a/gl_engine/Texture.h +++ b/gl_engine/Texture.h @@ -91,7 +91,21 @@ class Texture { class TextureCompressor { public: - enum class Encoder { Search, Dxt1, FastSplitFused, FastSplitBounds, Checksum }; + enum class Encoder { + Search, + Dxt1, + FastSplitFused, + FastSplitFusedProjection, + FastSplitFusedProjectionClamped, + FastSplitFusedExact, + FastSplitFusedExactTableSearch, + FastSplitBounds, + FastSplitBoundsProjection, + FastSplitBoundsProjectionClamped, + FastSplitBoundsExact, + FastSplitBoundsExactTableSearch, + Checksum + }; struct Settings { nucleus::utils::ColourTexture::Format algorithm = nucleus::utils::ColourTexture::Format::DXT1; diff --git a/gl_engine/shaders/texture_compress.vert b/gl_engine/shaders/texture_compress.vert index 79dcef79..455c55b4 100644 --- a/gl_engine/shaders/texture_compress.vert +++ b/gl_engine/shaders/texture_compress.vert @@ -132,6 +132,64 @@ highp int table_for_range(highp int range) return 7; } +highp uint select_etc1_modifier_exact(highp uvec3 pixel, highp ivec3 decoded_base, highp int table) +{ + highp uint selected = 0u; + highp uint selected_error = 0xffffffffu; + for (int index = 0; index < 4; ++index) { + highp ivec3 reconstructed = clamp(decoded_base + ivec3(modifier(table, index)), ivec3(0), ivec3(255)); + highp ivec3 delta = ivec3(pixel) - reconstructed; + highp uint error = uint(delta.x * delta.x + delta.y * delta.y + delta.z * delta.z); + if (error < selected_error) { + selected = uint(index); + selected_error = error; + } + } + return selected; +} + +highp uint select_etc1_modifier_projection(highp uvec3 pixel, highp ivec3 decoded_base, highp int table) +{ + highp ivec3 delta = ivec3(pixel) - decoded_base; + highp int delta_sum = delta.r + delta.g + delta.b; + highp int midpoint_limit = 3 * (modifier(table, 0) + modifier(table, 1)); + highp int twice_delta_sum = 2 * delta_sum; + if (twice_delta_sum < -midpoint_limit) + return 3u; + if (delta_sum < 0) + return 2u; + if (twice_delta_sum <= midpoint_limit) + return 0u; + return 1u; +} + +highp uint select_etc1_modifier(highp uvec3 pixel, + highp int pixel_brightness, + highp ivec3 decoded_base, + highp int table, + highp int middle, + highp int threshold) +{ +#if defined(ALP_ETC1_INDEX_EXACT) || defined(ALP_ETC1_INDEX_EXACT_TABLE_SEARCH) + return select_etc1_modifier_exact(pixel, decoded_base, table); +#elif defined(ALP_ETC1_INDEX_PROJECTION_CLAMPED) + highp int far_modifier = modifier(table, 1); + bool palette_clips = any(lessThan(decoded_base, ivec3(far_modifier))) + || any(greaterThan(decoded_base, ivec3(255 - far_modifier))); + if (palette_clips) + return select_etc1_modifier_exact(pixel, decoded_base, table); + return select_etc1_modifier_projection(pixel, decoded_base, table); +#elif defined(ALP_ETC1_INDEX_PROJECTION) + return select_etc1_modifier_projection(pixel, decoded_base, table); +#else + highp int brightness_delta = pixel_brightness - middle; + highp uint selected = uint(abs(brightness_delta) >= threshold); + if (brightness_delta < 0) + selected += 2u; + return selected; +#endif +} + struct FastEtc1Subblock { highp ivec3 colour; highp int table; @@ -188,6 +246,35 @@ FastEtc1Bases fast_split_bases(FastEtc1Subblock first, FastEtc1Subblock second) 0u); } +#ifdef ALP_ETC1_INDEX_EXACT_TABLE_SEARCH +highp int search_etc1_table(highp uvec3 pixels[16], highp ivec3 decoded_base, bool flip, bool second) +{ + highp uint best_error = 0xffffffffu; + highp int best_table = 0; + for (int table = 0; table < 8; ++table) { + highp uint total_error = 0u; + for (int y = 0; y < 4; ++y) { + for (int x = 0; x < 4; ++x) { + bool use_second = flip ? y >= 2 : x >= 2; + if (use_second != second) + continue; + highp uvec3 pixel = pixels[y * 4 + x]; + highp uint selected = select_etc1_modifier_exact(pixel, decoded_base, table); + highp ivec3 reconstructed + = clamp(decoded_base + ivec3(modifier(table, int(selected))), ivec3(0), ivec3(255)); + highp ivec3 delta = ivec3(pixel) - reconstructed; + total_error += uint(delta.x * delta.x + delta.y * delta.y + delta.z * delta.z); + } + } + if (total_error < best_error) { + best_error = total_error; + best_table = table; + } + } + return best_table; +} +#endif + highp uvec2 encode_etc1_fast_split_fused(highp uvec3 pixels[16]) { highp uvec3 left_minimum = uvec3(255u); @@ -232,6 +319,12 @@ highp uvec2 encode_etc1_fast_split_fused(highp uvec3 pixels[16]) FastEtc1Subblock bottom = fast_subblock_from_statistics(bottom_minimum, bottom_maximum, bottom_sum); FastEtc1Bases vertical_bases = fast_split_bases(left, right); FastEtc1Bases horizontal_bases = fast_split_bases(top, bottom); +#ifdef ALP_ETC1_INDEX_EXACT_TABLE_SEARCH + left.table = search_etc1_table(pixels, vertical_bases.first_decoded, false, false); + right.table = search_etc1_table(pixels, vertical_bases.second_decoded, false, true); + top.table = search_etc1_table(pixels, horizontal_bases.first_decoded, true, false); + bottom.table = search_etc1_table(pixels, horizontal_bases.second_decoded, true, true); +#endif highp uint vertical_indices = 0u; highp uint horizontal_indices = 0u; @@ -249,10 +342,8 @@ highp uvec2 encode_etc1_fast_split_fused(highp uvec3 pixels[16]) highp int vertical_threshold = use_right ? right.threshold : left.threshold; highp int vertical_table = use_right ? right.table : left.table; highp ivec3 vertical_base = use_right ? vertical_bases.second_decoded : vertical_bases.first_decoded; - highp int vertical_brightness_delta = pixel_brightness - vertical_middle; - highp uint vertical_selected = uint(abs(vertical_brightness_delta) >= vertical_threshold); - if (vertical_brightness_delta < 0) - vertical_selected += 2u; + highp uint vertical_selected = select_etc1_modifier( + pixel, pixel_brightness, vertical_base, vertical_table, vertical_middle, vertical_threshold); highp ivec3 vertical_reconstructed = clamp(vertical_base + ivec3(modifier(vertical_table, int(vertical_selected))), ivec3(0), ivec3(255)); highp ivec3 vertical_delta = ivec3(pixel) - vertical_reconstructed; @@ -262,10 +353,8 @@ highp uvec2 encode_etc1_fast_split_fused(highp uvec3 pixels[16]) highp int horizontal_threshold = use_bottom ? bottom.threshold : top.threshold; highp int horizontal_table = use_bottom ? bottom.table : top.table; highp ivec3 horizontal_base = use_bottom ? horizontal_bases.second_decoded : horizontal_bases.first_decoded; - highp int horizontal_brightness_delta = pixel_brightness - horizontal_middle; - highp uint horizontal_selected = uint(abs(horizontal_brightness_delta) >= horizontal_threshold); - if (horizontal_brightness_delta < 0) - horizontal_selected += 2u; + highp uint horizontal_selected = select_etc1_modifier( + pixel, pixel_brightness, horizontal_base, horizontal_table, horizontal_middle, horizontal_threshold); highp ivec3 horizontal_reconstructed = clamp(horizontal_base + ivec3(modifier(horizontal_table, int(horizontal_selected))), ivec3(0), ivec3(255)); highp ivec3 horizontal_delta = ivec3(pixel) - horizontal_reconstructed; @@ -294,17 +383,22 @@ highp uvec2 encode_etc1_selected_orientation(highp uvec3 pixels[16], bool flip) { FastEtc1Bases bases = fast_split_bases(first, second); +#ifdef ALP_ETC1_INDEX_EXACT_TABLE_SEARCH + first.table = search_etc1_table(pixels, bases.first_decoded, flip, false); + second.table = search_etc1_table(pixels, bases.second_decoded, flip, true); +#endif highp uint indices = 0u; for (int y = 0; y < 4; ++y) { for (int x = 0; x < 4; ++x) { highp int pixel_index = y * 4 + x; bool use_second = flip ? y >= 2 : x >= 2; + highp uvec3 pixel = pixels[pixel_index]; highp int middle = use_second ? second.middle : first.middle; highp int threshold = use_second ? second.threshold : first.threshold; - highp int brightness_delta = brightness(pixels[pixel_index]) - middle; - highp uint selected = uint(abs(brightness_delta) >= threshold); - if (brightness_delta < 0) - selected += 2u; + highp int table = use_second ? second.table : first.table; + highp ivec3 decoded_base = use_second ? bases.second_decoded : bases.first_decoded; + highp uint selected + = select_etc1_modifier(pixel, brightness(pixel), decoded_base, table, middle, threshold); highp uint bit_position = uint(x * 4 + y); indices |= (selected & 1u) << bit_position; indices |= (selected >> 1u) << (bit_position + 16u); diff --git a/unittests/texture_compression_benchmark/main.cpp b/unittests/texture_compression_benchmark/main.cpp index 9410cae7..84012360 100644 --- a/unittests/texture_compression_benchmark/main.cpp +++ b/unittests/texture_compression_benchmark/main.cpp @@ -312,7 +312,25 @@ std::vector supported_algorithms(Format format) result.push_back({ "DXT1", Operation::Compression, settings(Encoder::Dxt1) }); } else if (format == Format::ETC1) { result.push_back({ "ETC1 fast split fused", Operation::Compression, settings(Encoder::FastSplitFused) }); + result.push_back( + { "ETC1 fused projection", Operation::Compression, settings(Encoder::FastSplitFusedProjection) }); + result.push_back({ "ETC1 fused projection clamp", + Operation::Compression, + settings(Encoder::FastSplitFusedProjectionClamped) }); + result.push_back({ "ETC1 fused exact", Operation::Compression, settings(Encoder::FastSplitFusedExact) }); + result.push_back({ "ETC1 fused exact table search", + Operation::Compression, + settings(Encoder::FastSplitFusedExactTableSearch) }); result.push_back({ "ETC1 fast split bounds", Operation::Compression, settings(Encoder::FastSplitBounds) }); + result.push_back( + { "ETC1 bounds projection", Operation::Compression, settings(Encoder::FastSplitBoundsProjection) }); + result.push_back({ "ETC1 bounds projection clamp", + Operation::Compression, + settings(Encoder::FastSplitBoundsProjectionClamped) }); + result.push_back({ "ETC1 bounds exact", Operation::Compression, settings(Encoder::FastSplitBoundsExact) }); + result.push_back({ "ETC1 bounds exact table search", + Operation::Compression, + settings(Encoder::FastSplitBoundsExactTableSearch) }); } return result; } From 1da7532c8e141687383b182a03aef0e203a3af3a Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Sat, 22 Aug 2026 07:28:31 +0200 Subject: [PATCH 28/38] Keep ETC1 search and fused exact encoders --- .../TexturePreviewItem.cpp | 50 ++--- gl_engine/Texture.cpp | 70 +------ gl_engine/Texture.h | 9 - gl_engine/shaders/texture_compress.vert | 188 +----------------- .../texture_compression_benchmark/main.cpp | 19 -- 5 files changed, 25 insertions(+), 311 deletions(-) diff --git a/apps/texture_compression_benchmark/TexturePreviewItem.cpp b/apps/texture_compression_benchmark/TexturePreviewItem.cpp index 2307ce1e..e2f56c11 100644 --- a/apps/texture_compression_benchmark/TexturePreviewItem.cpp +++ b/apps/texture_compression_benchmark/TexturePreviewItem.cpp @@ -119,11 +119,23 @@ struct GpuPreview { unsigned effort; }; -constexpr std::array gpu_previews { { +constexpr std::array gpu_previews { { { "Search 0", "Tests the average block colour with every ETC1 modifier table and keeps the lowest-error result.", gl_engine::TextureCompressor::Encoder::Search, 0 }, + { "search 1", + "Tests two base colours around the block average with every ETC1 modifier table and keeps the lowest-error result.", + gl_engine::TextureCompressor::Encoder::Search, + 1 }, + { "search 2", + "Tests three base colours around the block average with every ETC1 modifier table and keeps the lowest-error result.", + gl_engine::TextureCompressor::Encoder::Search, + 2 }, + { "search 3", + "Tests four base colours around the block average with every ETC1 modifier table and keeps the lowest-error result.", + gl_engine::TextureCompressor::Encoder::Search, + 3 }, { "search 4", "Tests five base colours around the block average with every ETC1 modifier table and keeps the lowest-error result.", gl_engine::TextureCompressor::Encoder::Search, @@ -132,46 +144,10 @@ constexpr std::array gpu_previews { { "Tests eleven base colours around the block average with every ETC1 modifier table and keeps the lowest-error result.", gl_engine::TextureCompressor::Encoder::Search, 10 }, - { "split fused", - "Evaluates vertical and horizontal two-sub-block layouts while gathering their statistics and indices in shared shader loops.", - gl_engine::TextureCompressor::Encoder::FastSplitFused, - 0 }, - { "split fused projection", - "Selects modifiers from the RGB projection relative to each decoded base colour.", - gl_engine::TextureCompressor::Encoder::FastSplitFusedProjection, - 0 }, - { "split fused projection + clamp", - "Uses RGB projection, with exact palette evaluation when a modifier would clamp a colour channel.", - gl_engine::TextureCompressor::Encoder::FastSplitFusedProjectionClamped, - 0 }, { "split fused exact", "Evaluates all four colours in the selected modifier table for every pixel.", gl_engine::TextureCompressor::Encoder::FastSplitFusedExact, 0 }, - { "split fused exact + tables", - "Searches all modifier tables and evaluates all four reconstructed colours for every pixel.", - gl_engine::TextureCompressor::Encoder::FastSplitFusedExactTableSearch, - 0 }, - { "split bounds", - "Uses the sub-block colour bounds to choose an orientation first, then encodes only the selected layout.", - gl_engine::TextureCompressor::Encoder::FastSplitBounds, - 0 }, - { "split bounds projection", - "Chooses the orientation from colour bounds, then selects modifiers from decoded-base RGB projection.", - gl_engine::TextureCompressor::Encoder::FastSplitBoundsProjection, - 0 }, - { "split bounds projection + clamp", - "Uses RGB projection after bounds selection, with exact evaluation when a modifier would clamp.", - gl_engine::TextureCompressor::Encoder::FastSplitBoundsProjectionClamped, - 0 }, - { "split bounds exact", - "Chooses the orientation from colour bounds, then evaluates all four table colours per pixel.", - gl_engine::TextureCompressor::Encoder::FastSplitBoundsExact, - 0 }, - { "split bounds exact + tables", - "Chooses the orientation from colour bounds, then searches every modifier table with exact pixel evaluation.", - gl_engine::TextureCompressor::Encoder::FastSplitBoundsExactTableSearch, - 0 }, } }; constexpr size_t uncompressed_preview_index = 0; diff --git a/gl_engine/Texture.cpp b/gl_engine/Texture.cpp index 72eb9e7e..c4413f18 100644 --- a/gl_engine/Texture.cpp +++ b/gl_engine/Texture.cpp @@ -24,7 +24,6 @@ #include #include #include -#include #include #ifdef __EMSCRIPTEN__ #include @@ -385,46 +384,6 @@ float gl_engine::Texture::max_anisotropy() struct gl_engine::TextureCompressor::Impl { static constexpr unsigned max_shader_mip_levels = 16; - struct Etc1FastProgram { - Encoder encoder = Encoder::FastSplitFused; - std::unique_ptr program; - }; - - struct Etc1FastProgramDefinition { - Encoder encoder; - const char* algorithm_define; - const char* index_define; - }; - - static constexpr std::array etc1_fast_program_definitions { - Etc1FastProgramDefinition { Encoder::FastSplitFused, "#define ALP_COMPRESS_ETC1_SPLIT_FUSED", nullptr }, - Etc1FastProgramDefinition { Encoder::FastSplitFusedProjection, - "#define ALP_COMPRESS_ETC1_SPLIT_FUSED", - "#define ALP_ETC1_INDEX_PROJECTION" }, - Etc1FastProgramDefinition { Encoder::FastSplitFusedProjectionClamped, - "#define ALP_COMPRESS_ETC1_SPLIT_FUSED", - "#define ALP_ETC1_INDEX_PROJECTION_CLAMPED" }, - Etc1FastProgramDefinition { Encoder::FastSplitFusedExact, - "#define ALP_COMPRESS_ETC1_SPLIT_FUSED", - "#define ALP_ETC1_INDEX_EXACT" }, - Etc1FastProgramDefinition { Encoder::FastSplitFusedExactTableSearch, - "#define ALP_COMPRESS_ETC1_SPLIT_FUSED", - "#define ALP_ETC1_INDEX_EXACT_TABLE_SEARCH" }, - Etc1FastProgramDefinition { Encoder::FastSplitBounds, "#define ALP_COMPRESS_ETC1_SPLIT_BOUNDS", nullptr }, - Etc1FastProgramDefinition { Encoder::FastSplitBoundsProjection, - "#define ALP_COMPRESS_ETC1_SPLIT_BOUNDS", - "#define ALP_ETC1_INDEX_PROJECTION" }, - Etc1FastProgramDefinition { Encoder::FastSplitBoundsProjectionClamped, - "#define ALP_COMPRESS_ETC1_SPLIT_BOUNDS", - "#define ALP_ETC1_INDEX_PROJECTION_CLAMPED" }, - Etc1FastProgramDefinition { Encoder::FastSplitBoundsExact, - "#define ALP_COMPRESS_ETC1_SPLIT_BOUNDS", - "#define ALP_ETC1_INDEX_EXACT" }, - Etc1FastProgramDefinition { Encoder::FastSplitBoundsExactTableSearch, - "#define ALP_COMPRESS_ETC1_SPLIT_BOUNDS", - "#define ALP_ETC1_INDEX_EXACT_TABLE_SEARCH" }, - }; - unsigned width = 0; unsigned height = 0; unsigned max_batch_size = 0; @@ -442,7 +401,7 @@ struct gl_engine::TextureCompressor::Impl { GLuint packing_renderbuffer = 0; std::unique_ptr dxt1_fragment_program; std::unique_ptr etc1_fragment_program; - std::array etc1_fast_programs; + std::unique_ptr etc1_fast_split_fused_exact_fragment_program; std::unique_ptr checksum_fragment_program; std::unique_ptr packing_program; @@ -526,20 +485,10 @@ struct gl_engine::TextureCompressor::Impl { "texture_compress.vert", ShaderCodeSource::FILE, std::vector { QStringLiteral("#define ALP_COMPRESS_ETC1") }); - for (size_t i = 0; i < etc1_fast_program_definitions.size(); ++i) { - const auto& definition = etc1_fast_program_definitions[i]; - std::vector defines { - QStringLiteral("#define ALP_COMPRESS_ETC1"), - QString::fromLatin1(definition.algorithm_define), - }; - if (definition.index_define) - defines.push_back(QString::fromLatin1(definition.index_define)); - etc1_fast_programs[i] = { - definition.encoder, - std::make_unique( - "texture_compress_raster.vert", "texture_compress.vert", ShaderCodeSource::FILE, defines), - }; - } + etc1_fast_split_fused_exact_fragment_program = std::make_unique("texture_compress_raster.vert", + "texture_compress.vert", + ShaderCodeSource::FILE, + std::vector { QStringLiteral("#define ALP_COMPRESS_ETC1"), QStringLiteral("#define ALP_COMPRESS_ETC1_SPLIT_FUSED") }); checksum_fragment_program = std::make_unique("texture_compress_raster.vert", "texture_compress.vert", ShaderCodeSource::FILE, @@ -553,8 +502,7 @@ struct gl_engine::TextureCompressor::Impl { { dxt1_fragment_program.reset(); etc1_fragment_program.reset(); - for (auto& fast_program : etc1_fast_programs) - fast_program.program.reset(); + etc1_fast_split_fused_exact_fragment_program.reset(); checksum_fragment_program.reset(); packing_program.reset(); if (!QOpenGLContext::currentContext()) @@ -706,10 +654,8 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: program = m->checksum_fragment_program.get(); } else if (settings.algorithm == nucleus::utils::ColourTexture::Format::ETC1) { program = m->etc1_fragment_program.get(); - const auto fast_program = std::ranges::find( - m->etc1_fast_programs, settings.encoder, &Impl::Etc1FastProgram::encoder); - if (fast_program != m->etc1_fast_programs.end()) - program = fast_program->program.get(); + if (settings.encoder == Encoder::FastSplitFusedExact) + program = m->etc1_fast_split_fused_exact_fragment_program.get(); } f->glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previous_draw_framebuffer); f->glGetIntegerv(GL_VIEWPORT, previous_viewport); diff --git a/gl_engine/Texture.h b/gl_engine/Texture.h index c26138d9..d30f29d9 100644 --- a/gl_engine/Texture.h +++ b/gl_engine/Texture.h @@ -94,16 +94,7 @@ class TextureCompressor { enum class Encoder { Search, Dxt1, - FastSplitFused, - FastSplitFusedProjection, - FastSplitFusedProjectionClamped, FastSplitFusedExact, - FastSplitFusedExactTableSearch, - FastSplitBounds, - FastSplitBoundsProjection, - FastSplitBoundsProjectionClamped, - FastSplitBoundsExact, - FastSplitBoundsExactTableSearch, Checksum }; diff --git a/gl_engine/shaders/texture_compress.vert b/gl_engine/shaders/texture_compress.vert index 455c55b4..75427f29 100644 --- a/gl_engine/shaders/texture_compress.vert +++ b/gl_engine/shaders/texture_compress.vert @@ -148,53 +148,9 @@ highp uint select_etc1_modifier_exact(highp uvec3 pixel, highp ivec3 decoded_bas return selected; } -highp uint select_etc1_modifier_projection(highp uvec3 pixel, highp ivec3 decoded_base, highp int table) -{ - highp ivec3 delta = ivec3(pixel) - decoded_base; - highp int delta_sum = delta.r + delta.g + delta.b; - highp int midpoint_limit = 3 * (modifier(table, 0) + modifier(table, 1)); - highp int twice_delta_sum = 2 * delta_sum; - if (twice_delta_sum < -midpoint_limit) - return 3u; - if (delta_sum < 0) - return 2u; - if (twice_delta_sum <= midpoint_limit) - return 0u; - return 1u; -} - -highp uint select_etc1_modifier(highp uvec3 pixel, - highp int pixel_brightness, - highp ivec3 decoded_base, - highp int table, - highp int middle, - highp int threshold) -{ -#if defined(ALP_ETC1_INDEX_EXACT) || defined(ALP_ETC1_INDEX_EXACT_TABLE_SEARCH) - return select_etc1_modifier_exact(pixel, decoded_base, table); -#elif defined(ALP_ETC1_INDEX_PROJECTION_CLAMPED) - highp int far_modifier = modifier(table, 1); - bool palette_clips = any(lessThan(decoded_base, ivec3(far_modifier))) - || any(greaterThan(decoded_base, ivec3(255 - far_modifier))); - if (palette_clips) - return select_etc1_modifier_exact(pixel, decoded_base, table); - return select_etc1_modifier_projection(pixel, decoded_base, table); -#elif defined(ALP_ETC1_INDEX_PROJECTION) - return select_etc1_modifier_projection(pixel, decoded_base, table); -#else - highp int brightness_delta = pixel_brightness - middle; - highp uint selected = uint(abs(brightness_delta) >= threshold); - if (brightness_delta < 0) - selected += 2u; - return selected; -#endif -} - struct FastEtc1Subblock { highp ivec3 colour; highp int table; - highp int middle; - highp int threshold; }; FastEtc1Subblock fast_subblock_from_statistics(highp uvec3 minimum_colour, @@ -208,7 +164,7 @@ FastEtc1Subblock fast_subblock_from_statistics(highp uvec3 minimum_colour, highp ivec3 average = ivec3((sum + 4u) / 8u); highp int correction = middle - brightness(uvec3(average)); highp ivec3 colour = clamp(average + ivec3(correction), ivec3(0), ivec3(255)); - return FastEtc1Subblock(colour, table_for_range(range), middle, (range * 3 + 4) / 8); + return FastEtc1Subblock(colour, table_for_range(range)); } struct FastEtc1Bases { @@ -246,35 +202,6 @@ FastEtc1Bases fast_split_bases(FastEtc1Subblock first, FastEtc1Subblock second) 0u); } -#ifdef ALP_ETC1_INDEX_EXACT_TABLE_SEARCH -highp int search_etc1_table(highp uvec3 pixels[16], highp ivec3 decoded_base, bool flip, bool second) -{ - highp uint best_error = 0xffffffffu; - highp int best_table = 0; - for (int table = 0; table < 8; ++table) { - highp uint total_error = 0u; - for (int y = 0; y < 4; ++y) { - for (int x = 0; x < 4; ++x) { - bool use_second = flip ? y >= 2 : x >= 2; - if (use_second != second) - continue; - highp uvec3 pixel = pixels[y * 4 + x]; - highp uint selected = select_etc1_modifier_exact(pixel, decoded_base, table); - highp ivec3 reconstructed - = clamp(decoded_base + ivec3(modifier(table, int(selected))), ivec3(0), ivec3(255)); - highp ivec3 delta = ivec3(pixel) - reconstructed; - total_error += uint(delta.x * delta.x + delta.y * delta.y + delta.z * delta.z); - } - } - if (total_error < best_error) { - best_error = total_error; - best_table = table; - } - } - return best_table; -} -#endif - highp uvec2 encode_etc1_fast_split_fused(highp uvec3 pixels[16]) { highp uvec3 left_minimum = uvec3(255u); @@ -319,12 +246,6 @@ highp uvec2 encode_etc1_fast_split_fused(highp uvec3 pixels[16]) FastEtc1Subblock bottom = fast_subblock_from_statistics(bottom_minimum, bottom_maximum, bottom_sum); FastEtc1Bases vertical_bases = fast_split_bases(left, right); FastEtc1Bases horizontal_bases = fast_split_bases(top, bottom); -#ifdef ALP_ETC1_INDEX_EXACT_TABLE_SEARCH - left.table = search_etc1_table(pixels, vertical_bases.first_decoded, false, false); - right.table = search_etc1_table(pixels, vertical_bases.second_decoded, false, true); - top.table = search_etc1_table(pixels, horizontal_bases.first_decoded, true, false); - bottom.table = search_etc1_table(pixels, horizontal_bases.second_decoded, true, true); -#endif highp uint vertical_indices = 0u; highp uint horizontal_indices = 0u; @@ -334,27 +255,20 @@ highp uvec2 encode_etc1_fast_split_fused(highp uvec3 pixels[16]) for (int x = 0; x < 4; ++x) { highp int pixel_index = y * 4 + x; highp uvec3 pixel = pixels[pixel_index]; - highp int pixel_brightness = brightness(pixel); bool use_right = x >= 2; bool use_bottom = y >= 2; - highp int vertical_middle = use_right ? right.middle : left.middle; - highp int vertical_threshold = use_right ? right.threshold : left.threshold; highp int vertical_table = use_right ? right.table : left.table; highp ivec3 vertical_base = use_right ? vertical_bases.second_decoded : vertical_bases.first_decoded; - highp uint vertical_selected = select_etc1_modifier( - pixel, pixel_brightness, vertical_base, vertical_table, vertical_middle, vertical_threshold); + highp uint vertical_selected = select_etc1_modifier_exact(pixel, vertical_base, vertical_table); highp ivec3 vertical_reconstructed = clamp(vertical_base + ivec3(modifier(vertical_table, int(vertical_selected))), ivec3(0), ivec3(255)); highp ivec3 vertical_delta = ivec3(pixel) - vertical_reconstructed; vertical_error += uint(vertical_delta.x * vertical_delta.x + vertical_delta.y * vertical_delta.y + vertical_delta.z * vertical_delta.z); - highp int horizontal_middle = use_bottom ? bottom.middle : top.middle; - highp int horizontal_threshold = use_bottom ? bottom.threshold : top.threshold; highp int horizontal_table = use_bottom ? bottom.table : top.table; highp ivec3 horizontal_base = use_bottom ? horizontal_bases.second_decoded : horizontal_bases.first_decoded; - highp uint horizontal_selected = select_etc1_modifier( - pixel, pixel_brightness, horizontal_base, horizontal_table, horizontal_middle, horizontal_threshold); + highp uint horizontal_selected = select_etc1_modifier_exact(pixel, horizontal_base, horizontal_table); highp ivec3 horizontal_reconstructed = clamp(horizontal_base + ivec3(modifier(horizontal_table, int(horizontal_selected))), ivec3(0), ivec3(255)); highp ivec3 horizontal_delta = ivec3(pixel) - horizontal_reconstructed; @@ -377,98 +291,6 @@ highp uvec2 encode_etc1_fast_split_fused(highp uvec3 pixels[16]) return horizontal_error < vertical_error ? horizontal : vertical; } -highp uvec2 encode_etc1_selected_orientation(highp uvec3 pixels[16], - FastEtc1Subblock first, - FastEtc1Subblock second, - bool flip) -{ - FastEtc1Bases bases = fast_split_bases(first, second); -#ifdef ALP_ETC1_INDEX_EXACT_TABLE_SEARCH - first.table = search_etc1_table(pixels, bases.first_decoded, flip, false); - second.table = search_etc1_table(pixels, bases.second_decoded, flip, true); -#endif - highp uint indices = 0u; - for (int y = 0; y < 4; ++y) { - for (int x = 0; x < 4; ++x) { - highp int pixel_index = y * 4 + x; - bool use_second = flip ? y >= 2 : x >= 2; - highp uvec3 pixel = pixels[pixel_index]; - highp int middle = use_second ? second.middle : first.middle; - highp int threshold = use_second ? second.threshold : first.threshold; - highp int table = use_second ? second.table : first.table; - highp ivec3 decoded_base = use_second ? bases.second_decoded : bases.first_decoded; - highp uint selected - = select_etc1_modifier(pixel, brightness(pixel), decoded_base, table, middle, threshold); - highp uint bit_position = uint(x * 4 + y); - indices |= (selected & 1u) << bit_position; - indices |= (selected >> 1u) << (bit_position + 16u); - } - } - - highp uint control = uint(first.table) << 5u | uint(second.table) << 2u - | bases.differential_bit | (flip ? 1u : 0u); - return uvec2(bases.header | control << 24u, byte_swap(indices)); -} - -highp uint fast_bounds_score(highp uvec3 minimum_colour, highp uvec3 maximum_colour) -{ - highp uvec3 range = maximum_colour - minimum_colour; - return range.r * range.r + range.g * range.g + range.b * range.b; -} - -highp uvec2 encode_etc1_fast_split_bounds(highp uvec3 pixels[16]) -{ - highp uvec3 left_minimum = uvec3(255u); - highp uvec3 left_maximum = uvec3(0u); - highp uvec3 left_sum = uvec3(0u); - highp uvec3 right_minimum = uvec3(255u); - highp uvec3 right_maximum = uvec3(0u); - highp uvec3 right_sum = uvec3(0u); - highp uvec3 top_minimum = uvec3(255u); - highp uvec3 top_maximum = uvec3(0u); - highp uvec3 top_sum = uvec3(0u); - highp uvec3 bottom_minimum = uvec3(255u); - highp uvec3 bottom_maximum = uvec3(0u); - highp uvec3 bottom_sum = uvec3(0u); - for (int y = 0; y < 4; ++y) { - for (int x = 0; x < 4; ++x) { - highp uvec3 pixel = pixels[y * 4 + x]; - if (x < 2) { - left_minimum = min(left_minimum, pixel); - left_maximum = max(left_maximum, pixel); - left_sum += pixel; - } else { - right_minimum = min(right_minimum, pixel); - right_maximum = max(right_maximum, pixel); - right_sum += pixel; - } - if (y < 2) { - top_minimum = min(top_minimum, pixel); - top_maximum = max(top_maximum, pixel); - top_sum += pixel; - } else { - bottom_minimum = min(bottom_minimum, pixel); - bottom_maximum = max(bottom_maximum, pixel); - bottom_sum += pixel; - } - } - } - - highp uint vertical_score = fast_bounds_score(left_minimum, left_maximum) - + fast_bounds_score(right_minimum, right_maximum); - highp uint horizontal_score = fast_bounds_score(top_minimum, top_maximum) - + fast_bounds_score(bottom_minimum, bottom_maximum); - if (horizontal_score < vertical_score) { - FastEtc1Subblock top = fast_subblock_from_statistics(top_minimum, top_maximum, top_sum); - FastEtc1Subblock bottom = fast_subblock_from_statistics(bottom_minimum, bottom_maximum, bottom_sum); - return encode_etc1_selected_orientation(pixels, top, bottom, true); - } - - FastEtc1Subblock left = fast_subblock_from_statistics(left_minimum, left_maximum, left_sum); - FastEtc1Subblock right = fast_subblock_from_statistics(right_minimum, right_maximum, right_sum); - return encode_etc1_selected_orientation(pixels, left, right, false); -} - highp uvec2 encode_etc1(highp uvec3 pixels[16]) { highp uvec3 sum = uvec3(0u); @@ -550,9 +372,7 @@ highp uvec2 compress_block(highp ivec2 block, } return checksum; #elif defined(ALP_COMPRESS_ETC1) -#ifdef ALP_COMPRESS_ETC1_SPLIT_BOUNDS - return encode_etc1_fast_split_bounds(pixels); -#elif defined(ALP_COMPRESS_ETC1_SPLIT_FUSED) +#ifdef ALP_COMPRESS_ETC1_SPLIT_FUSED return encode_etc1_fast_split_fused(pixels); #else return encode_etc1(pixels); diff --git a/unittests/texture_compression_benchmark/main.cpp b/unittests/texture_compression_benchmark/main.cpp index 84012360..4f33764b 100644 --- a/unittests/texture_compression_benchmark/main.cpp +++ b/unittests/texture_compression_benchmark/main.cpp @@ -311,26 +311,7 @@ std::vector supported_algorithms(Format format) if (format == Format::DXT1) { result.push_back({ "DXT1", Operation::Compression, settings(Encoder::Dxt1) }); } else if (format == Format::ETC1) { - result.push_back({ "ETC1 fast split fused", Operation::Compression, settings(Encoder::FastSplitFused) }); - result.push_back( - { "ETC1 fused projection", Operation::Compression, settings(Encoder::FastSplitFusedProjection) }); - result.push_back({ "ETC1 fused projection clamp", - Operation::Compression, - settings(Encoder::FastSplitFusedProjectionClamped) }); result.push_back({ "ETC1 fused exact", Operation::Compression, settings(Encoder::FastSplitFusedExact) }); - result.push_back({ "ETC1 fused exact table search", - Operation::Compression, - settings(Encoder::FastSplitFusedExactTableSearch) }); - result.push_back({ "ETC1 fast split bounds", Operation::Compression, settings(Encoder::FastSplitBounds) }); - result.push_back( - { "ETC1 bounds projection", Operation::Compression, settings(Encoder::FastSplitBoundsProjection) }); - result.push_back({ "ETC1 bounds projection clamp", - Operation::Compression, - settings(Encoder::FastSplitBoundsProjectionClamped) }); - result.push_back({ "ETC1 bounds exact", Operation::Compression, settings(Encoder::FastSplitBoundsExact) }); - result.push_back({ "ETC1 bounds exact table search", - Operation::Compression, - settings(Encoder::FastSplitBoundsExactTableSearch) }); } return result; } From 601b5d3ff35ef3bd66ec3de3a4bfc56280cc7f64 Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Sat, 22 Aug 2026 16:58:28 +0200 Subject: [PATCH 29/38] Add ETC1 residual refinement encoders --- .../TexturePreviewItem.cpp | 10 +- gl_engine/Texture.cpp | 20 +++ gl_engine/Texture.h | 2 + gl_engine/shaders/texture_compress.vert | 152 +++++++++++++----- .../texture_compression_benchmark/main.cpp | 5 + 5 files changed, 145 insertions(+), 44 deletions(-) diff --git a/apps/texture_compression_benchmark/TexturePreviewItem.cpp b/apps/texture_compression_benchmark/TexturePreviewItem.cpp index e2f56c11..012bb848 100644 --- a/apps/texture_compression_benchmark/TexturePreviewItem.cpp +++ b/apps/texture_compression_benchmark/TexturePreviewItem.cpp @@ -119,7 +119,7 @@ struct GpuPreview { unsigned effort; }; -constexpr std::array gpu_previews { { +constexpr std::array gpu_previews { { { "Search 0", "Tests the average block colour with every ETC1 modifier table and keeps the lowest-error result.", gl_engine::TextureCompressor::Encoder::Search, @@ -148,6 +148,14 @@ constexpr std::array gpu_previews { { "Evaluates all four colours in the selected modifier table for every pixel.", gl_engine::TextureCompressor::Encoder::FastSplitFusedExact, 0 }, + { "split exact residual fit", + "Refits each base from the average per-channel reconstruction residual and evaluates it once.", + gl_engine::TextureCompressor::Encoder::FastSplitFusedExactResidual, + 0 }, + { "split exact shared residual", + "Applies one combined per-channel residual correction to both bases of the winning split.", + gl_engine::TextureCompressor::Encoder::FastSplitFusedExactSharedResidual, + 0 }, } }; constexpr size_t uncompressed_preview_index = 0; diff --git a/gl_engine/Texture.cpp b/gl_engine/Texture.cpp index c4413f18..b9620ccc 100644 --- a/gl_engine/Texture.cpp +++ b/gl_engine/Texture.cpp @@ -402,6 +402,8 @@ struct gl_engine::TextureCompressor::Impl { std::unique_ptr dxt1_fragment_program; std::unique_ptr etc1_fragment_program; std::unique_ptr etc1_fast_split_fused_exact_fragment_program; + std::unique_ptr etc1_fast_split_fused_exact_residual_fragment_program; + std::unique_ptr etc1_fast_split_fused_exact_shared_residual_fragment_program; std::unique_ptr checksum_fragment_program; std::unique_ptr packing_program; @@ -489,6 +491,18 @@ struct gl_engine::TextureCompressor::Impl { "texture_compress.vert", ShaderCodeSource::FILE, std::vector { QStringLiteral("#define ALP_COMPRESS_ETC1"), QStringLiteral("#define ALP_COMPRESS_ETC1_SPLIT_FUSED") }); + etc1_fast_split_fused_exact_residual_fragment_program = std::make_unique("texture_compress_raster.vert", + "texture_compress.vert", + ShaderCodeSource::FILE, + std::vector { QStringLiteral("#define ALP_COMPRESS_ETC1"), + QStringLiteral("#define ALP_COMPRESS_ETC1_SPLIT_FUSED"), + QStringLiteral("#define ALP_COMPRESS_ETC1_REFINE_RESIDUAL") }); + etc1_fast_split_fused_exact_shared_residual_fragment_program = std::make_unique("texture_compress_raster.vert", + "texture_compress.vert", + ShaderCodeSource::FILE, + std::vector { QStringLiteral("#define ALP_COMPRESS_ETC1"), + QStringLiteral("#define ALP_COMPRESS_ETC1_SPLIT_FUSED"), + QStringLiteral("#define ALP_COMPRESS_ETC1_REFINE_SHARED_RESIDUAL") }); checksum_fragment_program = std::make_unique("texture_compress_raster.vert", "texture_compress.vert", ShaderCodeSource::FILE, @@ -503,6 +517,8 @@ struct gl_engine::TextureCompressor::Impl { dxt1_fragment_program.reset(); etc1_fragment_program.reset(); etc1_fast_split_fused_exact_fragment_program.reset(); + etc1_fast_split_fused_exact_residual_fragment_program.reset(); + etc1_fast_split_fused_exact_shared_residual_fragment_program.reset(); checksum_fragment_program.reset(); packing_program.reset(); if (!QOpenGLContext::currentContext()) @@ -656,6 +672,10 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: program = m->etc1_fragment_program.get(); if (settings.encoder == Encoder::FastSplitFusedExact) program = m->etc1_fast_split_fused_exact_fragment_program.get(); + else if (settings.encoder == Encoder::FastSplitFusedExactResidual) + program = m->etc1_fast_split_fused_exact_residual_fragment_program.get(); + else if (settings.encoder == Encoder::FastSplitFusedExactSharedResidual) + program = m->etc1_fast_split_fused_exact_shared_residual_fragment_program.get(); } f->glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previous_draw_framebuffer); f->glGetIntegerv(GL_VIEWPORT, previous_viewport); diff --git a/gl_engine/Texture.h b/gl_engine/Texture.h index d30f29d9..3068b8d8 100644 --- a/gl_engine/Texture.h +++ b/gl_engine/Texture.h @@ -95,6 +95,8 @@ class TextureCompressor { Search, Dxt1, FastSplitFusedExact, + FastSplitFusedExactResidual, + FastSplitFusedExactSharedResidual, Checksum }; diff --git a/gl_engine/shaders/texture_compress.vert b/gl_engine/shaders/texture_compress.vert index 75427f29..bdfc35b2 100644 --- a/gl_engine/shaders/texture_compress.vert +++ b/gl_engine/shaders/texture_compress.vert @@ -202,6 +202,74 @@ FastEtc1Bases fast_split_bases(FastEtc1Subblock first, FastEtc1Subblock second) 0u); } +struct FastEtc1Evaluation { + highp uint indices; + highp uint error; + highp ivec3 first_residual; + highp ivec3 second_residual; +}; + +FastEtc1Evaluation evaluate_fast_split(highp uvec3 pixels[16], + FastEtc1Subblock first, + FastEtc1Subblock second, + FastEtc1Bases bases, + bool horizontal) +{ + highp uint indices = 0u; + highp uint total_error = 0u; + highp ivec3 first_residual = ivec3(0); + highp ivec3 second_residual = ivec3(0); + for (int y = 0; y < 4; ++y) { + for (int x = 0; x < 4; ++x) { + highp int pixel_index = y * 4 + x; + highp uvec3 pixel = pixels[pixel_index]; + bool second_subblock = horizontal ? y >= 2 : x >= 2; + highp int table = second_subblock ? second.table : first.table; + highp ivec3 base = second_subblock ? bases.second_decoded : bases.first_decoded; + highp uint selected = select_etc1_modifier_exact(pixel, base, table); + highp ivec3 reconstructed = clamp(base + ivec3(modifier(table, int(selected))), ivec3(0), ivec3(255)); + highp ivec3 delta = ivec3(pixel) - reconstructed; + total_error += uint(delta.x * delta.x + delta.y * delta.y + delta.z * delta.z); +#if defined(ALP_COMPRESS_ETC1_REFINE_RESIDUAL) || defined(ALP_COMPRESS_ETC1_REFINE_SHARED_RESIDUAL) + if (second_subblock) + second_residual += delta; + else + first_residual += delta; +#endif + + highp uint bit_position = uint(x * 4 + y); + indices |= (selected & 1u) << bit_position; + indices |= (selected >> 1u) << (bit_position + 16u); + } + } + return FastEtc1Evaluation(indices, total_error, first_residual, second_residual); +} + +highp uvec2 pack_fast_split(FastEtc1Evaluation evaluation, + FastEtc1Subblock first, + FastEtc1Subblock second, + FastEtc1Bases bases, + bool horizontal) +{ + highp uint control = uint(first.table) << 5u | uint(second.table) << 2u | bases.differential_bit; + if (horizontal) + control |= 1u; + return uvec2(bases.header | control << 24u, byte_swap(evaluation.indices)); +} + +FastEtc1Subblock refit_fast_subblock( + FastEtc1Subblock subblock, highp ivec3 decoded_base, highp ivec3 residual, highp int pixel_count) +{ + highp ivec3 rounded_residual = ivec3(0); + for (int channel = 0; channel < 3; ++channel) { + highp int value = residual[channel]; + highp int rounding = pixel_count / 2; + rounded_residual[channel] + = value >= 0 ? (value + rounding) / pixel_count : -((-value + rounding) / pixel_count); + } + return FastEtc1Subblock(clamp(decoded_base + rounded_residual, ivec3(0), ivec3(255)), subblock.table); +} + highp uvec2 encode_etc1_fast_split_fused(highp uvec3 pixels[16]) { highp uvec3 left_minimum = uvec3(255u); @@ -246,52 +314,50 @@ highp uvec2 encode_etc1_fast_split_fused(highp uvec3 pixels[16]) FastEtc1Subblock bottom = fast_subblock_from_statistics(bottom_minimum, bottom_maximum, bottom_sum); FastEtc1Bases vertical_bases = fast_split_bases(left, right); FastEtc1Bases horizontal_bases = fast_split_bases(top, bottom); + FastEtc1Evaluation vertical_evaluation = evaluate_fast_split(pixels, left, right, vertical_bases, false); + FastEtc1Evaluation horizontal_evaluation = evaluate_fast_split(pixels, top, bottom, horizontal_bases, true); + highp uvec2 vertical = pack_fast_split(vertical_evaluation, left, right, vertical_bases, false); + highp uvec2 horizontal = pack_fast_split(horizontal_evaluation, top, bottom, horizontal_bases, true); - highp uint vertical_indices = 0u; - highp uint horizontal_indices = 0u; - highp uint vertical_error = 0u; - highp uint horizontal_error = 0u; - for (int y = 0; y < 4; ++y) { - for (int x = 0; x < 4; ++x) { - highp int pixel_index = y * 4 + x; - highp uvec3 pixel = pixels[pixel_index]; - bool use_right = x >= 2; - bool use_bottom = y >= 2; - - highp int vertical_table = use_right ? right.table : left.table; - highp ivec3 vertical_base = use_right ? vertical_bases.second_decoded : vertical_bases.first_decoded; - highp uint vertical_selected = select_etc1_modifier_exact(pixel, vertical_base, vertical_table); - highp ivec3 vertical_reconstructed - = clamp(vertical_base + ivec3(modifier(vertical_table, int(vertical_selected))), ivec3(0), ivec3(255)); - highp ivec3 vertical_delta = ivec3(pixel) - vertical_reconstructed; - vertical_error += uint(vertical_delta.x * vertical_delta.x + vertical_delta.y * vertical_delta.y + vertical_delta.z * vertical_delta.z); - - highp int horizontal_table = use_bottom ? bottom.table : top.table; - highp ivec3 horizontal_base = use_bottom ? horizontal_bases.second_decoded : horizontal_bases.first_decoded; - highp uint horizontal_selected = select_etc1_modifier_exact(pixel, horizontal_base, horizontal_table); - highp ivec3 horizontal_reconstructed - = clamp(horizontal_base + ivec3(modifier(horizontal_table, int(horizontal_selected))), ivec3(0), ivec3(255)); - highp ivec3 horizontal_delta = ivec3(pixel) - horizontal_reconstructed; - horizontal_error += uint(horizontal_delta.x * horizontal_delta.x + horizontal_delta.y * horizontal_delta.y + horizontal_delta.z * horizontal_delta.z); - - highp uint bit_position = uint(x * 4 + y); - vertical_indices |= (vertical_selected & 1u) << bit_position; - vertical_indices |= (vertical_selected >> 1u) << (bit_position + 16u); - horizontal_indices |= (horizontal_selected & 1u) << bit_position; - horizontal_indices |= (horizontal_selected >> 1u) << (bit_position + 16u); - } +#if !defined(ALP_COMPRESS_ETC1_REFINE_RESIDUAL) && !defined(ALP_COMPRESS_ETC1_REFINE_SHARED_RESIDUAL) + return horizontal_evaluation.error < vertical_evaluation.error ? horizontal : vertical; +#else + bool horizontal_wins = horizontal_evaluation.error < vertical_evaluation.error; + FastEtc1Subblock first = left; + FastEtc1Subblock second = right; + FastEtc1Bases best_bases = vertical_bases; + FastEtc1Evaluation best_evaluation = vertical_evaluation; + highp uvec2 best_block = vertical; + if (horizontal_wins) { + first = top; + second = bottom; + best_bases = horizontal_bases; + best_evaluation = horizontal_evaluation; + best_block = horizontal; } +#ifdef ALP_COMPRESS_ETC1_REFINE_SHARED_RESIDUAL + highp ivec3 combined_residual = best_evaluation.first_residual + best_evaluation.second_residual; + FastEtc1Subblock candidate_first + = refit_fast_subblock(first, best_bases.first_decoded, combined_residual, 16); + FastEtc1Subblock candidate_second + = refit_fast_subblock(second, best_bases.second_decoded, combined_residual, 16); +#else + FastEtc1Subblock candidate_first + = refit_fast_subblock(first, best_bases.first_decoded, best_evaluation.first_residual, 8); + FastEtc1Subblock candidate_second + = refit_fast_subblock(second, best_bases.second_decoded, best_evaluation.second_residual, 8); +#endif + FastEtc1Bases candidate_bases = fast_split_bases(candidate_first, candidate_second); + FastEtc1Evaluation candidate_evaluation + = evaluate_fast_split(pixels, candidate_first, candidate_second, candidate_bases, horizontal_wins); + if (candidate_evaluation.error < best_evaluation.error) + best_block = pack_fast_split(candidate_evaluation, candidate_first, candidate_second, candidate_bases, horizontal_wins); - highp uint vertical_control - = uint(left.table) << 5u | uint(right.table) << 2u | vertical_bases.differential_bit; - highp uint horizontal_control - = uint(top.table) << 5u | uint(bottom.table) << 2u | horizontal_bases.differential_bit | 1u; - highp uvec2 vertical = uvec2(vertical_bases.header | vertical_control << 24u, byte_swap(vertical_indices)); - highp uvec2 horizontal = uvec2(horizontal_bases.header | horizontal_control << 24u, byte_swap(horizontal_indices)); - return horizontal_error < vertical_error ? horizontal : vertical; + return best_block; +#endif } -highp uvec2 encode_etc1(highp uvec3 pixels[16]) +highp uvec2 encode_etc1(highp uvec3 pixels[16], highp int search_effort) { highp uvec3 sum = uvec3(0u); for (int i = 0; i < 16; ++i) @@ -303,7 +369,7 @@ highp uvec2 encode_etc1(highp uvec3 pixels[16]) highp uint best_table = 0u; highp uint best_indices = 0u; for (int candidate = 0; candidate <= 10; ++candidate) { - if (candidate > effort) + if (candidate > search_effort) break; highp int magnitude = ((candidate + 1) / 2) * 4; highp int signed_offset = candidate == 0 ? 0 : ((candidate & 1) == 1 ? magnitude : -magnitude); @@ -375,7 +441,7 @@ highp uvec2 compress_block(highp ivec2 block, #ifdef ALP_COMPRESS_ETC1_SPLIT_FUSED return encode_etc1_fast_split_fused(pixels); #else - return encode_etc1(pixels); + return encode_etc1(pixels, effort); #endif #else return encode_dxt1(pixels); diff --git a/unittests/texture_compression_benchmark/main.cpp b/unittests/texture_compression_benchmark/main.cpp index 4f33764b..60950e0e 100644 --- a/unittests/texture_compression_benchmark/main.cpp +++ b/unittests/texture_compression_benchmark/main.cpp @@ -312,6 +312,11 @@ std::vector supported_algorithms(Format format) result.push_back({ "DXT1", Operation::Compression, settings(Encoder::Dxt1) }); } else if (format == Format::ETC1) { result.push_back({ "ETC1 fused exact", Operation::Compression, settings(Encoder::FastSplitFusedExact) }); + result.push_back( + { "ETC1 fused exact residual fit", Operation::Compression, settings(Encoder::FastSplitFusedExactResidual) }); + result.push_back({ "ETC1 fused exact shared residual", + Operation::Compression, + settings(Encoder::FastSplitFusedExactSharedResidual) }); } return result; } From 2f745698156f2da83b88358be1a5f0b6ca2d628c Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Sun, 23 Aug 2026 16:44:56 +0200 Subject: [PATCH 30/38] Use Radix to stitch compression benchmark tiles --- .../texture_compression_benchmark/main.cpp | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/unittests/texture_compression_benchmark/main.cpp b/unittests/texture_compression_benchmark/main.cpp index 60950e0e..24d836ce 100644 --- a/unittests/texture_compression_benchmark/main.cpp +++ b/unittests/texture_compression_benchmark/main.cpp @@ -16,7 +16,6 @@ #include #include #include -#include #include #include #include @@ -34,13 +33,13 @@ #include #include #include +#include #include #include #include #include #include -#include #include namespace { @@ -362,11 +361,11 @@ class BenchmarkWindow final : public QOpenGLWindow { auto* reply = m_network_manager.get(QNetworkRequest(QUrl(url))); connect(reply, &QNetworkReply::finished, this, [this, reply, tile_index, url]() { if (reply->error() == QNetworkReply::NoError) { - const auto image = nucleus::utils::image_loader::rgba8(reply->readAll()); + auto image = nucleus::utils::image_loader::rgba8(reply->readAll()); if (image && image->size() == glm::uvec2(256u)) - m_downloaded_tiles[tile_index] = nucleus::tile::conversion::to_QImage(*image); + m_downloaded_tiles[tile_index] = std::move(*image); } - if (m_downloaded_tiles[tile_index].isNull() && m_download_error.isEmpty()) + if (m_downloaded_tiles[tile_index].size() != glm::uvec2(256u) && m_download_error.isEmpty()) m_download_error = QStringLiteral("Unable to download benchmark tile: %1").arg(url); reply->deleteLater(); if (--m_downloads_remaining == 0) @@ -387,16 +386,22 @@ class BenchmarkWindow final : public QOpenGLWindow { m_sources.clear(); m_sources.reserve(texture_compression_data::tile_groups.size()); for (size_t group_index = 0; group_index < texture_compression_data::tile_groups.size(); ++group_index) { - QImage stitched(int(resolution), int(resolution), QImage::Format_RGBA8888); - QPainter painter(&stitched); - for (int y = 0; y < 2; ++y) { - for (int x = 0; x < 2; ++x) { - painter.drawImage(QPoint(x * 256, y * 256), - m_downloaded_tiles[group_index * 4 + size_t(y * 2 + x)]); - } + const auto tile_offset = group_index * 4; + auto top = radix::raster::concatenate_horizontally( + m_downloaded_tiles[tile_offset], m_downloaded_tiles[tile_offset + 1]); + auto bottom = radix::raster::concatenate_horizontally( + m_downloaded_tiles[tile_offset + 2], m_downloaded_tiles[tile_offset + 3]); + if (!top || !bottom) { + fail(QStringLiteral("Unable to stitch benchmark tile row.")); + return; + } + + auto stitched = radix::raster::concatenate_vertically(*top, *bottom); + if (!stitched) { + fail(QStringLiteral("Unable to stitch benchmark tile group.")); + return; } - painter.end(); - m_sources.push_back(nucleus::tile::conversion::to_rgba8raster(stitched)); + m_sources.push_back(std::move(*stitched)); } m_downloaded_tiles.clear(); m_data_ready = true; @@ -677,7 +682,7 @@ class BenchmarkWindow final : public QOpenGLWindow { } QNetworkAccessManager m_network_manager; - std::vector m_downloaded_tiles; + std::vector m_downloaded_tiles; std::vector m_sources; QString m_download_error; int m_downloads_remaining = 0; From 71884b21583b8942ec7f2f9b1b4910618db26994 Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Sun, 23 Aug 2026 23:02:13 +0200 Subject: [PATCH 31/38] Benchmark texture compression readback variants --- gl_engine/Texture.cpp | 214 +++++--- gl_engine/Texture.h | 7 + gl_engine/shaders/texture_compress.vert | 30 +- .../texture_compression_benchmark/main.cpp | 475 ++++++++++++------ 4 files changed, 488 insertions(+), 238 deletions(-) diff --git a/gl_engine/Texture.cpp b/gl_engine/Texture.cpp index b9620ccc..deb2a1b5 100644 --- a/gl_engine/Texture.cpp +++ b/gl_engine/Texture.cpp @@ -390,13 +390,17 @@ struct gl_engine::TextureCompressor::Impl { unsigned scratch_layers = 0; GLsizei block_atlas_width = 0; GLsizei block_atlas_height = 0; + GLsizei paired_atlas_width = 0; + GLsizei paired_atlas_height = 0; GLsizei output_atlas_width = 0; GLsizei output_atlas_height = 0; GLuint scratch_texture = 0; GLuint encoded_texture = 0; + GLuint paired_encoded_texture = 0; GLuint encoded_buffer = 0; GLuint vertex_array = 0; GLuint encoding_framebuffer = 0; + GLuint paired_encoding_framebuffer = 0; GLuint packing_framebuffer = 0; GLuint packing_renderbuffer = 0; std::unique_ptr dxt1_fragment_program; @@ -405,6 +409,12 @@ struct gl_engine::TextureCompressor::Impl { std::unique_ptr etc1_fast_split_fused_exact_residual_fragment_program; std::unique_ptr etc1_fast_split_fused_exact_shared_residual_fragment_program; std::unique_ptr checksum_fragment_program; + std::unique_ptr paired_dxt1_fragment_program; + std::unique_ptr paired_etc1_fragment_program; + std::unique_ptr paired_etc1_fast_split_fused_exact_fragment_program; + std::unique_ptr paired_etc1_fast_split_fused_exact_residual_fragment_program; + std::unique_ptr paired_etc1_fast_split_fused_exact_shared_residual_fragment_program; + std::unique_ptr paired_checksum_fragment_program; std::unique_ptr packing_program; Impl(unsigned texture_width, unsigned texture_height, unsigned maximum_batch_size) @@ -433,12 +443,19 @@ struct gl_engine::TextureCompressor::Impl { return std::pair(atlas_width, atlas_height); }; std::tie(block_atlas_width, block_atlas_height) = atlas_size(maximum_size / 8, maximum_texture_size); + std::tie(paired_atlas_width, paired_atlas_height) + = atlas_size((maximum_size + 15) / 16, maximum_texture_size); std::tie(output_atlas_width, output_atlas_height) = atlas_size(maximum_size / 4, maximum_renderbuffer_size); f->glGenBuffers(1, &encoded_buffer); f->glBindBuffer(GL_PIXEL_PACK_BUFFER, encoded_buffer); + const auto encoded_buffer_size = std::max({ + size_t(block_atlas_width) * size_t(block_atlas_height) * 8, + size_t(paired_atlas_width) * size_t(paired_atlas_height) * 16, + size_t(output_atlas_width) * size_t(output_atlas_height) * 4, + }); f->glBufferData(GL_PIXEL_PACK_BUFFER, - GLsizeiptr(size_t(output_atlas_width) * size_t(output_atlas_height) * 4), + GLsizeiptr(encoded_buffer_size), nullptr, GL_STREAM_DRAW); f->glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); @@ -466,6 +483,19 @@ struct gl_engine::TextureCompressor::Impl { f->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, encoded_texture, 0); Q_ASSERT(f->glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE); + f->glGenTextures(1, &paired_encoded_texture); + f->glBindTexture(GL_TEXTURE_2D, paired_encoded_texture); + f->glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA32UI, paired_atlas_width, paired_atlas_height); + f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + f->glGenFramebuffers(1, &paired_encoding_framebuffer); + f->glBindFramebuffer(GL_FRAMEBUFFER, paired_encoding_framebuffer); + f->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, paired_encoded_texture, 0); + Q_ASSERT(f->glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE); + f->glGenFramebuffers(1, &packing_framebuffer); f->glGenRenderbuffers(1, &packing_renderbuffer); f->glBindRenderbuffer(GL_RENDERBUFFER, packing_renderbuffer); @@ -480,36 +510,66 @@ struct gl_engine::TextureCompressor::Impl { f->glBindRenderbuffer(GL_RENDERBUFFER, GLuint(previous_renderbuffer)); f->glBindTexture(GL_TEXTURE_2D, GLuint(previous_texture)); - dxt1_fragment_program = std::make_unique("texture_compress_raster.vert", - "texture_compress.vert", - ShaderCodeSource::FILE); - etc1_fragment_program = std::make_unique("texture_compress_raster.vert", - "texture_compress.vert", - ShaderCodeSource::FILE, - std::vector { QStringLiteral("#define ALP_COMPRESS_ETC1") }); - etc1_fast_split_fused_exact_fragment_program = std::make_unique("texture_compress_raster.vert", - "texture_compress.vert", - ShaderCodeSource::FILE, - std::vector { QStringLiteral("#define ALP_COMPRESS_ETC1"), QStringLiteral("#define ALP_COMPRESS_ETC1_SPLIT_FUSED") }); - etc1_fast_split_fused_exact_residual_fragment_program = std::make_unique("texture_compress_raster.vert", - "texture_compress.vert", - ShaderCodeSource::FILE, - std::vector { QStringLiteral("#define ALP_COMPRESS_ETC1"), - QStringLiteral("#define ALP_COMPRESS_ETC1_SPLIT_FUSED"), - QStringLiteral("#define ALP_COMPRESS_ETC1_REFINE_RESIDUAL") }); - etc1_fast_split_fused_exact_shared_residual_fragment_program = std::make_unique("texture_compress_raster.vert", - "texture_compress.vert", - ShaderCodeSource::FILE, - std::vector { QStringLiteral("#define ALP_COMPRESS_ETC1"), - QStringLiteral("#define ALP_COMPRESS_ETC1_SPLIT_FUSED"), - QStringLiteral("#define ALP_COMPRESS_ETC1_REFINE_SHARED_RESIDUAL") }); - checksum_fragment_program = std::make_unique("texture_compress_raster.vert", - "texture_compress.vert", - ShaderCodeSource::FILE, - std::vector { QStringLiteral("#define ALP_COMPRESS_CHECKSUM") }); - packing_program = std::make_unique( - "texture_compress_raster.vert", "texture_compress_pack.frag", ShaderCodeSource::FILE); + } + ShaderProgram* paired_program(Encoder encoder, nucleus::utils::ColourTexture::Format algorithm) + { + auto* program = &paired_dxt1_fragment_program; + std::vector defines { QStringLiteral("#define ALP_COMPRESS_TWO_BLOCKS") }; + if (encoder == Encoder::Checksum) { + program = &paired_checksum_fragment_program; + defines.push_back(QStringLiteral("#define ALP_COMPRESS_CHECKSUM")); + } else if (algorithm == nucleus::utils::ColourTexture::Format::ETC1) { + program = &paired_etc1_fragment_program; + defines.push_back(QStringLiteral("#define ALP_COMPRESS_ETC1")); + if (encoder == Encoder::FastSplitFusedExact) { + program = &paired_etc1_fast_split_fused_exact_fragment_program; + defines.push_back(QStringLiteral("#define ALP_COMPRESS_ETC1_SPLIT_FUSED")); + } else if (encoder == Encoder::FastSplitFusedExactResidual) { + program = &paired_etc1_fast_split_fused_exact_residual_fragment_program; + defines.push_back(QStringLiteral("#define ALP_COMPRESS_ETC1_SPLIT_FUSED")); + defines.push_back(QStringLiteral("#define ALP_COMPRESS_ETC1_REFINE_RESIDUAL")); + } else if (encoder == Encoder::FastSplitFusedExactSharedResidual) { + program = &paired_etc1_fast_split_fused_exact_shared_residual_fragment_program; + defines.push_back(QStringLiteral("#define ALP_COMPRESS_ETC1_SPLIT_FUSED")); + defines.push_back(QStringLiteral("#define ALP_COMPRESS_ETC1_REFINE_SHARED_RESIDUAL")); + } + } + if (!*program) { + *program = std::make_unique( + "texture_compress_raster.vert", "texture_compress.vert", ShaderCodeSource::FILE, defines); + } + return program->get(); + } + + ShaderProgram* single_program(Encoder encoder, nucleus::utils::ColourTexture::Format algorithm) + { + auto* program = &dxt1_fragment_program; + std::vector defines; + if (encoder == Encoder::Checksum) { + program = &checksum_fragment_program; + defines.push_back(QStringLiteral("#define ALP_COMPRESS_CHECKSUM")); + } else if (algorithm == nucleus::utils::ColourTexture::Format::ETC1) { + program = &etc1_fragment_program; + defines.push_back(QStringLiteral("#define ALP_COMPRESS_ETC1")); + if (encoder == Encoder::FastSplitFusedExact) { + program = &etc1_fast_split_fused_exact_fragment_program; + defines.push_back(QStringLiteral("#define ALP_COMPRESS_ETC1_SPLIT_FUSED")); + } else if (encoder == Encoder::FastSplitFusedExactResidual) { + program = &etc1_fast_split_fused_exact_residual_fragment_program; + defines.push_back(QStringLiteral("#define ALP_COMPRESS_ETC1_SPLIT_FUSED")); + defines.push_back(QStringLiteral("#define ALP_COMPRESS_ETC1_REFINE_RESIDUAL")); + } else if (encoder == Encoder::FastSplitFusedExactSharedResidual) { + program = &etc1_fast_split_fused_exact_shared_residual_fragment_program; + defines.push_back(QStringLiteral("#define ALP_COMPRESS_ETC1_SPLIT_FUSED")); + defines.push_back(QStringLiteral("#define ALP_COMPRESS_ETC1_REFINE_SHARED_RESIDUAL")); + } + } + if (!*program) { + *program = std::make_unique( + "texture_compress_raster.vert", "texture_compress.vert", ShaderCodeSource::FILE, defines); + } + return program->get(); } ~Impl() @@ -520,14 +580,22 @@ struct gl_engine::TextureCompressor::Impl { etc1_fast_split_fused_exact_residual_fragment_program.reset(); etc1_fast_split_fused_exact_shared_residual_fragment_program.reset(); checksum_fragment_program.reset(); + paired_dxt1_fragment_program.reset(); + paired_etc1_fragment_program.reset(); + paired_etc1_fast_split_fused_exact_fragment_program.reset(); + paired_etc1_fast_split_fused_exact_residual_fragment_program.reset(); + paired_etc1_fast_split_fused_exact_shared_residual_fragment_program.reset(); + paired_checksum_fragment_program.reset(); packing_program.reset(); if (!QOpenGLContext::currentContext()) return; auto* f = QOpenGLContext::currentContext()->extraFunctions(); f->glDeleteFramebuffers(1, &encoding_framebuffer); + f->glDeleteFramebuffers(1, &paired_encoding_framebuffer); f->glDeleteFramebuffers(1, &packing_framebuffer); f->glDeleteRenderbuffers(1, &packing_renderbuffer); f->glDeleteTextures(1, &encoded_texture); + f->glDeleteTextures(1, &paired_encoded_texture); f->glDeleteVertexArrays(1, &vertex_array); f->glDeleteBuffers(1, &encoded_buffer); if (scratch_texture) @@ -663,20 +731,16 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: GLboolean cull_enabled = GL_FALSE; GLboolean depth_enabled = GL_FALSE; GLboolean scissor_enabled = GL_FALSE; + const auto paired_blocks = settings.transfer_mode == TransferMode::PairedRGBA32UI; + const auto total_blocks = total_encoded_size / 8; + const auto encoding_pixels = paired_blocks ? (total_blocks + 1) / 2 : total_blocks; + const auto maximum_encoding_width = paired_blocks ? m->paired_atlas_width : m->block_atlas_width; + const auto encoding_width = GLsizei(std::min(encoding_pixels, size_t(maximum_encoding_width))); + const auto encoding_height = GLsizei((encoding_pixels + size_t(encoding_width) - 1) / size_t(encoding_width)); { - auto* program = m->dxt1_fragment_program.get(); - if (settings.encoder == Encoder::Checksum) { - program = m->checksum_fragment_program.get(); - } else if (settings.algorithm == nucleus::utils::ColourTexture::Format::ETC1) { - program = m->etc1_fragment_program.get(); - if (settings.encoder == Encoder::FastSplitFusedExact) - program = m->etc1_fast_split_fused_exact_fragment_program.get(); - else if (settings.encoder == Encoder::FastSplitFusedExactResidual) - program = m->etc1_fast_split_fused_exact_residual_fragment_program.get(); - else if (settings.encoder == Encoder::FastSplitFusedExactSharedResidual) - program = m->etc1_fast_split_fused_exact_shared_residual_fragment_program.get(); - } + auto* program = paired_blocks ? m->paired_program(settings.encoder, settings.algorithm) + : m->single_program(settings.encoder, settings.algorithm); f->glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previous_draw_framebuffer); f->glGetIntegerv(GL_VIEWPORT, previous_viewport); f->glGetBooleanv(GL_COLOR_WRITEMASK, previous_colour_mask); @@ -691,15 +755,16 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: f->glDisable(GL_SCISSOR_TEST); f->glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); - f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m->encoding_framebuffer); - f->glViewport(0, 0, m->block_atlas_width, m->block_atlas_height); + f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, + paired_blocks ? m->paired_encoding_framebuffer : m->encoding_framebuffer); + f->glViewport(0, 0, encoding_width, encoding_height); program->bind(); program->set_uniform("source_texture", 7); program->set_uniform("texture_width", int(m->width)); program->set_uniform("texture_height", int(m->height)); program->set_uniform("effort", int(settings.effort)); - program->set_uniform("atlas_width", int(m->block_atlas_width)); - program->set_uniform("total_blocks", int(total_encoded_size / 8)); + program->set_uniform("atlas_width", int(encoding_width)); + program->set_uniform("total_blocks", int(total_blocks)); program->set_uniform("mip_levels", int(result.mip_levels)); program->set_uniform_array("level_offsets", level_offsets_blocks); program->set_uniform_array("level_blocks_x", level_blocks_x); @@ -708,35 +773,52 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: f->glBindTexture(GL_TEXTURE_2D_ARRAY, m->scratch_texture); f->glBindVertexArray(m->vertex_array); f->glDrawArrays(GL_TRIANGLES, 0, 3); + program->release(); } - { + GLsizei readback_width = encoding_width; + GLsizei readback_height = encoding_height; + GLuint readback_framebuffer = paired_blocks ? m->paired_encoding_framebuffer : m->encoding_framebuffer; + GLenum readback_format = paired_blocks ? GL_RGBA_INTEGER : GL_RG_INTEGER; + GLenum readback_type = GL_UNSIGNED_INT; + if (settings.transfer_mode == TransferMode::PackedRGBA8) { + if (!m->packing_program) { + m->packing_program = std::make_unique( + "texture_compress_raster.vert", "texture_compress_pack.frag", ShaderCodeSource::FILE); + } + const auto output_pixels = total_blocks * 2; + readback_width = GLsizei(std::min(output_pixels, size_t(m->output_atlas_width))); + readback_height = GLsizei((output_pixels + size_t(readback_width) - 1) / size_t(readback_width)); + readback_framebuffer = m->packing_framebuffer; + readback_format = GL_RGBA_INTEGER; + readback_type = GL_UNSIGNED_BYTE; + f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m->packing_framebuffer); - f->glViewport(0, 0, m->output_atlas_width, m->output_atlas_height); + f->glViewport(0, 0, readback_width, readback_height); m->packing_program->bind(); m->packing_program->set_uniform("encoded_blocks", 6); - m->packing_program->set_uniform("block_atlas_width", int(m->block_atlas_width)); - m->packing_program->set_uniform("output_atlas_width", int(m->output_atlas_width)); - m->packing_program->set_uniform("total_blocks", int(total_encoded_size / 8)); + m->packing_program->set_uniform("block_atlas_width", int(encoding_width)); + m->packing_program->set_uniform("output_atlas_width", int(readback_width)); + m->packing_program->set_uniform("total_blocks", int(total_blocks)); f->glActiveTexture(GL_TEXTURE6); f->glBindTexture(GL_TEXTURE_2D, m->encoded_texture); f->glDrawArrays(GL_TRIANGLES, 0, 3); - f->glBindVertexArray(0); m->packing_program->release(); - - if (blend_enabled) - f->glEnable(GL_BLEND); - if (cull_enabled) - f->glEnable(GL_CULL_FACE); - if (depth_enabled) - f->glEnable(GL_DEPTH_TEST); - if (scissor_enabled) - f->glEnable(GL_SCISSOR_TEST); - f->glColorMask(previous_colour_mask[0], previous_colour_mask[1], previous_colour_mask[2], previous_colour_mask[3]); - f->glViewport(previous_viewport[0], previous_viewport[1], previous_viewport[2], previous_viewport[3]); - f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, GLuint(previous_draw_framebuffer)); } + f->glBindVertexArray(0); + if (blend_enabled) + f->glEnable(GL_BLEND); + if (cull_enabled) + f->glEnable(GL_CULL_FACE); + if (depth_enabled) + f->glEnable(GL_DEPTH_TEST); + if (scissor_enabled) + f->glEnable(GL_SCISSOR_TEST); + f->glColorMask(previous_colour_mask[0], previous_colour_mask[1], previous_colour_mask[2], previous_colour_mask[3]); + f->glViewport(previous_viewport[0], previous_viewport[1], previous_viewport[2], previous_viewport[3]); + f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, GLuint(previous_draw_framebuffer)); + { GLint previous_read_framebuffer = 0; GLint previous_read_buffer = 0; @@ -744,11 +826,11 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: f->glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &previous_read_framebuffer); f->glGetIntegerv(GL_READ_BUFFER, &previous_read_buffer); f->glGetIntegerv(GL_PACK_ALIGNMENT, &previous_pack_alignment); - f->glBindFramebuffer(GL_READ_FRAMEBUFFER, m->packing_framebuffer); + f->glBindFramebuffer(GL_READ_FRAMEBUFFER, readback_framebuffer); f->glReadBuffer(GL_COLOR_ATTACHMENT0); f->glPixelStorei(GL_PACK_ALIGNMENT, 1); f->glBindBuffer(GL_PIXEL_PACK_BUFFER, m->encoded_buffer); - f->glReadPixels(0, 0, m->output_atlas_width, m->output_atlas_height, GL_RGBA_INTEGER, GL_UNSIGNED_BYTE, nullptr); + f->glReadPixels(0, 0, readback_width, readback_height, readback_format, readback_type, nullptr); f->glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); f->glPixelStorei(GL_PACK_ALIGNMENT, previous_pack_alignment); f->glBindFramebuffer(GL_READ_FRAMEBUFFER, GLuint(previous_read_framebuffer)); diff --git a/gl_engine/Texture.h b/gl_engine/Texture.h index 3068b8d8..083943a4 100644 --- a/gl_engine/Texture.h +++ b/gl_engine/Texture.h @@ -100,11 +100,18 @@ class TextureCompressor { Checksum }; + enum class TransferMode { + PackedRGBA8, + DirectRG32UI, + PairedRGBA32UI, + }; + struct Settings { nucleus::utils::ColourTexture::Format algorithm = nucleus::utils::ColourTexture::Format::DXT1; unsigned effort = 0; Encoder encoder = Encoder::Search; bool generate_mipmaps = true; + TransferMode transfer_mode = TransferMode::PackedRGBA8; }; struct Result { diff --git a/gl_engine/shaders/texture_compress.vert b/gl_engine/shaders/texture_compress.vert index bdfc35b2..5d589503 100644 --- a/gl_engine/shaders/texture_compress.vert +++ b/gl_engine/shaders/texture_compress.vert @@ -8,7 +8,11 @@ uniform highp int mip_levels; uniform highp int level_offsets[max_mip_levels]; uniform highp int level_blocks_x[max_mip_levels]; uniform highp int level_blocks_y[max_mip_levels]; +#ifdef ALP_COMPRESS_TWO_BLOCKS +layout(location = 0) out highp uvec4 encoded_blocks; +#else layout(location = 0) out highp uvec2 encoded_block; +#endif uniform highp int effort; highp uvec3 unpack_565(highp uint value) @@ -448,12 +452,8 @@ highp uvec2 compress_block(highp ivec2 block, #endif } -void main() +highp uvec2 compress_block_at_index(highp int output_index) { - highp int output_index = int(gl_FragCoord.y) * atlas_width + int(gl_FragCoord.x); - if (output_index >= total_blocks) - discard; - highp int level = 0; for (int candidate = 1; candidate < max_mip_levels; ++candidate) { if (candidate >= mip_levels || output_index < level_offsets[candidate]) @@ -468,5 +468,23 @@ void main() highp int layer = level_index / blocks_per_layer; highp int block_index = level_index - layer * blocks_per_layer; highp ivec2 block = ivec2(block_index % blocks_x_at_level, block_index / blocks_x_at_level); - encoded_block = compress_block(block, layer, level, max(1, texture_width >> level), max(1, texture_height >> level)); + return compress_block(block, layer, level, max(1, texture_width >> level), max(1, texture_height >> level)); +} + +void main() +{ + highp int output_index = int(gl_FragCoord.y) * atlas_width + int(gl_FragCoord.x); +#ifdef ALP_COMPRESS_TWO_BLOCKS + output_index *= 2; +#endif + if (output_index >= total_blocks) + discard; + +#ifdef ALP_COMPRESS_TWO_BLOCKS + highp uvec2 first = compress_block_at_index(output_index); + highp uvec2 second = output_index + 1 < total_blocks ? compress_block_at_index(output_index + 1) : uvec2(0u); + encoded_blocks = uvec4(first, second); +#else + encoded_block = compress_block_at_index(output_index); +#endif } diff --git a/unittests/texture_compression_benchmark/main.cpp b/unittests/texture_compression_benchmark/main.cpp index 24d836ce..4d5df034 100644 --- a/unittests/texture_compression_benchmark/main.cpp +++ b/unittests/texture_compression_benchmark/main.cpp @@ -47,9 +47,11 @@ using Clock = std::chrono::steady_clock; using Raster = radix::Raster; using Format = nucleus::utils::ColourTexture::Format; using Encoder = gl_engine::TextureCompressor::Encoder; +using TransferMode = gl_engine::TextureCompressor::TransferMode; constexpr unsigned resolution = 512; -constexpr unsigned batch_size = 4; +constexpr unsigned max_batch_size = 4; +constexpr std::array batch_sizes { 1, 2, 4 }; constexpr unsigned framebuffer_size = 32; constexpr int warmup_batches = 2; constexpr int measured_batches = 10; @@ -61,7 +63,7 @@ constexpr int ssim_window_radius = ssim_window_size / 2; enum class Operation { SamplingOnly, Compression }; struct Workload { - std::array source_indices {}; + std::array source_indices {}; uint32_t sampling_seed = 0; }; @@ -295,27 +297,44 @@ const char* format_name(Format format) std::vector supported_algorithms(Format format) { - const auto settings = [format](Encoder encoder) { + const auto settings = [format](Encoder encoder, TransferMode transfer_mode) { return gl_engine::TextureCompressor::Settings { .algorithm = format, .effort = 0, .encoder = encoder, .generate_mipmaps = true, + .transfer_mode = transfer_mode, }; }; std::vector result; - result.push_back({ "sampling only", Operation::SamplingOnly, settings(Encoder::Checksum) }); - result.push_back({ "checksum", Operation::Compression, settings(Encoder::Checksum), true }); - if (format == Format::DXT1) { - result.push_back({ "DXT1", Operation::Compression, settings(Encoder::Dxt1) }); - } else if (format == Format::ETC1) { - result.push_back({ "ETC1 fused exact", Operation::Compression, settings(Encoder::FastSplitFusedExact) }); - result.push_back( - { "ETC1 fused exact residual fit", Operation::Compression, settings(Encoder::FastSplitFusedExactResidual) }); - result.push_back({ "ETC1 fused exact shared residual", + result.push_back( + { "sampling only", Operation::SamplingOnly, settings(Encoder::Checksum, TransferMode::PackedRGBA8) }); + const std::array, 3> transfer_modes { { + { TransferMode::PackedRGBA8, "packed RGBA8" }, + { TransferMode::DirectRG32UI, "direct RG32UI" }, + { TransferMode::PairedRGBA32UI, "paired RGBA32UI" }, + } }; + for (const auto& [transfer_mode, transfer_name] : transfer_modes) { + result.push_back({ std::string("checksum / ") + transfer_name, Operation::Compression, - settings(Encoder::FastSplitFusedExactSharedResidual) }); + settings(Encoder::Checksum, transfer_mode), + true }); + if (format == Format::DXT1) { + result.push_back({ std::string("DXT1 / ") + transfer_name, + Operation::Compression, + settings(Encoder::Dxt1, transfer_mode) }); + } else if (format == Format::ETC1) { + result.push_back({ std::string("ETC1 fused exact / ") + transfer_name, + Operation::Compression, + settings(Encoder::FastSplitFusedExact, transfer_mode) }); + result.push_back({ std::string("ETC1 fused exact residual fit / ") + transfer_name, + Operation::Compression, + settings(Encoder::FastSplitFusedExactResidual, transfer_mode) }); + result.push_back({ std::string("ETC1 fused exact shared residual / ") + transfer_name, + Operation::Compression, + settings(Encoder::FastSplitFusedExactSharedResidual, transfer_mode) }); + } } return result; } @@ -340,14 +359,52 @@ class BenchmarkWindow final : public QOpenGLWindow { void paintGL() override { - if (!m_data_ready || m_benchmark_started) + if (!m_data_ready) + return; + const auto successful = m_benchmark_state ? advance_benchmark() : begin_benchmark(); + if (!successful) { + QTimer::singleShot(0, qApp, []() { QCoreApplication::exit(EXIT_FAILURE); }); + return; + } + if (m_batch_index == batch_sizes.size() && !m_benchmark_state) { +#if defined(__EMSCRIPTEN__) + qInfo().noquote() << QStringLiteral("Benchmark complete."); + m_data_ready = false; +#else + QTimer::singleShot(0, qApp, []() { QCoreApplication::exit(EXIT_SUCCESS); }); +#endif return; - m_benchmark_started = true; - const auto successful = run_benchmark(); - QTimer::singleShot(0, qApp, [successful]() { QCoreApplication::exit(successful ? EXIT_SUCCESS : EXIT_FAILURE); }); + } + QTimer::singleShot(10, this, [this]() { update(); }); } private: + struct BenchmarkState { + enum class Phase { Timing, QualitySetup, Quality, Report }; + + unsigned batch_size = 0; + Format format = Format::Uncompressed_RGBA; + std::vector algorithms; + std::unique_ptr destination; + std::unique_ptr compressor; + std::unique_ptr framebuffer; + std::unique_ptr sampling_shader; + gl_engine::helpers::ScreenQuadGeometry sampling_geometry; + std::array destination_layers { 0, 1, 2, 3 }; + std::mt19937 random_engine { random_seed }; + std::array, repetitions> workloads; + std::vector algorithm_order; + size_t startup_step = 0; + bool destination_initialised = false; + int repetition = 0; + size_t algorithm_position = 0; + Phase phase = Phase::Timing; + std::vector quality_references; + std::unique_ptr quality_framebuffer; + std::unique_ptr quality_shader; + size_t quality_algorithm = 0; + }; + void download_data() { m_downloads_remaining = int(m_downloaded_tiles.size()); @@ -409,23 +466,25 @@ class BenchmarkWindow final : public QOpenGLWindow { update(); } - bool run_benchmark() + bool begin_benchmark() { - const auto format = gl_engine::Texture::compression_algorithm(); - auto algorithms = supported_algorithms(format); - if (algorithms.size() <= 2) { + Q_ASSERT(m_batch_index < batch_sizes.size()); + auto state = std::make_unique(); + state->batch_size = batch_sizes[m_batch_index]; + state->format = gl_engine::Texture::compression_algorithm(); + state->algorithms = supported_algorithms(state->format); + if (state->algorithms.size() <= 2) { qInfo().noquote() << QStringLiteral("No supported GPU compression algorithm was found."); return false; } + qInfo().noquote() << QStringLiteral("Initialising batch size %1...").arg(state->batch_size); + m_benchmark_state = std::move(state); + return true; + } - gl_engine::Texture destination(gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); - destination.setParams(gl_engine::Texture::Filter::MipMapLinear, gl_engine::Texture::Filter::Linear); - destination.allocate_array(resolution, resolution, batch_size); - gl_engine::TextureCompressor compressor(resolution, resolution, batch_size); - gl_engine::Framebuffer framebuffer(gl_engine::Framebuffer::DepthFormat::None, - { gl_engine::Framebuffer::ColourFormat::RGBA8 }, - { framebuffer_size, framebuffer_size }); - gl_engine::ShaderProgram sampling_shader(R"( + std::unique_ptr make_sampling_shader() + { + return std::make_unique(R"( void main() { highp vec2 vertices[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0); @@ -433,6 +492,7 @@ class BenchmarkWindow final : public QOpenGLWindow { R"( uniform lowp sampler2DArray texture_sampler; uniform highp uint sampling_seed; + uniform highp uint active_layers; layout(location = 0) out lowp vec4 out_color; highp uint hash(highp uint value) { @@ -451,7 +511,7 @@ class BenchmarkWindow final : public QOpenGLWindow { random_value = hash(random_value); highp float y = float(random_value & 0xffffu) / 65535.0; random_value = hash(random_value); - highp float layer = float(random_value % 4u); + highp float layer = float(random_value % active_layers); random_value = hash(random_value); highp float level = float(random_value % 10u); lowp vec4 sampled = textureLod(texture_sampler, vec3(x, y, layer), level); @@ -461,105 +521,125 @@ class BenchmarkWindow final : public QOpenGLWindow { out_color = sampled; })", gl_engine::ShaderCodeSource::PLAINTEXT); - auto sampling_geometry = gl_engine::helpers::create_screen_quad_geometry(); - const std::array destination_layers { 0, 1, 2, 3 }; + } - std::mt19937 random_engine(random_seed); - std::array, repetitions> workloads; - for (auto& repetition : workloads) { - for (auto& workload : repetition) { - std::array indices; - std::iota(indices.begin(), indices.end(), 0); - std::ranges::shuffle(indices, random_engine); - std::ranges::copy_n(indices.begin(), batch_size, workload.source_indices.begin()); - workload.sampling_seed = random_engine(); + bool advance_startup(BenchmarkState& state) + { + switch (state.startup_step++) { + case 0: + state.destination = std::make_unique( + gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); + state.destination->setParams(gl_engine::Texture::Filter::MipMapLinear, gl_engine::Texture::Filter::Linear); + state.destination->allocate_array(resolution, resolution, state.batch_size); + return true; + case 1: + state.compressor + = std::make_unique(resolution, resolution, state.batch_size); + return true; + case 2: + state.framebuffer = std::make_unique( + gl_engine::Framebuffer::DepthFormat::None, + std::vector { gl_engine::Framebuffer::ColourFormat::RGBA8 }, + glm::uvec2 { framebuffer_size, framebuffer_size }); + return true; + case 3: + state.sampling_shader = make_sampling_shader(); + return true; + case 4: + state.sampling_geometry = gl_engine::helpers::create_screen_quad_geometry(); + return true; + case 5: + for (auto& repetition : state.workloads) { + for (auto& workload : repetition) { + std::array indices; + std::iota(indices.begin(), indices.end(), 0); + std::ranges::shuffle(indices, state.random_engine); + std::ranges::copy_n(indices.begin(), max_batch_size, workload.source_indices.begin()); + workload.sampling_seed = state.random_engine(); + } } + state.algorithm_order.resize(state.algorithms.size()); + std::iota(state.algorithm_order.begin(), state.algorithm_order.end(), 0); + qInfo().noquote() << QStringLiteral("\nTexture compression benchmark\n" + "GL vendor: %1\n" + "GL renderer: %2\n" + "GL version: %3\n" + "Compression format: %4\n" + "Random seed: 0x%5\n" + "Batch: %6 x 512x512 base-level textures, with mipmaps\n" + "Schedule: %7 repetitions, %8 warm-up + %9 measured batches per algorithm\n" + "Timer: steady-clock wall time through dependent one-pixel framebuffer readback\n") + .arg(QString::fromStdString(gl_string(GL_VENDOR))) + .arg(QString::fromStdString(gl_string(GL_RENDERER))) + .arg(QString::fromStdString(gl_string(GL_VERSION))) + .arg(QString::fromLatin1(format_name(state.format))) + .arg(QString::number(random_seed, 16)) + .arg(state.batch_size) + .arg(repetitions) + .arg(warmup_batches) + .arg(measured_batches); + return true; + default: + std::vector initial_sources(m_sources.begin(), m_sources.begin() + state.batch_size); + static_cast(state.compressor->compress(initial_sources, + *state.destination, + std::span(state.destination_layers).first(state.batch_size), + state.algorithms[1].settings)); + state.destination_initialised = true; + return true; } + } - const auto run_batch = [&](const Algorithm& algorithm, const Workload& workload) { - std::vector selected_sources; - if (algorithm.operation == Operation::Compression) { - selected_sources.reserve(batch_size); - for (const auto source_index : workload.source_indices) - selected_sources.push_back(m_sources[source_index]); - } - - const auto start = Clock::now(); - if (algorithm.operation == Operation::Compression) { - static_cast(compressor.compress( - selected_sources, destination, destination_layers, algorithm.settings)); - } - - auto* functions = QOpenGLContext::currentContext()->extraFunctions(); - framebuffer.bind(); - functions->glViewport(0, 0, framebuffer_size, framebuffer_size); - functions->glDisable(GL_BLEND); - functions->glDisable(GL_CULL_FACE); - functions->glDisable(GL_DEPTH_TEST); - functions->glDisable(GL_SCISSOR_TEST); - functions->glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); - sampling_shader.bind(); - destination.bind(0); - sampling_shader.set_uniform("texture_sampler", 0); - sampling_shader.set_uniform("sampling_seed", workload.sampling_seed); - sampling_geometry.draw(); - sampling_shader.release(); - const auto pixel = framebuffer.read_colour_attachment_pixel(0, { -1.0, -1.0 }); - const auto end = Clock::now(); - m_pixel_checksum += uint64_t(pixel.x) + 3u * uint64_t(pixel.y) + 5u * uint64_t(pixel.z) + 7u * uint64_t(pixel.w); - return std::chrono::duration(end - start).count(); - }; + double run_timed_batch(BenchmarkState& state, const Algorithm& algorithm, const Workload& workload) + { + std::vector selected_sources; + if (algorithm.operation == Operation::Compression) { + selected_sources.reserve(state.batch_size); + for (unsigned layer = 0; layer < state.batch_size; ++layer) + selected_sources.push_back(m_sources[workload.source_indices[layer]]); + } - // Give the sampling-only baseline valid compressed data even if it is first in the random order. - std::vector initial_sources(m_sources.begin(), m_sources.begin() + batch_size); - static_cast(compressor.compress( - initial_sources, destination, destination_layers, algorithms[1].settings)); - - qInfo().noquote() << QStringLiteral("\nTexture compression benchmark\n" - "GL vendor: %1\n" - "GL renderer: %2\n" - "GL version: %3\n" - "Compression format: %4\n" - "Random seed: 0x%5\n" - "Batch: 4 x 512x512 base-level textures, with mipmaps\n" - "Schedule: %6 repetitions, %7 warm-up + %8 measured batches per algorithm\n" - "Timer: steady-clock wall time through dependent one-pixel framebuffer readback\n") - .arg(QString::fromStdString(gl_string(GL_VENDOR))) - .arg(QString::fromStdString(gl_string(GL_RENDERER))) - .arg(QString::fromStdString(gl_string(GL_VERSION))) - .arg(QString::fromLatin1(format_name(format))) - .arg(QString::number(random_seed, 16)) - .arg(repetitions) - .arg(warmup_batches) - .arg(measured_batches); - - std::vector algorithm_order(algorithms.size()); - std::iota(algorithm_order.begin(), algorithm_order.end(), 0); - for (int repetition = 0; repetition < repetitions; ++repetition) { - std::ranges::shuffle(algorithm_order, random_engine); - for (const auto algorithm_index : algorithm_order) { - auto& algorithm = algorithms[algorithm_index]; - for (int batch = 0; batch < warmup_batches; ++batch) - static_cast(run_batch(algorithm, workloads[size_t(repetition)][size_t(batch)])); - for (int batch = 0; batch < measured_batches; ++batch) { - algorithm.samples.push_back(run_batch( - algorithm, workloads[size_t(repetition)][size_t(warmup_batches + batch)])); - } - } - qInfo().noquote() - << QStringLiteral("Completed repetition %1/%2").arg(repetition + 1).arg(repetitions); + const auto start = Clock::now(); + if (algorithm.operation == Operation::Compression) { + static_cast(state.compressor->compress(selected_sources, + *state.destination, + std::span(state.destination_layers).first(state.batch_size), + algorithm.settings)); } + auto* functions = QOpenGLContext::currentContext()->extraFunctions(); + state.framebuffer->bind(); + functions->glViewport(0, 0, framebuffer_size, framebuffer_size); + functions->glDisable(GL_BLEND); + functions->glDisable(GL_CULL_FACE); + functions->glDisable(GL_DEPTH_TEST); + functions->glDisable(GL_SCISSOR_TEST); + functions->glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + state.sampling_shader->bind(); + state.destination->bind(0); + state.sampling_shader->set_uniform("texture_sampler", 0); + state.sampling_shader->set_uniform("sampling_seed", workload.sampling_seed); + state.sampling_shader->set_uniform("active_layers", state.batch_size); + state.sampling_geometry.draw(); + state.sampling_shader->release(); + const auto pixel = state.framebuffer->read_colour_attachment_pixel(0, { -1.0, -1.0 }); + const auto end = Clock::now(); + m_pixel_checksum + += uint64_t(pixel.x) + 3u * uint64_t(pixel.y) + 5u * uint64_t(pixel.z) + 7u * uint64_t(pixel.w); + return std::chrono::duration(end - start).count(); + } + + void prepare_quality(BenchmarkState& state) + { qInfo().noquote() << QStringLiteral("Computing untimed 512x512 quality metrics..."); - std::vector quality_references; - quality_references.reserve(m_sources.size()); + state.quality_references.reserve(m_sources.size()); for (const auto& source : m_sources) - quality_references.push_back(make_quality_reference(source)); - - gl_engine::Framebuffer quality_framebuffer(gl_engine::Framebuffer::DepthFormat::None, - { gl_engine::Framebuffer::ColourFormat::RGBA8 }, - { resolution, resolution }); - gl_engine::ShaderProgram quality_shader(R"( + state.quality_references.push_back(make_quality_reference(source)); + state.quality_framebuffer = std::make_unique( + gl_engine::Framebuffer::DepthFormat::None, + std::vector { gl_engine::Framebuffer::ColourFormat::RGBA8 }, + glm::uvec2 { resolution, resolution }); + state.quality_shader = std::make_unique(R"( out highp vec2 texcoords; void main() { highp vec2 vertices[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); @@ -575,54 +655,59 @@ class BenchmarkWindow final : public QOpenGLWindow { out_color = textureLod(texture_sampler, vec3(texcoords.x, 1.0 - texcoords.y, texture_layer), 0.0); })", gl_engine::ShaderCodeSource::PLAINTEXT); + } - const auto reconstruct = [&](unsigned layer) { - auto* functions = QOpenGLContext::currentContext()->extraFunctions(); - quality_framebuffer.bind(); - functions->glViewport(0, 0, resolution, resolution); - functions->glDisable(GL_BLEND); - functions->glDisable(GL_CULL_FACE); - functions->glDisable(GL_DEPTH_TEST); - functions->glDisable(GL_SCISSOR_TEST); - functions->glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); - quality_shader.bind(); - destination.bind(0); - quality_shader.set_uniform("texture_sampler", 0); - quality_shader.set_uniform("texture_layer", float(layer)); - sampling_geometry.draw(); - quality_shader.release(); - return quality_framebuffer.read_colour_attachment(0); - }; + QImage reconstruct(BenchmarkState& state, unsigned layer) + { + auto* functions = QOpenGLContext::currentContext()->extraFunctions(); + state.quality_framebuffer->bind(); + functions->glViewport(0, 0, resolution, resolution); + functions->glDisable(GL_BLEND); + functions->glDisable(GL_CULL_FACE); + functions->glDisable(GL_DEPTH_TEST); + functions->glDisable(GL_SCISSOR_TEST); + functions->glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + state.quality_shader->bind(); + state.destination->bind(0); + state.quality_shader->set_uniform("texture_sampler", 0); + state.quality_shader->set_uniform("texture_layer", float(layer)); + state.sampling_geometry.draw(); + state.quality_shader->release(); + return state.quality_framebuffer->read_colour_attachment(0); + } - for (auto& algorithm : algorithms) { - if (algorithm.operation != Operation::Compression || algorithm.checksum) - continue; - QualityAccumulator accumulator; - for (size_t source_offset = 0; source_offset < m_sources.size(); source_offset += batch_size) { - Q_ASSERT(source_offset + batch_size <= m_sources.size()); - std::vector selected_sources; - selected_sources.reserve(batch_size); - for (size_t layer = 0; layer < batch_size; ++layer) - selected_sources.push_back(m_sources[source_offset + layer]); - static_cast(compressor.compress( - selected_sources, destination, destination_layers, algorithm.settings)); - for (unsigned layer = 0; layer < batch_size; ++layer) { - accumulate_quality(accumulator, - reconstruct(layer), - selected_sources[layer], - quality_references[source_offset + layer]); - } + void compute_quality(BenchmarkState& state, Algorithm& algorithm) + { + QualityAccumulator accumulator; + for (size_t source_offset = 0; source_offset < m_sources.size(); source_offset += state.batch_size) { + const auto active_batch_size + = std::min(state.batch_size, unsigned(m_sources.size() - source_offset)); + std::vector selected_sources; + selected_sources.reserve(active_batch_size); + for (size_t layer = 0; layer < active_batch_size; ++layer) + selected_sources.push_back(m_sources[source_offset + layer]); + static_cast(state.compressor->compress(selected_sources, + *state.destination, + std::span(state.destination_layers).first(active_batch_size), + algorithm.settings)); + for (unsigned layer = 0; layer < active_batch_size; ++layer) { + accumulate_quality(accumulator, + reconstruct(state, layer), + selected_sources[layer], + state.quality_references[source_offset + layer]); } - algorithm.quality = quality_metrics(accumulator); - qInfo().noquote() << QStringLiteral("Completed quality metrics for %1").arg(QString::fromStdString(algorithm.name)); } - gl_engine::Framebuffer::unbind(); + algorithm.quality = quality_metrics(accumulator); + qInfo().noquote() + << QStringLiteral("Completed quality metrics for %1").arg(QString::fromStdString(algorithm.name)); + } + bool write_report(const BenchmarkState& state) + { + gl_engine::Framebuffer::unbind(); const auto sampling_iterator = std::ranges::find_if( - algorithms, [](const Algorithm& algorithm) { return algorithm.operation == Operation::SamplingOnly; }); - const auto checksum_iterator = std::ranges::find_if( - algorithms, [](const Algorithm& algorithm) { return algorithm.checksum; }); - if (sampling_iterator == algorithms.end() || checksum_iterator == algorithms.end()) + state.algorithms, [](const Algorithm& algorithm) { return algorithm.operation == Operation::SamplingOnly; }); + if (sampling_iterator == state.algorithms.end()) return false; std::ostringstream report; @@ -631,14 +716,21 @@ class BenchmarkWindow final : public QOpenGLWindow { << repetitions << ").\n" << "Quality is an untimed pass over all " << m_sources.size() << " source textures at mip level 0 (512x512). PSNR uses linear RGB; SSIM uses sRGB luma.\n" - << std::left << std::setw(29) << "algorithm" + << std::left << std::setw(53) << "algorithm" << std::right << std::setw(12) << "raw mean" << std::setw(14) << "raw mean SD" << std::setw(8) << "n" << std::setw(17) << "minus sample" << std::setw(18) << "adjusted mean SD" << std::setw(17) << "encoding only" << std::setw(18) << "encoding mean SD" << std::setw(13) << "PSNR (dB)" << std::setw(11) << "SSIM" << '\n'; - for (const auto& algorithm : algorithms) { + for (const auto& algorithm : state.algorithms) { + const auto checksum_iterator + = std::ranges::find_if(state.algorithms, [&algorithm](const Algorithm& candidate) { + return candidate.checksum + && candidate.settings.transfer_mode == algorithm.settings.transfer_mode; + }); + if (algorithm.operation == Operation::Compression && checksum_iterator == state.algorithms.end()) + return false; std::vector sampling_subtracted; std::vector encoding_only; sampling_subtracted.reserve(algorithm.samples.size()); @@ -651,7 +743,7 @@ class BenchmarkWindow final : public QOpenGLWindow { const auto raw = mean_statistics(algorithm.samples); const auto adjusted = mean_statistics(sampling_subtracted); const auto encoding = mean_statistics(encoding_only); - report << std::left << std::setw(29) << algorithm.name << std::right << std::fixed << std::setprecision(3) + report << std::left << std::setw(53) << algorithm.name << std::right << std::fixed << std::setprecision(3) << std::setw(12) << raw.mean << std::setw(14) << raw.mean_standard_deviation << std::setw(8) << raw.sample_count << std::setw(17) << adjusted.mean << std::setw(18) << adjusted.mean_standard_deviation; @@ -675,6 +767,56 @@ class BenchmarkWindow final : public QOpenGLWindow { return true; } + bool advance_benchmark() + { + auto& state = *m_benchmark_state; + if (!state.destination_initialised) + return advance_startup(state); + if (state.phase == BenchmarkState::Phase::Timing) { + if (state.algorithm_position == 0) + std::ranges::shuffle(state.algorithm_order, state.random_engine); + auto& algorithm = state.algorithms[state.algorithm_order[state.algorithm_position]]; + for (int batch = 0; batch < warmup_batches; ++batch) + static_cast(run_timed_batch(state, algorithm, state.workloads[size_t(state.repetition)][size_t(batch)])); + for (int batch = 0; batch < measured_batches; ++batch) { + algorithm.samples.push_back(run_timed_batch( + state, algorithm, state.workloads[size_t(state.repetition)][size_t(warmup_batches + batch)])); + } + if (++state.algorithm_position == state.algorithm_order.size()) { + state.algorithm_position = 0; + qInfo().noquote() + << QStringLiteral("Completed repetition %1/%2").arg(state.repetition + 1).arg(repetitions); + if (++state.repetition == repetitions) + state.phase = BenchmarkState::Phase::QualitySetup; + } + return true; + } + + if (state.phase == BenchmarkState::Phase::QualitySetup) { + prepare_quality(state); + state.phase = BenchmarkState::Phase::Quality; + return true; + } + + if (state.phase == BenchmarkState::Phase::Quality) { + while (state.quality_algorithm < state.algorithms.size()) { + auto& algorithm = state.algorithms[state.quality_algorithm++]; + if (algorithm.operation == Operation::Compression && !algorithm.checksum) { + compute_quality(state, algorithm); + return true; + } + } + state.phase = BenchmarkState::Phase::Report; + return true; + } + + if (!write_report(state)) + return false; + m_benchmark_state.reset(); + ++m_batch_index; + return true; + } + void fail(const QString& message) { qInfo().noquote() << message; @@ -687,7 +829,8 @@ class BenchmarkWindow final : public QOpenGLWindow { QString m_download_error; int m_downloads_remaining = 0; bool m_data_ready = false; - bool m_benchmark_started = false; + size_t m_batch_index = 0; + std::unique_ptr m_benchmark_state; uint64_t m_pixel_checksum = 0; }; From cff632efe1d08060cab7d804b072fb6a3ea369ca Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Tue, 25 Aug 2026 18:01:17 +0200 Subject: [PATCH 32/38] Use automatic texture compression readback fallback --- gl_engine/CMakeLists.txt | 1 - gl_engine/Texture.cpp | 109 +++++++----------- gl_engine/Texture.h | 9 +- gl_engine/shaders/texture_compress_pack.frag | 18 --- .../texture_compression_benchmark/main.cpp | 67 +++++------ 5 files changed, 79 insertions(+), 125 deletions(-) delete mode 100644 gl_engine/shaders/texture_compress_pack.frag diff --git a/gl_engine/CMakeLists.txt b/gl_engine/CMakeLists.txt index d595e26a..c92fbb48 100644 --- a/gl_engine/CMakeLists.txt +++ b/gl_engine/CMakeLists.txt @@ -87,7 +87,6 @@ qt_add_resources(gl_engine "shaders" shaders/tile_id.glsl shaders/track.frag shaders/track.vert - shaders/texture_compress_pack.frag shaders/texture_compress_raster.vert shaders/texture_compress.vert shaders/turbo_colormap.glsl diff --git a/gl_engine/Texture.cpp b/gl_engine/Texture.cpp index deb2a1b5..80f89cf8 100644 --- a/gl_engine/Texture.cpp +++ b/gl_engine/Texture.cpp @@ -20,6 +20,7 @@ #include "ShaderProgram.h" #include "nucleus/utils/ColourTexture.h" +#include #include #include #include @@ -392,8 +393,6 @@ struct gl_engine::TextureCompressor::Impl { GLsizei block_atlas_height = 0; GLsizei paired_atlas_width = 0; GLsizei paired_atlas_height = 0; - GLsizei output_atlas_width = 0; - GLsizei output_atlas_height = 0; GLuint scratch_texture = 0; GLuint encoded_texture = 0; GLuint paired_encoded_texture = 0; @@ -401,8 +400,7 @@ struct gl_engine::TextureCompressor::Impl { GLuint vertex_array = 0; GLuint encoding_framebuffer = 0; GLuint paired_encoding_framebuffer = 0; - GLuint packing_framebuffer = 0; - GLuint packing_renderbuffer = 0; + ReadbackMode readback_mode = ReadbackMode::RG32UI; std::unique_ptr dxt1_fragment_program; std::unique_ptr etc1_fragment_program; std::unique_ptr etc1_fast_split_fused_exact_fragment_program; @@ -415,7 +413,6 @@ struct gl_engine::TextureCompressor::Impl { std::unique_ptr paired_etc1_fast_split_fused_exact_residual_fragment_program; std::unique_ptr paired_etc1_fast_split_fused_exact_shared_residual_fragment_program; std::unique_ptr paired_checksum_fragment_program; - std::unique_ptr packing_program; Impl(unsigned texture_width, unsigned texture_height, unsigned maximum_batch_size) : width(texture_width) @@ -432,9 +429,7 @@ struct gl_engine::TextureCompressor::Impl { } maximum_size *= max_batch_size; - GLint maximum_renderbuffer_size = 0; GLint maximum_texture_size = 0; - f->glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &maximum_renderbuffer_size); f->glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maximum_texture_size); const auto atlas_size = [](size_t pixels, GLint maximum_dimension) { const auto atlas_width = GLsizei(std::min({ pixels, size_t(maximum_dimension), size_t(256) })); @@ -445,14 +440,12 @@ struct gl_engine::TextureCompressor::Impl { std::tie(block_atlas_width, block_atlas_height) = atlas_size(maximum_size / 8, maximum_texture_size); std::tie(paired_atlas_width, paired_atlas_height) = atlas_size((maximum_size + 15) / 16, maximum_texture_size); - std::tie(output_atlas_width, output_atlas_height) = atlas_size(maximum_size / 4, maximum_renderbuffer_size); f->glGenBuffers(1, &encoded_buffer); f->glBindBuffer(GL_PIXEL_PACK_BUFFER, encoded_buffer); const auto encoded_buffer_size = std::max({ size_t(block_atlas_width) * size_t(block_atlas_height) * 8, size_t(paired_atlas_width) * size_t(paired_atlas_height) * 16, - size_t(output_atlas_width) * size_t(output_atlas_height) * 4, }); f->glBufferData(GL_PIXEL_PACK_BUFFER, GLsizeiptr(encoded_buffer_size), @@ -463,11 +456,9 @@ struct gl_engine::TextureCompressor::Impl { GLint previous_draw_framebuffer = 0; GLint previous_read_framebuffer = 0; - GLint previous_renderbuffer = 0; GLint previous_texture = 0; f->glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previous_draw_framebuffer); f->glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &previous_read_framebuffer); - f->glGetIntegerv(GL_RENDERBUFFER_BINDING, &previous_renderbuffer); f->glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture); f->glGenTextures(1, &encoded_texture); @@ -481,33 +472,41 @@ struct gl_engine::TextureCompressor::Impl { f->glGenFramebuffers(1, &encoding_framebuffer); f->glBindFramebuffer(GL_FRAMEBUFFER, encoding_framebuffer); f->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, encoded_texture, 0); - Q_ASSERT(f->glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE); - - f->glGenTextures(1, &paired_encoded_texture); - f->glBindTexture(GL_TEXTURE_2D, paired_encoded_texture); - f->glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA32UI, paired_atlas_width, paired_atlas_height); - f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - - f->glGenFramebuffers(1, &paired_encoding_framebuffer); - f->glBindFramebuffer(GL_FRAMEBUFFER, paired_encoding_framebuffer); - f->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, paired_encoded_texture, 0); - Q_ASSERT(f->glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE); - - f->glGenFramebuffers(1, &packing_framebuffer); - f->glGenRenderbuffers(1, &packing_renderbuffer); - f->glBindRenderbuffer(GL_RENDERBUFFER, packing_renderbuffer); - // RGBA8UI with RGBA_INTEGER/UNSIGNED_BYTE is the portable WebGL 2 integer readback path. - // Two pixels hold the two 32-bit words of each compressed block. - f->glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8UI, output_atlas_width, output_atlas_height); - f->glBindFramebuffer(GL_FRAMEBUFFER, packing_framebuffer); - f->glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, packing_renderbuffer); - Q_ASSERT(f->glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE); + const auto encoding_framebuffer_status = f->glCheckFramebufferStatus(GL_FRAMEBUFFER); + GLint implementation_read_format = 0; + GLint implementation_read_type = 0; + if (encoding_framebuffer_status == GL_FRAMEBUFFER_COMPLETE) { + f->glReadBuffer(GL_COLOR_ATTACHMENT0); + f->glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_FORMAT, &implementation_read_format); + f->glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_TYPE, &implementation_read_type); + } + if (encoding_framebuffer_status != GL_FRAMEBUFFER_COMPLETE + || implementation_read_format != GL_RG_INTEGER + || implementation_read_type != GL_UNSIGNED_INT) { + readback_mode = ReadbackMode::RGBA32UI; + qInfo().noquote() + << QStringLiteral("RG32UI readback is unavailable (framebuffer 0x%1, format 0x%2, type 0x%3); falling back to paired RGBA32UI readback.") + .arg(unsigned(encoding_framebuffer_status), 0, 16) + .arg(unsigned(implementation_read_format), 0, 16) + .arg(unsigned(implementation_read_type), 0, 16); + + f->glGenTextures(1, &paired_encoded_texture); + f->glBindTexture(GL_TEXTURE_2D, paired_encoded_texture); + f->glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA32UI, paired_atlas_width, paired_atlas_height); + f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + f->glGenFramebuffers(1, &paired_encoding_framebuffer); + f->glBindFramebuffer(GL_FRAMEBUFFER, paired_encoding_framebuffer); + f->glFramebufferTexture2D( + GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, paired_encoded_texture, 0); + if (f->glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) + qFatal("Paired RGBA32UI texture compression framebuffer is incomplete"); + } f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, GLuint(previous_draw_framebuffer)); f->glBindFramebuffer(GL_READ_FRAMEBUFFER, GLuint(previous_read_framebuffer)); - f->glBindRenderbuffer(GL_RENDERBUFFER, GLuint(previous_renderbuffer)); f->glBindTexture(GL_TEXTURE_2D, GLuint(previous_texture)); } @@ -586,14 +585,11 @@ struct gl_engine::TextureCompressor::Impl { paired_etc1_fast_split_fused_exact_residual_fragment_program.reset(); paired_etc1_fast_split_fused_exact_shared_residual_fragment_program.reset(); paired_checksum_fragment_program.reset(); - packing_program.reset(); if (!QOpenGLContext::currentContext()) return; auto* f = QOpenGLContext::currentContext()->extraFunctions(); f->glDeleteFramebuffers(1, &encoding_framebuffer); f->glDeleteFramebuffers(1, &paired_encoding_framebuffer); - f->glDeleteFramebuffers(1, &packing_framebuffer); - f->glDeleteRenderbuffers(1, &packing_renderbuffer); f->glDeleteTextures(1, &encoded_texture); f->glDeleteTextures(1, &paired_encoded_texture); f->glDeleteVertexArrays(1, &vertex_array); @@ -632,6 +628,11 @@ gl_engine::TextureCompressor::TextureCompressor(unsigned width, unsigned height, gl_engine::TextureCompressor::~TextureCompressor() = default; +gl_engine::TextureCompressor::ReadbackMode gl_engine::TextureCompressor::readback_mode() const +{ + return m->readback_mode; +} + size_t gl_engine::TextureCompressor::compressed_level_size(unsigned width, unsigned height) { return size_t(std::max(1u, (width + 3) / 4)) * std::max(1u, (height + 3) / 4) * 8; @@ -731,7 +732,7 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: GLboolean cull_enabled = GL_FALSE; GLboolean depth_enabled = GL_FALSE; GLboolean scissor_enabled = GL_FALSE; - const auto paired_blocks = settings.transfer_mode == TransferMode::PairedRGBA32UI; + const auto paired_blocks = m->readback_mode == ReadbackMode::RGBA32UI; const auto total_blocks = total_encoded_size / 8; const auto encoding_pixels = paired_blocks ? (total_blocks + 1) / 2 : total_blocks; const auto maximum_encoding_width = paired_blocks ? m->paired_atlas_width : m->block_atlas_width; @@ -780,31 +781,6 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: GLsizei readback_height = encoding_height; GLuint readback_framebuffer = paired_blocks ? m->paired_encoding_framebuffer : m->encoding_framebuffer; GLenum readback_format = paired_blocks ? GL_RGBA_INTEGER : GL_RG_INTEGER; - GLenum readback_type = GL_UNSIGNED_INT; - if (settings.transfer_mode == TransferMode::PackedRGBA8) { - if (!m->packing_program) { - m->packing_program = std::make_unique( - "texture_compress_raster.vert", "texture_compress_pack.frag", ShaderCodeSource::FILE); - } - const auto output_pixels = total_blocks * 2; - readback_width = GLsizei(std::min(output_pixels, size_t(m->output_atlas_width))); - readback_height = GLsizei((output_pixels + size_t(readback_width) - 1) / size_t(readback_width)); - readback_framebuffer = m->packing_framebuffer; - readback_format = GL_RGBA_INTEGER; - readback_type = GL_UNSIGNED_BYTE; - - f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m->packing_framebuffer); - f->glViewport(0, 0, readback_width, readback_height); - m->packing_program->bind(); - m->packing_program->set_uniform("encoded_blocks", 6); - m->packing_program->set_uniform("block_atlas_width", int(encoding_width)); - m->packing_program->set_uniform("output_atlas_width", int(readback_width)); - m->packing_program->set_uniform("total_blocks", int(total_blocks)); - f->glActiveTexture(GL_TEXTURE6); - f->glBindTexture(GL_TEXTURE_2D, m->encoded_texture); - f->glDrawArrays(GL_TRIANGLES, 0, 3); - m->packing_program->release(); - } f->glBindVertexArray(0); if (blend_enabled) @@ -830,7 +806,8 @@ gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std: f->glReadBuffer(GL_COLOR_ATTACHMENT0); f->glPixelStorei(GL_PACK_ALIGNMENT, 1); f->glBindBuffer(GL_PIXEL_PACK_BUFFER, m->encoded_buffer); - f->glReadPixels(0, 0, readback_width, readback_height, readback_format, readback_type, nullptr); + f->glReadPixels( + 0, 0, readback_width, readback_height, readback_format, GL_UNSIGNED_INT, nullptr); f->glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); f->glPixelStorei(GL_PACK_ALIGNMENT, previous_pack_alignment); f->glBindFramebuffer(GL_READ_FRAMEBUFFER, GLuint(previous_read_framebuffer)); diff --git a/gl_engine/Texture.h b/gl_engine/Texture.h index 083943a4..ae22ce86 100644 --- a/gl_engine/Texture.h +++ b/gl_engine/Texture.h @@ -100,10 +100,9 @@ class TextureCompressor { Checksum }; - enum class TransferMode { - PackedRGBA8, - DirectRG32UI, - PairedRGBA32UI, + enum class ReadbackMode { + RG32UI, + RGBA32UI, }; struct Settings { @@ -111,7 +110,6 @@ class TextureCompressor { unsigned effort = 0; Encoder encoder = Encoder::Search; bool generate_mipmaps = true; - TransferMode transfer_mode = TransferMode::PackedRGBA8; }; struct Result { @@ -134,6 +132,7 @@ class TextureCompressor { [[nodiscard]] static size_t compressed_level_size(unsigned width, unsigned height); [[nodiscard]] static unsigned mip_level_count(unsigned width, unsigned height); [[nodiscard]] static bool is_supported(); + [[nodiscard]] ReadbackMode readback_mode() const; private: struct Impl; diff --git a/gl_engine/shaders/texture_compress_pack.frag b/gl_engine/shaders/texture_compress_pack.frag deleted file mode 100644 index d759b153..00000000 --- a/gl_engine/shaders/texture_compress_pack.frag +++ /dev/null @@ -1,18 +0,0 @@ -uniform highp usampler2D encoded_blocks; -uniform highp int block_atlas_width; -uniform highp int output_atlas_width; -uniform highp int total_blocks; -layout(location = 0) out highp uvec4 encoded_pixel; - -void main() -{ - highp int output_pixel = int(gl_FragCoord.y) * output_atlas_width + int(gl_FragCoord.x); - if (output_pixel >= total_blocks * 2) - discard; - - highp int block_index = output_pixel / 2; - highp ivec2 block_position = ivec2(block_index % block_atlas_width, block_index / block_atlas_width); - highp uvec2 encoded = texelFetch(encoded_blocks, block_position, 0).rg; - highp uint word = output_pixel % 2 == 0 ? encoded.x : encoded.y; - encoded_pixel = uvec4(word & 0xffu, (word >> 8u) & 0xffu, (word >> 16u) & 0xffu, word >> 24u); -} diff --git a/unittests/texture_compression_benchmark/main.cpp b/unittests/texture_compression_benchmark/main.cpp index 4d5df034..fdda60ad 100644 --- a/unittests/texture_compression_benchmark/main.cpp +++ b/unittests/texture_compression_benchmark/main.cpp @@ -47,7 +47,6 @@ using Clock = std::chrono::steady_clock; using Raster = radix::Raster; using Format = nucleus::utils::ColourTexture::Format; using Encoder = gl_engine::TextureCompressor::Encoder; -using TransferMode = gl_engine::TextureCompressor::TransferMode; constexpr unsigned resolution = 512; constexpr unsigned max_batch_size = 4; @@ -295,46 +294,43 @@ const char* format_name(Format format) return "unknown"; } +const char* readback_mode_name(gl_engine::TextureCompressor::ReadbackMode mode) +{ + using ReadbackMode = gl_engine::TextureCompressor::ReadbackMode; + switch (mode) { + case ReadbackMode::RG32UI: + return "direct RG32UI"; + case ReadbackMode::RGBA32UI: + return "paired RGBA32UI"; + } + return "unknown"; +} + std::vector supported_algorithms(Format format) { - const auto settings = [format](Encoder encoder, TransferMode transfer_mode) { + const auto settings = [format](Encoder encoder) { return gl_engine::TextureCompressor::Settings { .algorithm = format, .effort = 0, .encoder = encoder, .generate_mipmaps = true, - .transfer_mode = transfer_mode, }; }; std::vector result; - result.push_back( - { "sampling only", Operation::SamplingOnly, settings(Encoder::Checksum, TransferMode::PackedRGBA8) }); - const std::array, 3> transfer_modes { { - { TransferMode::PackedRGBA8, "packed RGBA8" }, - { TransferMode::DirectRG32UI, "direct RG32UI" }, - { TransferMode::PairedRGBA32UI, "paired RGBA32UI" }, - } }; - for (const auto& [transfer_mode, transfer_name] : transfer_modes) { - result.push_back({ std::string("checksum / ") + transfer_name, + result.push_back({ "sampling only", Operation::SamplingOnly, settings(Encoder::Checksum) }); + result.push_back({ "checksum", Operation::Compression, settings(Encoder::Checksum), true }); + if (format == Format::DXT1) { + result.push_back({ "DXT1", Operation::Compression, settings(Encoder::Dxt1) }); + } else if (format == Format::ETC1) { + result.push_back( + { "ETC1 fused exact", Operation::Compression, settings(Encoder::FastSplitFusedExact) }); + result.push_back({ "ETC1 fused exact residual fit", Operation::Compression, - settings(Encoder::Checksum, transfer_mode), - true }); - if (format == Format::DXT1) { - result.push_back({ std::string("DXT1 / ") + transfer_name, - Operation::Compression, - settings(Encoder::Dxt1, transfer_mode) }); - } else if (format == Format::ETC1) { - result.push_back({ std::string("ETC1 fused exact / ") + transfer_name, - Operation::Compression, - settings(Encoder::FastSplitFusedExact, transfer_mode) }); - result.push_back({ std::string("ETC1 fused exact residual fit / ") + transfer_name, - Operation::Compression, - settings(Encoder::FastSplitFusedExactResidual, transfer_mode) }); - result.push_back({ std::string("ETC1 fused exact shared residual / ") + transfer_name, - Operation::Compression, - settings(Encoder::FastSplitFusedExactSharedResidual, transfer_mode) }); - } + settings(Encoder::FastSplitFusedExactResidual) }); + result.push_back({ "ETC1 fused exact shared residual", + Operation::Compression, + settings(Encoder::FastSplitFusedExactSharedResidual) }); } return result; } @@ -565,14 +561,16 @@ class BenchmarkWindow final : public QOpenGLWindow { "GL renderer: %2\n" "GL version: %3\n" "Compression format: %4\n" - "Random seed: 0x%5\n" - "Batch: %6 x 512x512 base-level textures, with mipmaps\n" - "Schedule: %7 repetitions, %8 warm-up + %9 measured batches per algorithm\n" + "Readback: %5\n" + "Random seed: 0x%6\n" + "Batch: %7 x 512x512 base-level textures, with mipmaps\n" + "Schedule: %8 repetitions, %9 warm-up + %10 measured batches per algorithm\n" "Timer: steady-clock wall time through dependent one-pixel framebuffer readback\n") .arg(QString::fromStdString(gl_string(GL_VENDOR))) .arg(QString::fromStdString(gl_string(GL_RENDERER))) .arg(QString::fromStdString(gl_string(GL_VERSION))) .arg(QString::fromLatin1(format_name(state.format))) + .arg(QString::fromLatin1(readback_mode_name(state.compressor->readback_mode()))) .arg(QString::number(random_seed, 16)) .arg(state.batch_size) .arg(repetitions) @@ -725,9 +723,8 @@ class BenchmarkWindow final : public QOpenGLWindow { for (const auto& algorithm : state.algorithms) { const auto checksum_iterator - = std::ranges::find_if(state.algorithms, [&algorithm](const Algorithm& candidate) { - return candidate.checksum - && candidate.settings.transfer_mode == algorithm.settings.transfer_mode; + = std::ranges::find_if(state.algorithms, [](const Algorithm& candidate) { + return candidate.checksum; }); if (algorithm.operation == Operation::Compression && checksum_iterator == state.algorithms.end()) return false; From c0075da0b8a343b81340464f8383f7f6a11d898e Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Sat, 29 Aug 2026 15:19:38 +0200 Subject: [PATCH 33/38] Refactor GPU texture compression encoder --- .../TexturePreviewItem.cpp | 145 ++--- gl_engine/CMakeLists.txt | 5 +- gl_engine/Texture.cpp | 487 +--------------- gl_engine/Texture.h | 58 +- gl_engine/TextureCompressor.cpp | 523 ++++++++++++++++++ gl_engine/TextureCompressor.h | 123 ++++ ...re_compress.vert => texture_compress.frag} | 13 +- .../shaders/texture_compress_raster.vert | 5 - gl_engine/shaders/texture_copy.frag | 9 + unittests/gl_engine/CMakeLists.txt | 2 +- unittests/gl_engine/texture.cpp | 95 ---- unittests/gl_engine/texture_compressor.cpp | 275 +++++++++ .../texture_compression_benchmark/main.cpp | 101 ++-- 13 files changed, 1098 insertions(+), 743 deletions(-) create mode 100644 gl_engine/TextureCompressor.cpp create mode 100644 gl_engine/TextureCompressor.h rename gl_engine/shaders/{texture_compress.vert => texture_compress.frag} (96%) delete mode 100644 gl_engine/shaders/texture_compress_raster.vert create mode 100644 gl_engine/shaders/texture_copy.frag create mode 100644 unittests/gl_engine/texture_compressor.cpp diff --git a/apps/texture_compression_benchmark/TexturePreviewItem.cpp b/apps/texture_compression_benchmark/TexturePreviewItem.cpp index 012bb848..71ae2c84 100644 --- a/apps/texture_compression_benchmark/TexturePreviewItem.cpp +++ b/apps/texture_compression_benchmark/TexturePreviewItem.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -115,53 +116,49 @@ QImage reconstruct(gl_engine::Texture& texture, unsigned resolution, unsigned la struct GpuPreview { const char* name; const char* description; - gl_engine::TextureCompressor::Encoder encoder; - unsigned effort; + gl_engine::TextureCompressor::Settings settings; }; -constexpr std::array gpu_previews { { - { "Search 0", +std::vector gpu_previews(nucleus::utils::ColourTexture::Format format) +{ + using Compressor = gl_engine::TextureCompressor; + const auto search = [](unsigned effort) { + return Compressor::Settings { + .dxt1_algorithm = Compressor::Dxt1Algorithm::SlowSearch, + .etc_algorithm = Compressor::EtcAlgorithm::SlowSearch, + .search_effort = effort, + }; + }; + std::vector result { + { "Slow search 0", "Tests the average block colour with every ETC1 modifier table and keeps the lowest-error result.", - gl_engine::TextureCompressor::Encoder::Search, - 0 }, - { "search 1", + search(0) }, + { "Slow search 1", "Tests two base colours around the block average with every ETC1 modifier table and keeps the lowest-error result.", - gl_engine::TextureCompressor::Encoder::Search, - 1 }, - { "search 2", + search(1) }, + { "Slow search 2", "Tests three base colours around the block average with every ETC1 modifier table and keeps the lowest-error result.", - gl_engine::TextureCompressor::Encoder::Search, - 2 }, - { "search 3", + search(2) }, + { "Slow search 3", "Tests four base colours around the block average with every ETC1 modifier table and keeps the lowest-error result.", - gl_engine::TextureCompressor::Encoder::Search, - 3 }, - { "search 4", + search(3) }, + { "Slow search 4", "Tests five base colours around the block average with every ETC1 modifier table and keeps the lowest-error result.", - gl_engine::TextureCompressor::Encoder::Search, - 4 }, - { "search 10", + search(4) }, + { "Slow search 10", "Tests eleven base colours around the block average with every ETC1 modifier table and keeps the lowest-error result.", - gl_engine::TextureCompressor::Encoder::Search, - 10 }, - { "split fused exact", - "Evaluates all four colours in the selected modifier table for every pixel.", - gl_engine::TextureCompressor::Encoder::FastSplitFusedExact, - 0 }, - { "split exact residual fit", - "Refits each base from the average per-channel reconstruction residual and evaluates it once.", - gl_engine::TextureCompressor::Encoder::FastSplitFusedExactResidual, - 0 }, - { "split exact shared residual", - "Applies one combined per-channel residual correction to both bases of the winning split.", - gl_engine::TextureCompressor::Encoder::FastSplitFusedExactSharedResidual, - 0 }, -} }; - -constexpr size_t uncompressed_preview_index = 0; -constexpr size_t goofy_preview_index = 1; -constexpr size_t first_gpu_preview_index = 2; -constexpr size_t preview_count = first_gpu_preview_index + gpu_previews.size(); + search(10) }, + }; + if (format == nucleus::utils::ColourTexture::Format::ETC1) { + result.push_back({ "Fastest", + "Evaluates horizontal and vertical ETC splits with exact modifier selection.", + { .etc_algorithm = Compressor::EtcAlgorithm::Fastest } }); + result.push_back({ "Fast", + "Refits each ETC base from its average reconstruction residual and evaluates it once.", + { .etc_algorithm = Compressor::EtcAlgorithm::Fast } }); + } + return result; +} } // namespace class TexturePreviewRenderer final : public QQuickFramebufferObject::Renderer { @@ -209,9 +206,6 @@ class TexturePreviewRenderer final : public QQuickFramebufferObject::Renderer { constexpr unsigned resolution = 512; if (m_source_images.size() != tile_groups.size()) return QStringLiteral("Preview imagery is incomplete."); - if (!gl_engine::TextureCompressor::is_supported()) - return QStringLiteral("GPU compression is unavailable on this device."); - std::vector sources; sources.reserve(m_source_images.size()); for (const auto& image : m_source_images) @@ -221,14 +215,17 @@ class TexturePreviewRenderer final : public QQuickFramebufferObject::Renderer { std::iota(layers.begin(), layers.end(), 0u); const auto algorithm = gl_engine::Texture::compression_algorithm(); const auto filter = gl_engine::Texture::Filter::MipMapLinear; + const auto mip_levels = gl_engine::TextureCompressor::mip_level_count(resolution, resolution); + const auto previews = gpu_previews(algorithm); m_preview_results.clear(); - m_preview_results.reserve(preview_count); - - auto create_texture = [&](gl_engine::Texture::Format format) { - auto texture = std::make_unique(gl_engine::Texture::Target::_2dArray, format); - texture->setParams(format == gl_engine::Texture::Format::CompressedRGBA8 ? filter : gl_engine::Texture::Filter::Linear, - gl_engine::Texture::Filter::Linear); - texture->allocate_array(resolution, resolution, unsigned(sources.size())); + m_preview_results.reserve(3 + previews.size()); + m_preview_textures.clear(); + m_preview_textures.reserve(3 + previews.size()); + + auto create_texture = [&](gl_engine::Texture::Format format, gl_engine::Texture::Filter min_filter) { + auto texture = std::make_shared(gl_engine::Texture::Target::_2dArray, format); + texture->setParams(min_filter, gl_engine::Texture::Filter::Linear); + texture->allocate_array(resolution, resolution, unsigned(sources.size()), mip_levels); return texture; }; auto psnr = [&](gl_engine::Texture& texture) { @@ -239,36 +236,44 @@ class TexturePreviewRenderer final : public QQuickFramebufferObject::Renderer { return linearPsnr(reconstructed, sources); }; - m_preview_textures[uncompressed_preview_index] = create_texture(gl_engine::Texture::Format::SRGBA8); + auto scratch = create_texture(gl_engine::Texture::Format::RGBA8, gl_engine::Texture::Filter::Nearest); for (size_t layer = 0; layer < sources.size(); ++layer) - m_preview_textures[uncompressed_preview_index]->upload(sources[layer], unsigned(layer)); + scratch->upload(sources[layer], unsigned(layer)); + scratch->generate_mipmaps(); + + auto reference = create_texture(gl_engine::Texture::Format::SRGBA8, gl_engine::Texture::Filter::Linear); + for (size_t layer = 0; layer < sources.size(); ++layer) + reference->upload(sources[layer], unsigned(layer)); + m_preview_textures.push_back(reference); m_preview_results.push_back({ QStringLiteral("Ref"), QStringLiteral("The original uncompressed texture array used as the visual and PSNR reference."), std::numeric_limits::infinity() }); - m_preview_textures[goofy_preview_index] = create_texture(gl_engine::Texture::Format::CompressedRGBA8); + auto copied = create_texture(gl_engine::Texture::Format::SRGBA8, filter); + gl_engine::TextureCompressor copy_compressor(scratch, copied); + if (const auto result = copy_compressor.compress(layers); !result) + return QString::fromStdString(result.error()); + m_preview_textures.push_back(copied); + m_preview_results.push_back({ QStringLiteral("GPU copy"), + QStringLiteral("The RGBA8 scratch texture copied through the portable RGBA8 framebuffer path."), + psnr(*copied) }); + + auto goofy = create_texture(gl_engine::Texture::Format::CompressedRGBA8, filter); for (size_t layer = 0; layer < sources.size(); ++layer) { const auto compressed = nucleus::utils::generate_mipmapped_colour_texture(sources[layer], algorithm); - m_preview_textures[goofy_preview_index]->upload(compressed, unsigned(layer)); + goofy->upload(compressed, unsigned(layer)); } + m_preview_textures.push_back(goofy); m_preview_results.push_back({ QStringLiteral("Goofy"), QStringLiteral("CPU reference compressed by Goofy into the device's active ETC1 or DXT1 block format."), - psnr(*m_preview_textures[goofy_preview_index]) }); - - for (size_t i = 0; i < gpu_previews.size(); ++i) { - const auto& preview = gpu_previews[i]; - auto& texture = m_preview_textures[first_gpu_preview_index + i]; - texture = create_texture(gl_engine::Texture::Format::CompressedRGBA8); - gl_engine::TextureCompressor compressor(resolution, resolution, unsigned(sources.size())); - static_cast(compressor.compress(sources, - *texture, - layers, - { - .algorithm = algorithm, - .effort = preview.effort, - .encoder = preview.encoder, - .generate_mipmaps = true, - })); + psnr(*goofy) }); + + for (const auto& preview : previews) { + auto texture = create_texture(gl_engine::Texture::Format::CompressedRGBA8, filter); + gl_engine::TextureCompressor compressor(scratch, texture, preview.settings); + if (const auto result = compressor.compress(layers); !result) + return QString::fromStdString(result.error()); + m_preview_textures.push_back(texture); m_preview_results.push_back( { QString::fromLatin1(preview.name), QString::fromLatin1(preview.description), psnr(*texture) }); } @@ -331,7 +336,7 @@ class TexturePreviewRenderer final : public QQuickFramebufferObject::Renderer { bool m_pending = false; std::vector m_source_images; std::vector m_preview_results; - std::array, preview_count> m_preview_textures; + std::vector> m_preview_textures; std::unique_ptr m_preview_shader; gl_engine::helpers::ScreenQuadGeometry m_preview_geometry; }; diff --git a/gl_engine/CMakeLists.txt b/gl_engine/CMakeLists.txt index c92fbb48..879db930 100644 --- a/gl_engine/CMakeLists.txt +++ b/gl_engine/CMakeLists.txt @@ -41,6 +41,7 @@ qt_add_library(gl_engine STATIC ShadowMapping.h ShadowMapping.cpp GpuAsyncQueryTimer.h GpuAsyncQueryTimer.cpp Texture.h Texture.cpp + TextureCompressor.h TextureCompressor.cpp TrackManager.h TrackManager.cpp Context.h Context.cpp TileGeometry.h TileGeometry.cpp @@ -87,8 +88,8 @@ qt_add_resources(gl_engine "shaders" shaders/tile_id.glsl shaders/track.frag shaders/track.vert - shaders/texture_compress_raster.vert - shaders/texture_compress.vert + shaders/texture_compress.frag + shaders/texture_copy.frag shaders/turbo_colormap.glsl shaders/intersection.glsl shaders/eaws.glsl diff --git a/gl_engine/Texture.cpp b/gl_engine/Texture.cpp index 80f89cf8..e1ce7694 100644 --- a/gl_engine/Texture.cpp +++ b/gl_engine/Texture.cpp @@ -17,7 +17,6 @@ *****************************************************************************/ #include "Texture.h" -#include "ShaderProgram.h" #include "nucleus/utils/ColourTexture.h" #include @@ -52,6 +51,8 @@ GlParams gl_tex_params(gl_engine::Texture::Format format) return { GLint(gl_engine::Texture::compressed_texture_format()), 0, 0, 0, 0, true }; case F::RGBA8: return { GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, 4, 1, true }; + case F::RGB565: + return { GL_RGB565, GL_RGB, GL_UNSIGNED_SHORT_5_6_5, 1, 2, true }; case F::SRGBA8: return { GL_SRGB8_ALPHA8, GL_RGBA, GL_UNSIGNED_BYTE, 4, 1, true }; case F::RGBA8UI: @@ -118,24 +119,39 @@ void gl_engine::Texture::setParams(Filter min_filter, Filter mag_filter, bool an f->glTexParameterf(GLenum(m_target), max_anisotropy_param(), max_anisotropy()); } -void gl_engine::Texture::allocate_array(unsigned int width, unsigned int height, unsigned int n_layers) +void gl_engine::Texture::allocate_array(unsigned int width, unsigned int height, unsigned int n_layers, unsigned mip_levels) { Q_ASSERT(m_target == Target::_2dArray); Q_ASSERT(m_format != Format::Invalid); - auto mip_level_count = 1; - if (m_min_filter == Filter::MipMapLinear) - mip_level_count = GLsizei(1 + std::floor(std::log2(std::max(width, height)))); + auto mip_level_count = GLsizei(mip_levels); + if (mip_level_count == 0) { + mip_level_count = 1; + if (m_min_filter == Filter::MipMapLinear) + mip_level_count = GLsizei(1 + std::floor(std::log2(std::max(width, height)))); + } m_width = width; m_height = height; m_n_layers = n_layers; + m_mip_levels = unsigned(mip_level_count); auto* f = QOpenGLContext::currentContext()->extraFunctions(); f->glBindTexture(GLenum(m_target), m_id); f->glTexStorage3D(GLenum(m_target), mip_level_count, gl_tex_params(m_format).internal_format, GLsizei(width), GLsizei(height), GLsizei(n_layers)); } +void gl_engine::Texture::generate_mipmaps() +{ + Q_ASSERT(m_target == Target::_2dArray); + Q_ASSERT(m_mip_levels > 1); + Q_ASSERT(m_format != Format::CompressedRGBA8); + Q_ASSERT(gl_tex_params(m_format).is_texture_filterable); + auto* f = QOpenGLContext::currentContext()->extraFunctions(); + f->glBindTexture(GLenum(m_target), m_id); + f->glGenerateMipmap(GLenum(m_target)); +} + void gl_engine::Texture::upload(const nucleus::utils::ColourTexture& texture) { QOpenGLFunctions* f = QOpenGLContext::currentContext()->functions(); @@ -381,464 +397,3 @@ float gl_engine::Texture::max_anisotropy() return max_anisotropy; #endif } - -struct gl_engine::TextureCompressor::Impl { - static constexpr unsigned max_shader_mip_levels = 16; - - unsigned width = 0; - unsigned height = 0; - unsigned max_batch_size = 0; - unsigned scratch_layers = 0; - GLsizei block_atlas_width = 0; - GLsizei block_atlas_height = 0; - GLsizei paired_atlas_width = 0; - GLsizei paired_atlas_height = 0; - GLuint scratch_texture = 0; - GLuint encoded_texture = 0; - GLuint paired_encoded_texture = 0; - GLuint encoded_buffer = 0; - GLuint vertex_array = 0; - GLuint encoding_framebuffer = 0; - GLuint paired_encoding_framebuffer = 0; - ReadbackMode readback_mode = ReadbackMode::RG32UI; - std::unique_ptr dxt1_fragment_program; - std::unique_ptr etc1_fragment_program; - std::unique_ptr etc1_fast_split_fused_exact_fragment_program; - std::unique_ptr etc1_fast_split_fused_exact_residual_fragment_program; - std::unique_ptr etc1_fast_split_fused_exact_shared_residual_fragment_program; - std::unique_ptr checksum_fragment_program; - std::unique_ptr paired_dxt1_fragment_program; - std::unique_ptr paired_etc1_fragment_program; - std::unique_ptr paired_etc1_fast_split_fused_exact_fragment_program; - std::unique_ptr paired_etc1_fast_split_fused_exact_residual_fragment_program; - std::unique_ptr paired_etc1_fast_split_fused_exact_shared_residual_fragment_program; - std::unique_ptr paired_checksum_fragment_program; - - Impl(unsigned texture_width, unsigned texture_height, unsigned maximum_batch_size) - : width(texture_width) - , height(texture_height) - , max_batch_size(maximum_batch_size) - { - Q_ASSERT(width > 0 && height > 0 && max_batch_size > 0); - Q_ASSERT(TextureCompressor::mip_level_count(width, height) <= max_shader_mip_levels); - auto* f = QOpenGLContext::currentContext()->extraFunctions(); - size_t maximum_size = 0; - for (unsigned level = 0; level < TextureCompressor::mip_level_count(width, height); ++level) { - maximum_size += TextureCompressor::compressed_level_size( - std::max(1u, width >> level), std::max(1u, height >> level)); - } - maximum_size *= max_batch_size; - - GLint maximum_texture_size = 0; - f->glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maximum_texture_size); - const auto atlas_size = [](size_t pixels, GLint maximum_dimension) { - const auto atlas_width = GLsizei(std::min({ pixels, size_t(maximum_dimension), size_t(256) })); - const auto atlas_height = GLsizei((pixels + size_t(atlas_width) - 1) / size_t(atlas_width)); - Q_ASSERT(atlas_width > 0 && atlas_height > 0 && atlas_height <= maximum_dimension); - return std::pair(atlas_width, atlas_height); - }; - std::tie(block_atlas_width, block_atlas_height) = atlas_size(maximum_size / 8, maximum_texture_size); - std::tie(paired_atlas_width, paired_atlas_height) - = atlas_size((maximum_size + 15) / 16, maximum_texture_size); - - f->glGenBuffers(1, &encoded_buffer); - f->glBindBuffer(GL_PIXEL_PACK_BUFFER, encoded_buffer); - const auto encoded_buffer_size = std::max({ - size_t(block_atlas_width) * size_t(block_atlas_height) * 8, - size_t(paired_atlas_width) * size_t(paired_atlas_height) * 16, - }); - f->glBufferData(GL_PIXEL_PACK_BUFFER, - GLsizeiptr(encoded_buffer_size), - nullptr, - GL_STREAM_DRAW); - f->glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); - f->glGenVertexArrays(1, &vertex_array); - - GLint previous_draw_framebuffer = 0; - GLint previous_read_framebuffer = 0; - GLint previous_texture = 0; - f->glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previous_draw_framebuffer); - f->glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &previous_read_framebuffer); - f->glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture); - - f->glGenTextures(1, &encoded_texture); - f->glBindTexture(GL_TEXTURE_2D, encoded_texture); - f->glTexStorage2D(GL_TEXTURE_2D, 1, GL_RG32UI, block_atlas_width, block_atlas_height); - f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - - f->glGenFramebuffers(1, &encoding_framebuffer); - f->glBindFramebuffer(GL_FRAMEBUFFER, encoding_framebuffer); - f->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, encoded_texture, 0); - const auto encoding_framebuffer_status = f->glCheckFramebufferStatus(GL_FRAMEBUFFER); - GLint implementation_read_format = 0; - GLint implementation_read_type = 0; - if (encoding_framebuffer_status == GL_FRAMEBUFFER_COMPLETE) { - f->glReadBuffer(GL_COLOR_ATTACHMENT0); - f->glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_FORMAT, &implementation_read_format); - f->glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_TYPE, &implementation_read_type); - } - if (encoding_framebuffer_status != GL_FRAMEBUFFER_COMPLETE - || implementation_read_format != GL_RG_INTEGER - || implementation_read_type != GL_UNSIGNED_INT) { - readback_mode = ReadbackMode::RGBA32UI; - qInfo().noquote() - << QStringLiteral("RG32UI readback is unavailable (framebuffer 0x%1, format 0x%2, type 0x%3); falling back to paired RGBA32UI readback.") - .arg(unsigned(encoding_framebuffer_status), 0, 16) - .arg(unsigned(implementation_read_format), 0, 16) - .arg(unsigned(implementation_read_type), 0, 16); - - f->glGenTextures(1, &paired_encoded_texture); - f->glBindTexture(GL_TEXTURE_2D, paired_encoded_texture); - f->glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA32UI, paired_atlas_width, paired_atlas_height); - f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - - f->glGenFramebuffers(1, &paired_encoding_framebuffer); - f->glBindFramebuffer(GL_FRAMEBUFFER, paired_encoding_framebuffer); - f->glFramebufferTexture2D( - GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, paired_encoded_texture, 0); - if (f->glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) - qFatal("Paired RGBA32UI texture compression framebuffer is incomplete"); - } - f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, GLuint(previous_draw_framebuffer)); - f->glBindFramebuffer(GL_READ_FRAMEBUFFER, GLuint(previous_read_framebuffer)); - f->glBindTexture(GL_TEXTURE_2D, GLuint(previous_texture)); - - } - - ShaderProgram* paired_program(Encoder encoder, nucleus::utils::ColourTexture::Format algorithm) - { - auto* program = &paired_dxt1_fragment_program; - std::vector defines { QStringLiteral("#define ALP_COMPRESS_TWO_BLOCKS") }; - if (encoder == Encoder::Checksum) { - program = &paired_checksum_fragment_program; - defines.push_back(QStringLiteral("#define ALP_COMPRESS_CHECKSUM")); - } else if (algorithm == nucleus::utils::ColourTexture::Format::ETC1) { - program = &paired_etc1_fragment_program; - defines.push_back(QStringLiteral("#define ALP_COMPRESS_ETC1")); - if (encoder == Encoder::FastSplitFusedExact) { - program = &paired_etc1_fast_split_fused_exact_fragment_program; - defines.push_back(QStringLiteral("#define ALP_COMPRESS_ETC1_SPLIT_FUSED")); - } else if (encoder == Encoder::FastSplitFusedExactResidual) { - program = &paired_etc1_fast_split_fused_exact_residual_fragment_program; - defines.push_back(QStringLiteral("#define ALP_COMPRESS_ETC1_SPLIT_FUSED")); - defines.push_back(QStringLiteral("#define ALP_COMPRESS_ETC1_REFINE_RESIDUAL")); - } else if (encoder == Encoder::FastSplitFusedExactSharedResidual) { - program = &paired_etc1_fast_split_fused_exact_shared_residual_fragment_program; - defines.push_back(QStringLiteral("#define ALP_COMPRESS_ETC1_SPLIT_FUSED")); - defines.push_back(QStringLiteral("#define ALP_COMPRESS_ETC1_REFINE_SHARED_RESIDUAL")); - } - } - if (!*program) { - *program = std::make_unique( - "texture_compress_raster.vert", "texture_compress.vert", ShaderCodeSource::FILE, defines); - } - return program->get(); - } - - ShaderProgram* single_program(Encoder encoder, nucleus::utils::ColourTexture::Format algorithm) - { - auto* program = &dxt1_fragment_program; - std::vector defines; - if (encoder == Encoder::Checksum) { - program = &checksum_fragment_program; - defines.push_back(QStringLiteral("#define ALP_COMPRESS_CHECKSUM")); - } else if (algorithm == nucleus::utils::ColourTexture::Format::ETC1) { - program = &etc1_fragment_program; - defines.push_back(QStringLiteral("#define ALP_COMPRESS_ETC1")); - if (encoder == Encoder::FastSplitFusedExact) { - program = &etc1_fast_split_fused_exact_fragment_program; - defines.push_back(QStringLiteral("#define ALP_COMPRESS_ETC1_SPLIT_FUSED")); - } else if (encoder == Encoder::FastSplitFusedExactResidual) { - program = &etc1_fast_split_fused_exact_residual_fragment_program; - defines.push_back(QStringLiteral("#define ALP_COMPRESS_ETC1_SPLIT_FUSED")); - defines.push_back(QStringLiteral("#define ALP_COMPRESS_ETC1_REFINE_RESIDUAL")); - } else if (encoder == Encoder::FastSplitFusedExactSharedResidual) { - program = &etc1_fast_split_fused_exact_shared_residual_fragment_program; - defines.push_back(QStringLiteral("#define ALP_COMPRESS_ETC1_SPLIT_FUSED")); - defines.push_back(QStringLiteral("#define ALP_COMPRESS_ETC1_REFINE_SHARED_RESIDUAL")); - } - } - if (!*program) { - *program = std::make_unique( - "texture_compress_raster.vert", "texture_compress.vert", ShaderCodeSource::FILE, defines); - } - return program->get(); - } - - ~Impl() - { - dxt1_fragment_program.reset(); - etc1_fragment_program.reset(); - etc1_fast_split_fused_exact_fragment_program.reset(); - etc1_fast_split_fused_exact_residual_fragment_program.reset(); - etc1_fast_split_fused_exact_shared_residual_fragment_program.reset(); - checksum_fragment_program.reset(); - paired_dxt1_fragment_program.reset(); - paired_etc1_fragment_program.reset(); - paired_etc1_fast_split_fused_exact_fragment_program.reset(); - paired_etc1_fast_split_fused_exact_residual_fragment_program.reset(); - paired_etc1_fast_split_fused_exact_shared_residual_fragment_program.reset(); - paired_checksum_fragment_program.reset(); - if (!QOpenGLContext::currentContext()) - return; - auto* f = QOpenGLContext::currentContext()->extraFunctions(); - f->glDeleteFramebuffers(1, &encoding_framebuffer); - f->glDeleteFramebuffers(1, &paired_encoding_framebuffer); - f->glDeleteTextures(1, &encoded_texture); - f->glDeleteTextures(1, &paired_encoded_texture); - f->glDeleteVertexArrays(1, &vertex_array); - f->glDeleteBuffers(1, &encoded_buffer); - if (scratch_texture) - f->glDeleteTextures(1, &scratch_texture); - } - - void ensure_scratch_storage(unsigned layers) - { - if (scratch_layers == layers) - return; - auto* f = QOpenGLContext::currentContext()->extraFunctions(); - if (scratch_texture) - f->glDeleteTextures(1, &scratch_texture); - f->glGenTextures(1, &scratch_texture); - f->glBindTexture(GL_TEXTURE_2D_ARRAY, scratch_texture); - f->glTexStorage3D(GL_TEXTURE_2D_ARRAY, - GLsizei(TextureCompressor::mip_level_count(width, height)), - GL_RGBA8, - GLsizei(width), - GLsizei(height), - GLsizei(layers)); - f->glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST); - f->glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - f->glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - f->glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - scratch_layers = layers; - } -}; - -gl_engine::TextureCompressor::TextureCompressor(unsigned width, unsigned height, unsigned max_batch_size) - : m(std::make_unique(width, height, max_batch_size)) -{ -} - -gl_engine::TextureCompressor::~TextureCompressor() = default; - -gl_engine::TextureCompressor::ReadbackMode gl_engine::TextureCompressor::readback_mode() const -{ - return m->readback_mode; -} - -size_t gl_engine::TextureCompressor::compressed_level_size(unsigned width, unsigned height) -{ - return size_t(std::max(1u, (width + 3) / 4)) * std::max(1u, (height + 3) / 4) * 8; -} - -unsigned gl_engine::TextureCompressor::mip_level_count(unsigned width, unsigned height) -{ - Q_ASSERT(width > 0 && height > 0); - return 1u + unsigned(std::floor(std::log2(std::max(width, height)))); -} - -bool gl_engine::TextureCompressor::is_supported() -{ -#if defined(__EMSCRIPTEN__) - const auto context = emscripten_webgl_get_current_context(); - if (!context) - return false; - if (emscripten_webgl_enable_extension(context, "WEBGL_compressed_texture_etc")) - return true; - return emscripten_webgl_enable_extension(context, "WEBGL_compressed_texture_s3tc") - && emscripten_webgl_enable_extension(context, "WEBGL_compressed_texture_s3tc_srgb"); -#else - return true; -#endif -} - -gl_engine::TextureCompressor::Result gl_engine::TextureCompressor::compress(std::span> textures, - Texture& destination, - std::span destination_layers, - const Settings& settings) -{ - Q_ASSERT(is_supported()); - Q_ASSERT(!textures.empty()); - Q_ASSERT(textures.size() == destination_layers.size()); - Q_ASSERT(textures.size() <= m->max_batch_size); - Q_ASSERT(destination.m_target == Texture::Target::_2dArray); - Q_ASSERT(destination.m_format == Texture::Format::CompressedRGBA8); - Q_ASSERT(destination.m_width == m->width && destination.m_height == m->height); - Q_ASSERT(settings.algorithm == Texture::compression_algorithm()); - Q_ASSERT(settings.effort <= 10); - for (size_t i = 0; i < textures.size(); ++i) { - Q_ASSERT(unsigned(textures[i].width()) == m->width && unsigned(textures[i].height()) == m->height); - Q_ASSERT(destination_layers[i] < destination.m_n_layers); - } - - m->ensure_scratch_storage(unsigned(textures.size())); - auto* f = QOpenGLContext::currentContext()->extraFunctions(); - Result result; - result.mip_levels = settings.generate_mipmaps ? mip_level_count(m->width, m->height) : 1; - - { - f->glBindTexture(GL_TEXTURE_2D_ARRAY, m->scratch_texture); - f->glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - for (size_t layer = 0; layer < textures.size(); ++layer) { - f->glTexSubImage3D(GL_TEXTURE_2D_ARRAY, - 0, - 0, - 0, - GLint(layer), - GLsizei(m->width), - GLsizei(m->height), - 1, - GL_RGBA, - GL_UNSIGNED_BYTE, - textures[layer].bytes().data()); - } - } - if (settings.generate_mipmaps) { - f->glBindTexture(GL_TEXTURE_2D_ARRAY, m->scratch_texture); - f->glGenerateMipmap(GL_TEXTURE_2D_ARRAY); - } - - std::vector level_offsets; - std::vector level_offsets_blocks; - std::vector level_blocks_x; - std::vector level_blocks_y; - level_offsets.reserve(result.mip_levels); - level_offsets_blocks.reserve(result.mip_levels); - level_blocks_x.reserve(result.mip_levels); - level_blocks_y.reserve(result.mip_levels); - size_t total_encoded_size = 0; - for (unsigned level = 0; level < result.mip_levels; ++level) { - level_offsets.push_back(total_encoded_size); - const auto level_width = std::max(1u, m->width >> level); - const auto level_height = std::max(1u, m->height >> level); - level_offsets_blocks.push_back(int(total_encoded_size / 8)); - level_blocks_x.push_back(int(std::max(1u, (level_width + 3) / 4))); - level_blocks_y.push_back(int(std::max(1u, (level_height + 3) / 4))); - total_encoded_size += compressed_level_size(level_width, level_height) * textures.size(); - } - result.encoded_bytes = total_encoded_size; - - GLint previous_draw_framebuffer = 0; - GLint previous_viewport[4] = {}; - GLboolean previous_colour_mask[4] = {}; - GLboolean blend_enabled = GL_FALSE; - GLboolean cull_enabled = GL_FALSE; - GLboolean depth_enabled = GL_FALSE; - GLboolean scissor_enabled = GL_FALSE; - const auto paired_blocks = m->readback_mode == ReadbackMode::RGBA32UI; - const auto total_blocks = total_encoded_size / 8; - const auto encoding_pixels = paired_blocks ? (total_blocks + 1) / 2 : total_blocks; - const auto maximum_encoding_width = paired_blocks ? m->paired_atlas_width : m->block_atlas_width; - const auto encoding_width = GLsizei(std::min(encoding_pixels, size_t(maximum_encoding_width))); - const auto encoding_height = GLsizei((encoding_pixels + size_t(encoding_width) - 1) / size_t(encoding_width)); - - { - auto* program = paired_blocks ? m->paired_program(settings.encoder, settings.algorithm) - : m->single_program(settings.encoder, settings.algorithm); - f->glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previous_draw_framebuffer); - f->glGetIntegerv(GL_VIEWPORT, previous_viewport); - f->glGetBooleanv(GL_COLOR_WRITEMASK, previous_colour_mask); - blend_enabled = f->glIsEnabled(GL_BLEND); - cull_enabled = f->glIsEnabled(GL_CULL_FACE); - depth_enabled = f->glIsEnabled(GL_DEPTH_TEST); - scissor_enabled = f->glIsEnabled(GL_SCISSOR_TEST); - - f->glDisable(GL_BLEND); - f->glDisable(GL_CULL_FACE); - f->glDisable(GL_DEPTH_TEST); - f->glDisable(GL_SCISSOR_TEST); - f->glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); - - f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, - paired_blocks ? m->paired_encoding_framebuffer : m->encoding_framebuffer); - f->glViewport(0, 0, encoding_width, encoding_height); - program->bind(); - program->set_uniform("source_texture", 7); - program->set_uniform("texture_width", int(m->width)); - program->set_uniform("texture_height", int(m->height)); - program->set_uniform("effort", int(settings.effort)); - program->set_uniform("atlas_width", int(encoding_width)); - program->set_uniform("total_blocks", int(total_blocks)); - program->set_uniform("mip_levels", int(result.mip_levels)); - program->set_uniform_array("level_offsets", level_offsets_blocks); - program->set_uniform_array("level_blocks_x", level_blocks_x); - program->set_uniform_array("level_blocks_y", level_blocks_y); - f->glActiveTexture(GL_TEXTURE7); - f->glBindTexture(GL_TEXTURE_2D_ARRAY, m->scratch_texture); - f->glBindVertexArray(m->vertex_array); - f->glDrawArrays(GL_TRIANGLES, 0, 3); - program->release(); - } - - GLsizei readback_width = encoding_width; - GLsizei readback_height = encoding_height; - GLuint readback_framebuffer = paired_blocks ? m->paired_encoding_framebuffer : m->encoding_framebuffer; - GLenum readback_format = paired_blocks ? GL_RGBA_INTEGER : GL_RG_INTEGER; - - f->glBindVertexArray(0); - if (blend_enabled) - f->glEnable(GL_BLEND); - if (cull_enabled) - f->glEnable(GL_CULL_FACE); - if (depth_enabled) - f->glEnable(GL_DEPTH_TEST); - if (scissor_enabled) - f->glEnable(GL_SCISSOR_TEST); - f->glColorMask(previous_colour_mask[0], previous_colour_mask[1], previous_colour_mask[2], previous_colour_mask[3]); - f->glViewport(previous_viewport[0], previous_viewport[1], previous_viewport[2], previous_viewport[3]); - f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, GLuint(previous_draw_framebuffer)); - - { - GLint previous_read_framebuffer = 0; - GLint previous_read_buffer = 0; - GLint previous_pack_alignment = 0; - f->glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &previous_read_framebuffer); - f->glGetIntegerv(GL_READ_BUFFER, &previous_read_buffer); - f->glGetIntegerv(GL_PACK_ALIGNMENT, &previous_pack_alignment); - f->glBindFramebuffer(GL_READ_FRAMEBUFFER, readback_framebuffer); - f->glReadBuffer(GL_COLOR_ATTACHMENT0); - f->glPixelStorei(GL_PACK_ALIGNMENT, 1); - f->glBindBuffer(GL_PIXEL_PACK_BUFFER, m->encoded_buffer); - f->glReadPixels( - 0, 0, readback_width, readback_height, readback_format, GL_UNSIGNED_INT, nullptr); - f->glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); - f->glPixelStorei(GL_PACK_ALIGNMENT, previous_pack_alignment); - f->glBindFramebuffer(GL_READ_FRAMEBUFFER, GLuint(previous_read_framebuffer)); - f->glReadBuffer(GLenum(previous_read_buffer)); - } - - { - f->glBindTexture(GL_TEXTURE_2D_ARRAY, destination.m_id); - f->glBindBuffer(GL_PIXEL_UNPACK_BUFFER, m->encoded_buffer); - const auto format = Texture::compressed_texture_format(); - for (unsigned level = 0; level < result.mip_levels; ++level) { - const auto level_width = std::max(1u, m->width >> level); - const auto level_height = std::max(1u, m->height >> level); - const auto layer_size = compressed_level_size(level_width, level_height); - for (size_t layer = 0; layer < textures.size(); ++layer) { - const auto offset = level_offsets[level] + layer_size * layer; - f->glCompressedTexSubImage3D(GL_TEXTURE_2D_ARRAY, - GLint(level), - 0, - 0, - GLint(destination_layers[layer]), - GLsizei(level_width), - GLsizei(level_height), - 1, - format, - GLsizei(layer_size), - reinterpret_cast(quintptr(offset))); - } - } - f->glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); - } - f->glActiveTexture(GL_TEXTURE0); - return result; -} diff --git a/gl_engine/Texture.h b/gl_engine/Texture.h index ae22ce86..ad373ca1 100644 --- a/gl_engine/Texture.h +++ b/gl_engine/Texture.h @@ -20,10 +20,7 @@ #include #include -#include #include -#include -#include #ifdef ANDROID #include #endif @@ -38,6 +35,7 @@ class Texture { enum class Target : GLenum { _2d = GL_TEXTURE_2D, _2dArray = GL_TEXTURE_2D_ARRAY }; // no 1D textures in webgl enum class Format { RGBA8, // normalised on gpu + RGB565, // normalised on gpu SRGBA8, // normalised on gpu CompressedRGBA8, // normalised on gpu, compression format depends on desktop/mobile RGBA8UI, @@ -62,7 +60,8 @@ class Texture { void bind(unsigned texture_unit); void setParams(Filter min_filter, Filter mag_filter, bool anisotropic_filtering = false); - void allocate_array(unsigned width, unsigned height, unsigned n_layers); + void allocate_array(unsigned width, unsigned height, unsigned n_layers, unsigned mip_levels = 0); + void generate_mipmaps(); void upload(const nucleus::utils::ColourTexture& texture); void upload(const nucleus::utils::ColourTexture& texture, unsigned array_index); void upload(const nucleus::utils::MipmappedColourTexture& mipped_texture, unsigned array_index); @@ -87,56 +86,7 @@ class Texture { unsigned m_width = unsigned(-1); unsigned m_height = unsigned(-1); unsigned m_n_layers = unsigned(-1); -}; - -class TextureCompressor { -public: - enum class Encoder { - Search, - Dxt1, - FastSplitFusedExact, - FastSplitFusedExactResidual, - FastSplitFusedExactSharedResidual, - Checksum - }; - - enum class ReadbackMode { - RG32UI, - RGBA32UI, - }; - - struct Settings { - nucleus::utils::ColourTexture::Format algorithm = nucleus::utils::ColourTexture::Format::DXT1; - unsigned effort = 0; - Encoder encoder = Encoder::Search; - bool generate_mipmaps = true; - }; - - struct Result { - size_t encoded_bytes = 0; - unsigned mip_levels = 0; - }; - - TextureCompressor(unsigned width, unsigned height, unsigned max_batch_size); - ~TextureCompressor(); - TextureCompressor(const TextureCompressor&) = delete; - TextureCompressor(TextureCompressor&&) = delete; - TextureCompressor& operator=(const TextureCompressor&) = delete; - TextureCompressor& operator=(TextureCompressor&&) = delete; - - [[nodiscard]] Result compress(std::span> textures, - Texture& destination, - std::span destination_layers, - const Settings& settings); - - [[nodiscard]] static size_t compressed_level_size(unsigned width, unsigned height); - [[nodiscard]] static unsigned mip_level_count(unsigned width, unsigned height); - [[nodiscard]] static bool is_supported(); - [[nodiscard]] ReadbackMode readback_mode() const; - -private: - struct Impl; - std::unique_ptr m; + unsigned m_mip_levels = unsigned(-1); }; extern template void gl_engine::Texture::upload(const radix::Raster&); diff --git a/gl_engine/TextureCompressor.cpp b/gl_engine/TextureCompressor.cpp new file mode 100644 index 00000000..8a217b3a --- /dev/null +++ b/gl_engine/TextureCompressor.cpp @@ -0,0 +1,523 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2026 Adam Celarek + * + * 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 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#include "TextureCompressor.h" + +#include "Framebuffer.h" +#include "ShaderProgram.h" +#include "Texture.h" +#include "helpers.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace { +constexpr unsigned max_shader_mip_levels = 16; + +std::optional contract_error(bool condition, const char* message) +{ + if (condition) + return std::nullopt; + Q_ASSERT_X(false, "TextureCompressor", message); + return std::string(message); +} + +struct DrawState { + GLint draw_framebuffer = 0; + GLint viewport[4] = {}; + GLboolean colour_mask[4] = {}; + GLboolean blend_enabled = GL_FALSE; + GLboolean cull_enabled = GL_FALSE; + GLboolean depth_enabled = GL_FALSE; + GLboolean scissor_enabled = GL_FALSE; + + explicit DrawState(QOpenGLExtraFunctions* f) + { + f->glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &draw_framebuffer); + f->glGetIntegerv(GL_VIEWPORT, viewport); + f->glGetBooleanv(GL_COLOR_WRITEMASK, colour_mask); + blend_enabled = f->glIsEnabled(GL_BLEND); + cull_enabled = f->glIsEnabled(GL_CULL_FACE); + depth_enabled = f->glIsEnabled(GL_DEPTH_TEST); + scissor_enabled = f->glIsEnabled(GL_SCISSOR_TEST); + } + + void prepare(QOpenGLExtraFunctions* f) const + { + f->glDisable(GL_BLEND); + f->glDisable(GL_CULL_FACE); + f->glDisable(GL_DEPTH_TEST); + f->glDisable(GL_SCISSOR_TEST); + f->glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + } + + void restore(QOpenGLExtraFunctions* f) const + { + if (blend_enabled) + f->glEnable(GL_BLEND); + else + f->glDisable(GL_BLEND); + if (cull_enabled) + f->glEnable(GL_CULL_FACE); + else + f->glDisable(GL_CULL_FACE); + if (depth_enabled) + f->glEnable(GL_DEPTH_TEST); + else + f->glDisable(GL_DEPTH_TEST); + if (scissor_enabled) + f->glEnable(GL_SCISSOR_TEST); + else + f->glDisable(GL_SCISSOR_TEST); + f->glColorMask(colour_mask[0], colour_mask[1], colour_mask[2], colour_mask[3]); + f->glViewport(viewport[0], viewport[1], viewport[2], viewport[3]); + f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, GLuint(draw_framebuffer)); + } +}; +} + +gl_engine::TextureCompressor::TextureCompressor(std::weak_ptr scratch, std::weak_ptr destination) + : TextureCompressor(std::move(scratch), std::move(destination), Settings {}) +{ +} + +gl_engine::TextureCompressor::TextureCompressor(std::weak_ptr scratch, + std::weak_ptr destination, + Settings settings) + : m_scratch(std::move(scratch)) + , m_destination(std::move(destination)) + , m_settings(settings) +{ + m_initialisation_error = initialise(); +} + +gl_engine::TextureCompressor::~TextureCompressor() +{ + m_program.reset(); + m_copy_framebuffer.reset(); + m_screen_quad.reset(); + if (!QOpenGLContext::currentContext()) + return; + auto* f = QOpenGLContext::currentContext()->extraFunctions(); + f->glDeleteFramebuffers(1, &m_encoding_framebuffer); + f->glDeleteTextures(1, &m_encoded_texture); + f->glDeleteBuffers(1, &m_encoded_buffer); +} + +std::optional gl_engine::TextureCompressor::initialise() +{ + auto scratch = m_scratch.lock(); + auto destination = m_destination.lock(); + if (!scratch || !destination) + return "Texture compressor input or destination expired during construction"; + if (auto error = validate_textures(*scratch, *destination)) + return error; + if (auto error = contract_error(m_settings.search_effort <= 10, "Texture compression search effort must be at most 10")) + return error; + + m_width = scratch->m_width; + m_height = scratch->m_height; + m_scratch_layers = scratch->m_n_layers; + m_destination_layers = destination->m_n_layers; + m_mip_levels = scratch->m_mip_levels; + m_screen_quad = std::make_unique(helpers::create_screen_quad_geometry()); + + if (destination->m_format == Texture::Format::SRGBA8) { + m_operation = Operation::Copy; + m_copy_framebuffer = std::make_unique( + Framebuffer::DepthFormat::None, + std::vector { Framebuffer::ColourFormat::RGBA8 }, + glm::uvec2 { m_width, m_height }); + m_program = std::make_unique("screen_pass.vert", "texture_copy.frag"); + auto* f = QOpenGLContext::currentContext()->extraFunctions(); + f->glGenBuffers(1, &m_encoded_buffer); + f->glBindBuffer(GL_PIXEL_PACK_BUFFER, m_encoded_buffer); + f->glBufferData(GL_PIXEL_PACK_BUFFER, GLsizeiptr(size_t(m_width) * m_height * 4), nullptr, GL_STREAM_DRAW); + f->glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + return std::nullopt; + } + + const auto format = Texture::compression_algorithm(); + m_operation = format == nucleus::utils::ColourTexture::Format::DXT1 ? Operation::Dxt1 : Operation::Etc; + + size_t maximum_size = 0; + for (unsigned level = 0; level < m_mip_levels; ++level) { + maximum_size += compressed_level_size( + std::max(1u, m_width >> level), std::max(1u, m_height >> level)); + } + maximum_size *= m_scratch_layers; + + auto* f = QOpenGLContext::currentContext()->extraFunctions(); + GLint maximum_texture_size = 0; + f->glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maximum_texture_size); + + const auto create_output = [&](ReadbackMode mode) -> std::optional { + const auto bytes_per_pixel = mode == ReadbackMode::RG32UI ? size_t(8) : size_t(16); + const auto pixels = (maximum_size + bytes_per_pixel - 1) / bytes_per_pixel; + m_atlas_width = GLsizei(std::min({ pixels, size_t(maximum_texture_size), size_t(256) })); + m_atlas_height = GLsizei((pixels + size_t(m_atlas_width) - 1) / size_t(m_atlas_width)); + if (m_atlas_width <= 0 || m_atlas_height <= 0 || m_atlas_height > maximum_texture_size) + return "Texture compression output atlas exceeds the maximum texture size"; + + const auto internal_format = mode == ReadbackMode::RG32UI ? GL_RG32UI : GL_RGBA32UI; + f->glGenTextures(1, &m_encoded_texture); + f->glBindTexture(GL_TEXTURE_2D, m_encoded_texture); + f->glTexStorage2D(GL_TEXTURE_2D, 1, internal_format, m_atlas_width, m_atlas_height); + f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + f->glGenFramebuffers(1, &m_encoding_framebuffer); + f->glBindFramebuffer(GL_FRAMEBUFFER, m_encoding_framebuffer); + f->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_encoded_texture, 0); + const auto framebuffer_status = f->glCheckFramebufferStatus(GL_FRAMEBUFFER); + if (framebuffer_status != GL_FRAMEBUFFER_COMPLETE) + return "Texture compression framebuffer is incomplete"; + + if (mode == ReadbackMode::RG32UI) { + GLint implementation_read_format = 0; + GLint implementation_read_type = 0; + f->glReadBuffer(GL_COLOR_ATTACHMENT0); + f->glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_FORMAT, &implementation_read_format); + f->glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_TYPE, &implementation_read_type); + if (implementation_read_format != GL_RG_INTEGER || implementation_read_type != GL_UNSIGNED_INT) + return "RG32UI framebuffer readback is unavailable"; + } + return std::nullopt; + }; + + GLint previous_draw_framebuffer = 0; + GLint previous_read_framebuffer = 0; + GLint previous_texture = 0; + f->glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &previous_draw_framebuffer); + f->glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &previous_read_framebuffer); + f->glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture); + + auto requested_mode = m_settings.readback_mode; + auto selected_mode = requested_mode == ReadbackMode::RGBA32UI ? ReadbackMode::RGBA32UI : ReadbackMode::RG32UI; + auto output_error = create_output(selected_mode); + if (output_error && requested_mode == ReadbackMode::Auto) { + f->glDeleteFramebuffers(1, &m_encoding_framebuffer); + f->glDeleteTextures(1, &m_encoded_texture); + m_encoding_framebuffer = 0; + m_encoded_texture = 0; + selected_mode = ReadbackMode::RGBA32UI; + output_error = create_output(selected_mode); + if (!output_error) + qInfo() << "RG32UI texture compression readback is unavailable; using RGBA32UI"; + } + + f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, GLuint(previous_draw_framebuffer)); + f->glBindFramebuffer(GL_READ_FRAMEBUFFER, GLuint(previous_read_framebuffer)); + f->glBindTexture(GL_TEXTURE_2D, GLuint(previous_texture)); + if (output_error) + return output_error; + m_effective_readback_mode = selected_mode; + + f->glGenBuffers(1, &m_encoded_buffer); + f->glBindBuffer(GL_PIXEL_PACK_BUFFER, m_encoded_buffer); + const auto bytes_per_pixel = selected_mode == ReadbackMode::RG32UI ? size_t(8) : size_t(16); + f->glBufferData(GL_PIXEL_PACK_BUFFER, + GLsizeiptr(size_t(m_atlas_width) * m_atlas_height * bytes_per_pixel), + nullptr, + GL_STREAM_DRAW); + f->glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + + std::vector defines; + if (selected_mode == ReadbackMode::RGBA32UI) + defines.push_back(QStringLiteral("#define ALP_COMPRESS_TWO_BLOCKS")); + if ((m_operation == Operation::Dxt1 && m_settings.dxt1_algorithm == Dxt1Algorithm::DebugChecksum) + || (m_operation == Operation::Etc && m_settings.etc_algorithm == EtcAlgorithm::DebugChecksum)) { + defines.push_back(QStringLiteral("#define ALP_COMPRESS_CHECKSUM")); + } else if (m_operation == Operation::Etc) { + defines.push_back(QStringLiteral("#define ALP_COMPRESS_ETC1")); + if (m_settings.etc_algorithm == EtcAlgorithm::Fastest || m_settings.etc_algorithm == EtcAlgorithm::Fast) + defines.push_back(QStringLiteral("#define ALP_COMPRESS_ETC1_SPLIT_FUSED")); + if (m_settings.etc_algorithm == EtcAlgorithm::Fast) + defines.push_back(QStringLiteral("#define ALP_COMPRESS_ETC1_REFINE_RESIDUAL")); + } + m_program = std::make_unique( + "screen_pass.vert", "texture_compress.frag", ShaderCodeSource::FILE, defines); + return std::nullopt; +} + +std::optional gl_engine::TextureCompressor::validate_textures( + const Texture& scratch, const Texture& destination) const +{ + if (auto error = contract_error(scratch.m_target == Texture::Target::_2dArray, + "Texture compressor scratch must be a 2D texture array")) + return error; + if (auto error = contract_error(destination.m_target == Texture::Target::_2dArray, + "Texture compressor destination must be a 2D texture array")) + return error; + if (auto error = contract_error(scratch.m_format == Texture::Format::RGBA8 || scratch.m_format == Texture::Format::RGB565, + "Texture compressor scratch must use non-sRGB RGBA8 or RGB565 storage")) + return error; + if (auto error = contract_error(destination.m_format == Texture::Format::CompressedRGBA8 + || destination.m_format == Texture::Format::SRGBA8, + "Texture compressor destination must use compressed sRGB or SRGBA8 storage")) + return error; + if (auto error = contract_error(scratch.m_width == destination.m_width && scratch.m_height == destination.m_height, + "Texture compressor scratch and destination sizes must agree")) + return error; + if (auto error = contract_error(scratch.m_mip_levels == destination.m_mip_levels, + "Texture compressor scratch and destination mip counts must agree")) + return error; + if (auto error = contract_error(scratch.m_width > 0 && scratch.m_height > 0 && scratch.m_n_layers > 0 + && destination.m_n_layers > 0 && scratch.m_mip_levels > 0, + "Texture compressor textures must have allocated storage")) + return error; + if (auto error = contract_error(scratch.m_mip_levels <= max_shader_mip_levels, + "Texture compressor supports at most 16 mip levels")) + return error; + return std::nullopt; +} + +std::expected gl_engine::TextureCompressor::compress( + std::span destination_layers) +{ + if (m_initialisation_error) + return std::unexpected(*m_initialisation_error); + auto scratch = m_scratch.lock(); + auto destination = m_destination.lock(); + if (!scratch || !destination) + return std::unexpected("Texture compressor input or destination has expired"); + if (auto error = validate_textures(*scratch, *destination)) + return std::unexpected(*error); + if (scratch->m_width != m_width || scratch->m_height != m_height || scratch->m_n_layers != m_scratch_layers + || scratch->m_mip_levels != m_mip_levels || destination->m_n_layers != m_destination_layers) { + Q_ASSERT_X(false, "TextureCompressor", "Texture storage changed after compressor construction"); + return std::unexpected("Texture storage changed after compressor construction"); + } + if (auto error = contract_error(!destination_layers.empty(), "Texture compressor requires at least one layer")) + return std::unexpected(*error); + if (auto error = contract_error(destination_layers.size() <= m_scratch_layers, + "Texture compressor batch exceeds the scratch layer count")) + return std::unexpected(*error); + for (const auto layer : destination_layers) { + if (auto error = contract_error(layer < m_destination_layers, + "Texture compressor destination layer is out of range")) + return std::unexpected(*error); + } + + if (m_operation == Operation::Copy) + return copy_srgb(*scratch, *destination, destination_layers); + return compress_blocks(*scratch, *destination, destination_layers); +} + +std::expected gl_engine::TextureCompressor::compress_blocks( + const Texture& scratch, Texture& destination, std::span destination_layers) +{ + Result result { + .bytes_written = 0, + .layers_written = unsigned(destination_layers.size()), + .mip_levels_written = m_mip_levels, + }; + std::vector level_offsets; + std::vector level_offsets_blocks; + std::vector level_blocks_x; + std::vector level_blocks_y; + level_offsets.reserve(m_mip_levels); + level_offsets_blocks.reserve(m_mip_levels); + level_blocks_x.reserve(m_mip_levels); + level_blocks_y.reserve(m_mip_levels); + for (unsigned level = 0; level < m_mip_levels; ++level) { + level_offsets.push_back(result.bytes_written); + const auto level_width = std::max(1u, m_width >> level); + const auto level_height = std::max(1u, m_height >> level); + level_offsets_blocks.push_back(int(result.bytes_written / 8)); + level_blocks_x.push_back(int(std::max(1u, (level_width + 3) / 4))); + level_blocks_y.push_back(int(std::max(1u, (level_height + 3) / 4))); + result.bytes_written += compressed_level_size(level_width, level_height) * destination_layers.size(); + } + + auto* f = QOpenGLContext::currentContext()->extraFunctions(); + const DrawState draw_state(f); + draw_state.prepare(f); + const auto paired_blocks = *m_effective_readback_mode == ReadbackMode::RGBA32UI; + const auto total_blocks = result.bytes_written / 8; + const auto encoding_pixels = paired_blocks ? (total_blocks + 1) / 2 : total_blocks; + const auto encoding_width = GLsizei(std::min(encoding_pixels, size_t(m_atlas_width))); + const auto encoding_height = GLsizei((encoding_pixels + size_t(encoding_width) - 1) / size_t(encoding_width)); + + f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_encoding_framebuffer); + f->glViewport(0, 0, encoding_width, encoding_height); + m_program->bind(); + m_program->set_uniform("source_texture", 7); + m_program->set_uniform("texture_width", int(m_width)); + m_program->set_uniform("texture_height", int(m_height)); + m_program->set_uniform("effort", int(m_settings.search_effort)); + m_program->set_uniform("atlas_width", int(encoding_width)); + m_program->set_uniform("total_blocks", int(total_blocks)); + m_program->set_uniform("mip_levels", int(m_mip_levels)); + m_program->set_uniform_array("level_offsets", level_offsets_blocks); + m_program->set_uniform_array("level_blocks_x", level_blocks_x); + m_program->set_uniform_array("level_blocks_y", level_blocks_y); + f->glActiveTexture(GL_TEXTURE7); + f->glBindTexture(GL_TEXTURE_2D_ARRAY, scratch.m_id); + m_screen_quad->draw(); + m_program->release(); + draw_state.restore(f); + + GLint previous_read_framebuffer = 0; + GLint previous_read_buffer = 0; + GLint previous_pack_alignment = 0; + f->glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &previous_read_framebuffer); + f->glGetIntegerv(GL_READ_BUFFER, &previous_read_buffer); + f->glGetIntegerv(GL_PACK_ALIGNMENT, &previous_pack_alignment); + f->glBindFramebuffer(GL_READ_FRAMEBUFFER, m_encoding_framebuffer); + f->glReadBuffer(GL_COLOR_ATTACHMENT0); + f->glPixelStorei(GL_PACK_ALIGNMENT, 1); + f->glBindBuffer(GL_PIXEL_PACK_BUFFER, m_encoded_buffer); + f->glReadPixels(0, + 0, + encoding_width, + encoding_height, + paired_blocks ? GL_RGBA_INTEGER : GL_RG_INTEGER, + GL_UNSIGNED_INT, + nullptr); + f->glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + f->glPixelStorei(GL_PACK_ALIGNMENT, previous_pack_alignment); + f->glBindFramebuffer(GL_READ_FRAMEBUFFER, GLuint(previous_read_framebuffer)); + f->glReadBuffer(GLenum(previous_read_buffer)); + + f->glBindTexture(GL_TEXTURE_2D_ARRAY, destination.m_id); + f->glBindBuffer(GL_PIXEL_UNPACK_BUFFER, m_encoded_buffer); + const auto format = Texture::compressed_texture_format(); + for (unsigned level = 0; level < m_mip_levels; ++level) { + const auto level_width = std::max(1u, m_width >> level); + const auto level_height = std::max(1u, m_height >> level); + const auto layer_size = compressed_level_size(level_width, level_height); + for (size_t layer = 0; layer < destination_layers.size(); ++layer) { + const auto offset = level_offsets[level] + layer_size * layer; + f->glCompressedTexSubImage3D(GL_TEXTURE_2D_ARRAY, + GLint(level), + 0, + 0, + GLint(destination_layers[layer]), + GLsizei(level_width), + GLsizei(level_height), + 1, + format, + GLsizei(layer_size), + reinterpret_cast(quintptr(offset))); + } + } + f->glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); + f->glActiveTexture(GL_TEXTURE0); + return result; +} + +std::expected gl_engine::TextureCompressor::copy_srgb( + const Texture& scratch, Texture& destination, std::span destination_layers) +{ + Result result { + .bytes_written = 0, + .layers_written = unsigned(destination_layers.size()), + .mip_levels_written = m_mip_levels, + }; + auto* f = QOpenGLContext::currentContext()->extraFunctions(); + const DrawState draw_state(f); + GLint previous_read_framebuffer = 0; + GLint previous_read_buffer = 0; + f->glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &previous_read_framebuffer); + f->glGetIntegerv(GL_READ_BUFFER, &previous_read_buffer); + draw_state.prepare(f); + m_copy_framebuffer->bind(); + m_program->bind(); + m_program->set_uniform("source_texture", 7); + f->glActiveTexture(GL_TEXTURE7); + f->glBindTexture(GL_TEXTURE_2D_ARRAY, scratch.m_id); + + GLint previous_pack_alignment = 0; + GLint previous_unpack_alignment = 0; + f->glGetIntegerv(GL_PACK_ALIGNMENT, &previous_pack_alignment); + f->glGetIntegerv(GL_UNPACK_ALIGNMENT, &previous_unpack_alignment); + f->glPixelStorei(GL_PACK_ALIGNMENT, 1); + f->glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + + for (unsigned level = 0; level < m_mip_levels; ++level) { + const auto level_width = std::max(1u, m_width >> level); + const auto level_height = std::max(1u, m_height >> level); + const auto layer_size = size_t(level_width) * level_height * 4; + f->glViewport(0, 0, GLsizei(level_width), GLsizei(level_height)); + m_program->set_uniform("source_level", int(level)); + for (size_t layer = 0; layer < destination_layers.size(); ++layer) { + m_program->set_uniform("source_layer", int(layer)); + m_screen_quad->draw(); + + f->glReadBuffer(GL_COLOR_ATTACHMENT0); + f->glBindBuffer(GL_PIXEL_PACK_BUFFER, m_encoded_buffer); + f->glReadPixels(0, + 0, + GLsizei(level_width), + GLsizei(level_height), + GL_RGBA, + GL_UNSIGNED_BYTE, + nullptr); + f->glActiveTexture(GL_TEXTURE0); + f->glBindTexture(GL_TEXTURE_2D_ARRAY, destination.m_id); + f->glBindBuffer(GL_PIXEL_UNPACK_BUFFER, m_encoded_buffer); + f->glTexSubImage3D(GL_TEXTURE_2D_ARRAY, + GLint(level), + 0, + 0, + GLint(destination_layers[layer]), + GLsizei(level_width), + GLsizei(level_height), + 1, + GL_RGBA, + GL_UNSIGNED_BYTE, + nullptr); + f->glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); + f->glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + f->glActiveTexture(GL_TEXTURE7); + result.bytes_written += layer_size; + } + } + f->glPixelStorei(GL_PACK_ALIGNMENT, previous_pack_alignment); + f->glPixelStorei(GL_UNPACK_ALIGNMENT, previous_unpack_alignment); + m_program->release(); + draw_state.restore(f); + f->glBindFramebuffer(GL_READ_FRAMEBUFFER, GLuint(previous_read_framebuffer)); + f->glReadBuffer(GLenum(previous_read_buffer)); + f->glActiveTexture(GL_TEXTURE0); + return result; +} + +size_t gl_engine::TextureCompressor::compressed_level_size(unsigned width, unsigned height) +{ + return size_t(std::max(1u, (width + 3) / 4)) * std::max(1u, (height + 3) / 4) * 8; +} + +unsigned gl_engine::TextureCompressor::mip_level_count(unsigned width, unsigned height) +{ + Q_ASSERT(width > 0 && height > 0); + return 1u + unsigned(std::floor(std::log2(std::max(width, height)))); +} + +std::optional gl_engine::TextureCompressor::effective_readback_mode() const +{ + return m_effective_readback_mode; +} diff --git a/gl_engine/TextureCompressor.h b/gl_engine/TextureCompressor.h new file mode 100644 index 00000000..276b5e03 --- /dev/null +++ b/gl_engine/TextureCompressor.h @@ -0,0 +1,123 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2026 Adam Celarek + * + * 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 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + *****************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace gl_engine { +class Framebuffer; +class ShaderProgram; +class Texture; +namespace helpers { +struct ScreenQuadGeometry; +} + +class TextureCompressor { +public: + enum class ReadbackMode { + Auto, + RG32UI, + RGBA32UI, + }; + + enum class Dxt1Algorithm { + SlowSearch, + DebugChecksum, + }; + + enum class EtcAlgorithm { + Fastest, + Fast, + SlowSearch, + DebugChecksum, + }; + + struct Settings { + ReadbackMode readback_mode = ReadbackMode::Auto; + Dxt1Algorithm dxt1_algorithm = Dxt1Algorithm::SlowSearch; + EtcAlgorithm etc_algorithm = EtcAlgorithm::Fast; + unsigned search_effort = 0; + }; + + struct Result { + size_t bytes_written = 0; + unsigned layers_written = 0; + unsigned mip_levels_written = 0; + }; + + TextureCompressor(std::weak_ptr scratch, + std::weak_ptr destination); + TextureCompressor(std::weak_ptr scratch, + std::weak_ptr destination, + Settings settings); + ~TextureCompressor(); + TextureCompressor(const TextureCompressor&) = delete; + TextureCompressor(TextureCompressor&&) = delete; + TextureCompressor& operator=(const TextureCompressor&) = delete; + TextureCompressor& operator=(TextureCompressor&&) = delete; + + [[nodiscard]] std::expected compress(std::span destination_layers); + + [[nodiscard]] static size_t compressed_level_size(unsigned width, unsigned height); + [[nodiscard]] static unsigned mip_level_count(unsigned width, unsigned height); + [[nodiscard]] std::optional effective_readback_mode() const; + +private: + enum class Operation { + Dxt1, + Etc, + Copy, + }; + + [[nodiscard]] std::optional initialise(); + [[nodiscard]] std::optional validate_textures(const Texture& scratch, const Texture& destination) const; + [[nodiscard]] std::expected compress_blocks( + const Texture& scratch, Texture& destination, std::span destination_layers); + [[nodiscard]] std::expected copy_srgb( + const Texture& scratch, Texture& destination, std::span destination_layers); + + std::weak_ptr m_scratch; + std::weak_ptr m_destination; + Settings m_settings; + Operation m_operation = Operation::Copy; + std::optional m_effective_readback_mode; + std::optional m_initialisation_error; + + unsigned m_width = 0; + unsigned m_height = 0; + unsigned m_scratch_layers = 0; + unsigned m_destination_layers = 0; + unsigned m_mip_levels = 0; + GLsizei m_atlas_width = 0; + GLsizei m_atlas_height = 0; + GLuint m_encoded_texture = 0; + GLuint m_encoded_buffer = 0; + GLuint m_encoding_framebuffer = 0; + + std::unique_ptr m_program; + std::unique_ptr m_copy_framebuffer; + std::unique_ptr m_screen_quad; +}; + +} // namespace gl_engine diff --git a/gl_engine/shaders/texture_compress.vert b/gl_engine/shaders/texture_compress.frag similarity index 96% rename from gl_engine/shaders/texture_compress.vert rename to gl_engine/shaders/texture_compress.frag index 5d589503..c811d8be 100644 --- a/gl_engine/shaders/texture_compress.vert +++ b/gl_engine/shaders/texture_compress.frag @@ -1,3 +1,4 @@ +// GPU texture block encoder fragment shader. uniform highp sampler2DArray source_texture; uniform highp int texture_width; uniform highp int texture_height; @@ -234,7 +235,7 @@ FastEtc1Evaluation evaluate_fast_split(highp uvec3 pixels[16], highp ivec3 reconstructed = clamp(base + ivec3(modifier(table, int(selected))), ivec3(0), ivec3(255)); highp ivec3 delta = ivec3(pixel) - reconstructed; total_error += uint(delta.x * delta.x + delta.y * delta.y + delta.z * delta.z); -#if defined(ALP_COMPRESS_ETC1_REFINE_RESIDUAL) || defined(ALP_COMPRESS_ETC1_REFINE_SHARED_RESIDUAL) +#ifdef ALP_COMPRESS_ETC1_REFINE_RESIDUAL if (second_subblock) second_residual += delta; else @@ -323,7 +324,7 @@ highp uvec2 encode_etc1_fast_split_fused(highp uvec3 pixels[16]) highp uvec2 vertical = pack_fast_split(vertical_evaluation, left, right, vertical_bases, false); highp uvec2 horizontal = pack_fast_split(horizontal_evaluation, top, bottom, horizontal_bases, true); -#if !defined(ALP_COMPRESS_ETC1_REFINE_RESIDUAL) && !defined(ALP_COMPRESS_ETC1_REFINE_SHARED_RESIDUAL) +#ifndef ALP_COMPRESS_ETC1_REFINE_RESIDUAL return horizontal_evaluation.error < vertical_evaluation.error ? horizontal : vertical; #else bool horizontal_wins = horizontal_evaluation.error < vertical_evaluation.error; @@ -339,18 +340,10 @@ highp uvec2 encode_etc1_fast_split_fused(highp uvec3 pixels[16]) best_evaluation = horizontal_evaluation; best_block = horizontal; } -#ifdef ALP_COMPRESS_ETC1_REFINE_SHARED_RESIDUAL - highp ivec3 combined_residual = best_evaluation.first_residual + best_evaluation.second_residual; - FastEtc1Subblock candidate_first - = refit_fast_subblock(first, best_bases.first_decoded, combined_residual, 16); - FastEtc1Subblock candidate_second - = refit_fast_subblock(second, best_bases.second_decoded, combined_residual, 16); -#else FastEtc1Subblock candidate_first = refit_fast_subblock(first, best_bases.first_decoded, best_evaluation.first_residual, 8); FastEtc1Subblock candidate_second = refit_fast_subblock(second, best_bases.second_decoded, best_evaluation.second_residual, 8); -#endif FastEtc1Bases candidate_bases = fast_split_bases(candidate_first, candidate_second); FastEtc1Evaluation candidate_evaluation = evaluate_fast_split(pixels, candidate_first, candidate_second, candidate_bases, horizontal_wins); diff --git a/gl_engine/shaders/texture_compress_raster.vert b/gl_engine/shaders/texture_compress_raster.vert deleted file mode 100644 index 5d332762..00000000 --- a/gl_engine/shaders/texture_compress_raster.vert +++ /dev/null @@ -1,5 +0,0 @@ -void main() -{ - highp vec2 position = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2)); - gl_Position = vec4(position * 2.0 - 1.0, 0.0, 1.0); -} diff --git a/gl_engine/shaders/texture_copy.frag b/gl_engine/shaders/texture_copy.frag new file mode 100644 index 00000000..38fa9d95 --- /dev/null +++ b/gl_engine/shaders/texture_copy.frag @@ -0,0 +1,9 @@ +uniform highp sampler2DArray source_texture; +uniform highp int source_layer; +uniform highp int source_level; +layout(location = 0) out highp vec4 out_color; + +void main() +{ + out_color = texelFetch(source_texture, ivec3(ivec2(gl_FragCoord.xy), source_layer), source_level); +} diff --git a/unittests/gl_engine/CMakeLists.txt b/unittests/gl_engine/CMakeLists.txt index 0899a1ca..406add69 100644 --- a/unittests/gl_engine/CMakeLists.txt +++ b/unittests/gl_engine/CMakeLists.txt @@ -24,6 +24,7 @@ alp_add_unittest(unittests_gl_engine framebuffer.cpp uniformbuffer.cpp texture.cpp + texture_compressor.cpp ) target_sources(unittests_gl_engine @@ -32,4 +33,3 @@ target_sources(unittests_gl_engine ) target_link_libraries(unittests_gl_engine PUBLIC gl_engine) - diff --git a/unittests/gl_engine/texture.cpp b/unittests/gl_engine/texture.cpp index 900e193d..ec076d87 100644 --- a/unittests/gl_engine/texture.cpp +++ b/unittests/gl_engine/texture.cpp @@ -280,35 +280,6 @@ QImage create_test_rgba_qimage(unsigned width, unsigned height) } radix::Raster create_test_rgba_raster(unsigned width, unsigned height) { return nucleus::tile::conversion::to_rgba8raster(create_test_rgba_qimage(width, height)); } -double srgb_to_linear(uint8_t value) -{ - const auto normalised = double(value) / 255.0; - if (normalised <= 0.04045) - return normalised / 12.92; - return std::pow((normalised + 0.055) / 1.055, 2.4); -} - -double linear_psnr(const QImage& reconstructed, const radix::Raster& source) -{ - double squared_error = 0.0; - for (int y = 0; y < reconstructed.height(); ++y) { - for (int x = 0; x < reconstructed.width(); ++x) { - const auto actual = reconstructed.pixel(x, y); - const auto expected = source.pixel({ x, y }); - const std::array actual_channels { qRed(actual) / 255.0, qGreen(actual) / 255.0, qBlue(actual) / 255.0 }; - const std::array expected_channels { - srgb_to_linear(expected.x), srgb_to_linear(expected.y), srgb_to_linear(expected.z) - }; - for (size_t channel = 0; channel < actual_channels.size(); ++channel) { - const auto difference = actual_channels[channel] - expected_channels[channel]; - squared_error += difference * difference; - } - } - } - const auto mse = squared_error / double(reconstructed.width() * reconstructed.height() * 3); - return mse == 0.0 ? std::numeric_limits::infinity() : 10.0 * std::log10(1.0 / mse); -} - } // namespace TEST_CASE("gl texture") @@ -688,69 +659,3 @@ TEST_CASE("gl texture") } } } - -TEST_CASE("gl texture GPU compression quality") -{ - constexpr unsigned resolution = 64; - auto detailed = create_test_rgba_raster(resolution, resolution); - auto constant = radix::Raster(glm::uvec2(resolution), glm::u8vec4(42, 142, 242, 255)); - std::vector> sources; - sources.push_back(detailed); - sources.push_back(constant); - - gl_engine::Texture destination(gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); - destination.setParams(gl_engine::Texture::Filter::MipMapLinear, gl_engine::Texture::Filter::Nearest); - destination.allocate_array(resolution, resolution, unsigned(sources.size())); - - auto* f = QOpenGLContext::currentContext()->extraFunctions(); - while (f->glGetError() != GL_NO_ERROR) { } - gl_engine::TextureCompressor compressor(resolution, resolution, unsigned(sources.size())); - const std::array destination_layers { 0, 1 }; - const auto result = compressor.compress(sources, - destination, - destination_layers, - { .algorithm = gl_engine::Texture::compression_algorithm(), .effort = 4, .generate_mipmaps = true }); - CHECK(f->glGetError() == GL_NO_ERROR); - size_t expected_size = 0; - for (unsigned level = 0; level < gl_engine::TextureCompressor::mip_level_count(resolution, resolution); ++level) { - expected_size += gl_engine::TextureCompressor::compressed_level_size( - std::max(1u, resolution >> level), std::max(1u, resolution >> level)); - } - CHECK(result.encoded_bytes == expected_size * sources.size()); - CHECK(result.mip_levels == 7); - - Framebuffer framebuffer(Framebuffer::DepthFormat::None, { Framebuffer::ColourFormat::RGBA8 }, { resolution, resolution }); - framebuffer.bind(); - ShaderProgram shader = create_debug_shader(R"( - uniform lowp sampler2DArray texture_sampler; - uniform highp int texture_layer; - uniform highp int mip_level; - in highp vec2 texcoords; - out lowp vec4 out_color; - void main() { - out_color = textureLod(texture_sampler, vec3(texcoords.x, 1.0 - texcoords.y, float(texture_layer)), float(mip_level)); - } - )"); - shader.bind(); - destination.bind(0); - shader.set_uniform("texture_sampler", 0); - shader.set_uniform("mip_level", 0); - for (int layer = 0; layer < int(sources.size()); ++layer) { - shader.set_uniform("texture_layer", layer); - gl_engine::helpers::create_screen_quad_geometry().draw(); - const auto reconstructed = framebuffer.read_colour_attachment(0); - const auto psnr = linear_psnr(reconstructed, sources[size_t(layer)]); - CAPTURE(layer, psnr); - CHECK(psnr > 12.0); - } - shader.set_uniform("texture_layer", 1); - for (int level = 1; level < int(result.mip_levels); ++level) { - shader.set_uniform("mip_level", level); - gl_engine::helpers::create_screen_quad_geometry().draw(); - const auto reconstructed = framebuffer.read_colour_attachment(0); - const auto psnr = linear_psnr(reconstructed, constant); - CAPTURE(level, psnr); - CHECK(psnr > 20.0); - } - Framebuffer::unbind(); -} diff --git a/unittests/gl_engine/texture_compressor.cpp b/unittests/gl_engine/texture_compressor.cpp new file mode 100644 index 00000000..d4b84315 --- /dev/null +++ b/unittests/gl_engine/texture_compressor.cpp @@ -0,0 +1,275 @@ +/***************************************************************************** + * AlpineMaps.org + * Copyright (C) 2026 Adam Celarek + * SPDX-License-Identifier: GPL-3.0-or-later + *****************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace { +using Raster = radix::Raster; +using Compressor = gl_engine::TextureCompressor; + +Raster test_raster(unsigned resolution) +{ + Raster result { glm::uvec2(resolution) }; + for (unsigned y = 0; y < resolution; ++y) { + for (unsigned x = 0; x < resolution; ++x) { + result.pixel({ x, y }) = glm::u8vec4( + uint8_t((x * 17 + y * 3) & 255), + uint8_t((x * 5 + y * 11) & 255), + uint8_t((x * 7 + y * 13) & 255), + 255); + } + } + return result; +} + +std::shared_ptr rgba_scratch(std::span sources, unsigned mip_levels) +{ + const auto width = unsigned(sources.front().width()); + const auto height = unsigned(sources.front().height()); + auto scratch = std::make_shared( + gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::RGBA8); + scratch->setParams(gl_engine::Texture::Filter::Nearest, gl_engine::Texture::Filter::Nearest); + scratch->allocate_array(width, height, unsigned(sources.size()), mip_levels); + for (size_t layer = 0; layer < sources.size(); ++layer) + scratch->upload(sources[layer], unsigned(layer)); + if (mip_levels > 1) + scratch->generate_mipmaps(); + return scratch; +} + +std::shared_ptr destination( + gl_engine::Texture::Format format, unsigned resolution, unsigned layers, unsigned mip_levels) +{ + auto result = std::make_shared(gl_engine::Texture::Target::_2dArray, format); + result->setParams(mip_levels > 1 ? gl_engine::Texture::Filter::MipMapLinear : gl_engine::Texture::Filter::Linear, + gl_engine::Texture::Filter::Linear); + result->allocate_array(resolution, resolution, layers, mip_levels); + return result; +} + +QImage reconstruct_srgb(gl_engine::Texture& texture, unsigned resolution, unsigned layer, unsigned level = 0) +{ + const auto level_resolution = std::max(1u, resolution >> level); + gl_engine::Framebuffer framebuffer(gl_engine::Framebuffer::DepthFormat::None, + { gl_engine::Framebuffer::ColourFormat::RGBA8 }, + { level_resolution, level_resolution }); + framebuffer.bind(); + gl_engine::ShaderProgram shader(R"( + out highp vec2 texcoords; + void main() { + highp vec2 vertices[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0)); + gl_Position = vec4(vertices[gl_VertexID], 0.0, 1.0); + texcoords = 0.5 * gl_Position.xy + vec2(0.5); + })", + R"( + uniform lowp sampler2DArray texture_sampler; + uniform highp int texture_layer; + uniform highp int mip_level; + in highp vec2 texcoords; + out lowp vec4 out_color; + highp vec3 linear_to_srgb(highp vec3 linear) { + return mix(12.92 * linear, + 1.055 * pow(linear, vec3(1.0 / 2.4)) - 0.055, + step(vec3(0.0031308), linear)); + } + void main() { + lowp vec4 colour = textureLod(texture_sampler, + vec3(texcoords.x, 1.0 - texcoords.y, float(texture_layer)), float(mip_level)); + out_color = vec4(linear_to_srgb(colour.rgb), colour.a); + })", + gl_engine::ShaderCodeSource::PLAINTEXT); + shader.bind(); + texture.bind(0); + shader.set_uniform("texture_sampler", 0); + shader.set_uniform("texture_layer", int(layer)); + shader.set_uniform("mip_level", int(level)); + gl_engine::helpers::create_screen_quad_geometry().draw(); + const auto result = framebuffer.read_colour_attachment(0); + gl_engine::Framebuffer::unbind(); + return result; +} + +double psnr(const QImage& image, const Raster& source) +{ + double squared_error = 0.0; + for (int y = 0; y < image.height(); ++y) { + for (int x = 0; x < image.width(); ++x) { + const auto actual = image.pixel(x, y); + const auto expected = source.pixel({ x, y }); + const std::array delta { + qRed(actual) - int(expected.x), + qGreen(actual) - int(expected.y), + qBlue(actual) - int(expected.z), + }; + for (const auto value : delta) + squared_error += double(value * value); + } + } + const auto mse = squared_error / double(image.width() * image.height() * 3); + return mse == 0.0 ? std::numeric_limits::infinity() : 10.0 * std::log10(255.0 * 255.0 / mse); +} +} + +TEST_CASE("GPU texture compression processes external scratch layers and mipmaps") +{ + constexpr unsigned resolution = 64; + const auto mip_levels = Compressor::mip_level_count(resolution, resolution); + const std::vector sources { + test_raster(resolution), + Raster(glm::uvec2(resolution), glm::u8vec4(42, 142, 242, 255)), + }; + auto scratch = rgba_scratch(sources, mip_levels); + auto output = destination(gl_engine::Texture::Format::CompressedRGBA8, resolution, 3, mip_levels); + Compressor compressor(scratch, output, { .search_effort = 4 }); + const std::array layers { 2, 0 }; + + auto* f = QOpenGLContext::currentContext()->extraFunctions(); + while (f->glGetError() != GL_NO_ERROR) { } + const auto result = compressor.compress(layers); + REQUIRE(result); + CHECK(f->glGetError() == GL_NO_ERROR); + CHECK(result->layers_written == 2); + CHECK(result->mip_levels_written == mip_levels); + size_t expected_size = 0; + for (unsigned level = 0; level < mip_levels; ++level) { + expected_size += Compressor::compressed_level_size( + std::max(1u, resolution >> level), std::max(1u, resolution >> level)); + } + CHECK(result->bytes_written == expected_size * sources.size()); + CHECK(psnr(reconstruct_srgb(*output, resolution, 2), sources[0]) > 10.0); + CHECK(psnr(reconstruct_srgb(*output, resolution, 0), sources[1]) > 20.0); + for (unsigned level = 1; level < mip_levels; ++level) + CHECK(psnr(reconstruct_srgb(*output, resolution, 0, level), + Raster(glm::uvec2(std::max(1u, resolution >> level)), glm::u8vec4(42, 142, 242, 255))) + > 20.0); +} + +TEST_CASE("GPU texture compression supports automatic and explicit readback modes") +{ + constexpr unsigned resolution = 16; + const std::vector sources { test_raster(resolution) }; + auto scratch = rgba_scratch(sources, 1); + const std::array layers { 0 }; + + auto auto_output = destination(gl_engine::Texture::Format::CompressedRGBA8, resolution, 1, 1); + Compressor automatic(scratch, auto_output, { .readback_mode = Compressor::ReadbackMode::Auto }); + REQUIRE(automatic.compress(layers)); + + auto paired_output = destination(gl_engine::Texture::Format::CompressedRGBA8, resolution, 1, 1); + Compressor paired(scratch, paired_output, { .readback_mode = Compressor::ReadbackMode::RGBA32UI }); + REQUIRE(paired.compress(layers)); + CHECK(paired.effective_readback_mode() == Compressor::ReadbackMode::RGBA32UI); + CHECK(reconstruct_srgb(*auto_output, resolution, 0) == reconstruct_srgb(*paired_output, resolution, 0)); + + auto direct_output = destination(gl_engine::Texture::Format::CompressedRGBA8, resolution, 1, 1); + Compressor direct(scratch, direct_output, { .readback_mode = Compressor::ReadbackMode::RG32UI }); + const auto direct_result = direct.compress(layers); + if (direct_result) { + CHECK(direct.effective_readback_mode() == Compressor::ReadbackMode::RG32UI); + CHECK(reconstruct_srgb(*auto_output, resolution, 0) == reconstruct_srgb(*direct_output, resolution, 0)); + } else { + CHECK(direct_result.error().find("RG32UI") != std::string::npos); + } +} + +TEST_CASE("GPU texture compressor exposes every platform algorithm") +{ + constexpr unsigned resolution = 8; + const std::vector sources { test_raster(resolution) }; + auto scratch = rgba_scratch(sources, 1); + const std::array layers { 0 }; + std::vector settings; + if (gl_engine::Texture::compression_algorithm() == nucleus::utils::ColourTexture::Format::DXT1) { + settings.push_back({ .dxt1_algorithm = Compressor::Dxt1Algorithm::SlowSearch }); + settings.push_back({ .dxt1_algorithm = Compressor::Dxt1Algorithm::DebugChecksum }); + } else { + settings.push_back({ .etc_algorithm = Compressor::EtcAlgorithm::Fastest }); + settings.push_back({ .etc_algorithm = Compressor::EtcAlgorithm::Fast }); + settings.push_back({ .etc_algorithm = Compressor::EtcAlgorithm::SlowSearch }); + settings.push_back({ .etc_algorithm = Compressor::EtcAlgorithm::DebugChecksum }); + } + + for (const auto& setting : settings) { + auto output = destination(gl_engine::Texture::Format::CompressedRGBA8, resolution, 1, 1); + Compressor compressor(scratch, output, setting); + CHECK(compressor.compress(layers)); + } +} + +TEST_CASE("GPU texture compressor copies sRGB bytes through an RGBA8 framebuffer") +{ + constexpr unsigned resolution = 16; + const auto mip_levels = Compressor::mip_level_count(resolution, resolution); + const std::vector sources { + test_raster(resolution), + Raster(glm::uvec2(resolution), glm::u8vec4(23, 101, 207, 255)), + }; + auto scratch = rgba_scratch(sources, mip_levels); + auto output = destination(gl_engine::Texture::Format::SRGBA8, resolution, 3, mip_levels); + Compressor compressor(scratch, output); + const std::array layers { 2, 0 }; + const auto result = compressor.compress(layers); + REQUIRE(result); + CHECK_FALSE(compressor.effective_readback_mode()); + size_t expected_size = 0; + for (unsigned level = 0; level < mip_levels; ++level) + expected_size += size_t(std::max(1u, resolution >> level)) * std::max(1u, resolution >> level) * 4 * sources.size(); + CHECK(result->bytes_written == expected_size); + CHECK(psnr(reconstruct_srgb(*output, resolution, 2), sources[0]) > 45.0); + CHECK(psnr(reconstruct_srgb(*output, resolution, 0), sources[1]) > 45.0); +} + +TEST_CASE("GPU texture compressor accepts RGB565 scratch storage") +{ + constexpr unsigned resolution = 4; + constexpr uint16_t packed = uint16_t((21u << 11u) | (37u << 5u) | 9u); + auto scratch = std::make_shared( + gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::RGB565); + scratch->setParams(gl_engine::Texture::Filter::Nearest, gl_engine::Texture::Filter::Nearest); + scratch->allocate_array(resolution, resolution, 1, 1); + scratch->upload(radix::Raster(glm::uvec2(resolution), packed), 0); + auto output = destination(gl_engine::Texture::Format::SRGBA8, resolution, 1, 1); + Compressor compressor(scratch, output); + const std::array layers { 0 }; + REQUIRE(compressor.compress(layers)); + + const glm::u8vec4 expected( + uint8_t(21u * 255u / 31u), + uint8_t(37u * 255u / 63u), + uint8_t(9u * 255u / 31u), + 255); + CHECK(psnr(reconstruct_srgb(*output, resolution, 0), Raster(glm::uvec2(resolution), expected)) > 40.0); +} + +TEST_CASE("GPU texture compressor reports expired textures") +{ + constexpr unsigned resolution = 4; + const std::vector sources { test_raster(resolution) }; + auto scratch = rgba_scratch(sources, 1); + auto output = destination(gl_engine::Texture::Format::SRGBA8, resolution, 1, 1); + Compressor compressor(scratch, output); + scratch.reset(); + output.reset(); + const std::array layers { 0 }; + const auto result = compressor.compress(layers); + REQUIRE_FALSE(result); + CHECK(result.error().find("expired") != std::string::npos); +} diff --git a/unittests/texture_compression_benchmark/main.cpp b/unittests/texture_compression_benchmark/main.cpp index fdda60ad..8c3c3969 100644 --- a/unittests/texture_compression_benchmark/main.cpp +++ b/unittests/texture_compression_benchmark/main.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include #include @@ -46,7 +47,6 @@ namespace { using Clock = std::chrono::steady_clock; using Raster = radix::Raster; using Format = nucleus::utils::ColourTexture::Format; -using Encoder = gl_engine::TextureCompressor::Encoder; constexpr unsigned resolution = 512; constexpr unsigned max_batch_size = 4; @@ -76,6 +76,7 @@ struct Algorithm { Operation operation = Operation::Compression; gl_engine::TextureCompressor::Settings settings; bool checksum = false; + std::unique_ptr compressor; std::vector samples; std::optional quality; }; @@ -298,6 +299,8 @@ const char* readback_mode_name(gl_engine::TextureCompressor::ReadbackMode mode) { using ReadbackMode = gl_engine::TextureCompressor::ReadbackMode; switch (mode) { + case ReadbackMode::Auto: + return "automatic"; case ReadbackMode::RG32UI: return "direct RG32UI"; case ReadbackMode::RGBA32UI: @@ -308,29 +311,27 @@ const char* readback_mode_name(gl_engine::TextureCompressor::ReadbackMode mode) std::vector supported_algorithms(Format format) { - const auto settings = [format](Encoder encoder) { - return gl_engine::TextureCompressor::Settings { - .algorithm = format, - .effort = 0, - .encoder = encoder, - .generate_mipmaps = true, - }; + using Compressor = gl_engine::TextureCompressor; + Compressor::Settings checksum_settings { + .dxt1_algorithm = Compressor::Dxt1Algorithm::DebugChecksum, + .etc_algorithm = Compressor::EtcAlgorithm::DebugChecksum, }; std::vector result; - result.push_back({ "sampling only", Operation::SamplingOnly, settings(Encoder::Checksum) }); - result.push_back({ "checksum", Operation::Compression, settings(Encoder::Checksum), true }); + result.push_back({ "sampling only", Operation::SamplingOnly, checksum_settings }); + result.push_back({ "debug checksum", Operation::Compression, checksum_settings, true }); if (format == Format::DXT1) { - result.push_back({ "DXT1", Operation::Compression, settings(Encoder::Dxt1) }); + result.push_back({ "DXT1 slow search", Operation::Compression, {} }); } else if (format == Format::ETC1) { - result.push_back( - { "ETC1 fused exact", Operation::Compression, settings(Encoder::FastSplitFusedExact) }); - result.push_back({ "ETC1 fused exact residual fit", + result.push_back({ "ETC fastest", Operation::Compression, - settings(Encoder::FastSplitFusedExactResidual) }); - result.push_back({ "ETC1 fused exact shared residual", + { .etc_algorithm = Compressor::EtcAlgorithm::Fastest } }); + result.push_back({ "ETC fast", Operation::Compression, - settings(Encoder::FastSplitFusedExactSharedResidual) }); + { .etc_algorithm = Compressor::EtcAlgorithm::Fast } }); + result.push_back({ "ETC slow search", + Operation::Compression, + { .etc_algorithm = Compressor::EtcAlgorithm::SlowSearch } }); } return result; } @@ -346,10 +347,6 @@ class BenchmarkWindow final : public QOpenGLWindow { protected: void initializeGL() override { - if (!gl_engine::TextureCompressor::is_supported()) { - fail(QStringLiteral("GPU texture compression is not supported by this context.")); - return; - } download_data(); } @@ -381,8 +378,8 @@ class BenchmarkWindow final : public QOpenGLWindow { unsigned batch_size = 0; Format format = Format::Uncompressed_RGBA; std::vector algorithms; - std::unique_ptr destination; - std::unique_ptr compressor; + std::shared_ptr scratch; + std::shared_ptr destination; std::unique_ptr framebuffer; std::unique_ptr sampling_shader; gl_engine::helpers::ScreenQuadGeometry sampling_geometry; @@ -523,14 +520,28 @@ class BenchmarkWindow final : public QOpenGLWindow { { switch (state.startup_step++) { case 0: - state.destination = std::make_unique( + state.scratch = std::make_shared( + gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::RGBA8); + state.scratch->setParams(gl_engine::Texture::Filter::Nearest, gl_engine::Texture::Filter::Linear); + state.scratch->allocate_array(resolution, + resolution, + state.batch_size, + gl_engine::TextureCompressor::mip_level_count(resolution, resolution)); + state.destination = std::make_shared( gl_engine::Texture::Target::_2dArray, gl_engine::Texture::Format::CompressedRGBA8); state.destination->setParams(gl_engine::Texture::Filter::MipMapLinear, gl_engine::Texture::Filter::Linear); - state.destination->allocate_array(resolution, resolution, state.batch_size); + state.destination->allocate_array(resolution, + resolution, + state.batch_size, + gl_engine::TextureCompressor::mip_level_count(resolution, resolution)); return true; case 1: - state.compressor - = std::make_unique(resolution, resolution, state.batch_size); + for (auto& algorithm : state.algorithms) { + if (algorithm.operation == Operation::Compression) { + algorithm.compressor = std::make_unique( + state.scratch, state.destination, algorithm.settings); + } + } return true; case 2: state.framebuffer = std::make_unique( @@ -570,7 +581,8 @@ class BenchmarkWindow final : public QOpenGLWindow { .arg(QString::fromStdString(gl_string(GL_RENDERER))) .arg(QString::fromStdString(gl_string(GL_VERSION))) .arg(QString::fromLatin1(format_name(state.format))) - .arg(QString::fromLatin1(readback_mode_name(state.compressor->readback_mode()))) + .arg(QString::fromLatin1(readback_mode_name( + *state.algorithms[1].compressor->effective_readback_mode()))) .arg(QString::number(random_seed, 16)) .arg(state.batch_size) .arg(repetitions) @@ -579,10 +591,15 @@ class BenchmarkWindow final : public QOpenGLWindow { return true; default: std::vector initial_sources(m_sources.begin(), m_sources.begin() + state.batch_size); - static_cast(state.compressor->compress(initial_sources, - *state.destination, - std::span(state.destination_layers).first(state.batch_size), - state.algorithms[1].settings)); + for (size_t layer = 0; layer < initial_sources.size(); ++layer) + state.scratch->upload(initial_sources[layer], unsigned(layer)); + state.scratch->generate_mipmaps(); + const auto result = state.algorithms[1].compressor->compress( + std::span(state.destination_layers).first(state.batch_size)); + if (!result) { + fail(QString::fromStdString(result.error())); + return false; + } state.destination_initialised = true; return true; } @@ -599,10 +616,12 @@ class BenchmarkWindow final : public QOpenGLWindow { const auto start = Clock::now(); if (algorithm.operation == Operation::Compression) { - static_cast(state.compressor->compress(selected_sources, - *state.destination, - std::span(state.destination_layers).first(state.batch_size), - algorithm.settings)); + for (size_t layer = 0; layer < selected_sources.size(); ++layer) + state.scratch->upload(selected_sources[layer], unsigned(layer)); + state.scratch->generate_mipmaps(); + const auto result = algorithm.compressor->compress( + std::span(state.destination_layers).first(state.batch_size)); + Q_ASSERT(result); } auto* functions = QOpenGLContext::currentContext()->extraFunctions(); @@ -684,10 +703,12 @@ class BenchmarkWindow final : public QOpenGLWindow { selected_sources.reserve(active_batch_size); for (size_t layer = 0; layer < active_batch_size; ++layer) selected_sources.push_back(m_sources[source_offset + layer]); - static_cast(state.compressor->compress(selected_sources, - *state.destination, - std::span(state.destination_layers).first(active_batch_size), - algorithm.settings)); + for (size_t layer = 0; layer < selected_sources.size(); ++layer) + state.scratch->upload(selected_sources[layer], unsigned(layer)); + state.scratch->generate_mipmaps(); + const auto result = algorithm.compressor->compress( + std::span(state.destination_layers).first(active_batch_size)); + Q_ASSERT(result); for (unsigned layer = 0; layer < active_batch_size; ++layer) { accumulate_quality(accumulator, reconstruct(state, layer), From 006b68cc5ea59f9820134eab9371c94aa3d11174 Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Sat, 29 Aug 2026 15:57:45 +0200 Subject: [PATCH 34/38] Fix KTX dependency for WebGPU engine --- nucleus/CMakeLists.txt | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/nucleus/CMakeLists.txt b/nucleus/CMakeLists.txt index 6755210b..04500c8a 100644 --- a/nucleus/CMakeLists.txt +++ b/nucleus/CMakeLists.txt @@ -29,10 +29,6 @@ if(ALP_ENABLE_LABELS) endif() alp_add_git_repository(goofy_tc URL https://github.com/AlpineMapsOrgDependencies/Goofy_slim.git COMMITISH 13b228784960a6227bb6ca704ff34161bbac1b91 DO_NOT_ADD_SUBPROJECT) alp_add_git_repository(cdt URL https://github.com/artem-ogre/CDT.git COMMITISH 46f1ce1f495a97617d90e8c833d0d29406335fdf DO_NOT_ADD_SUBPROJECT) -if (ALP_BUILD_WEBGPU_APP) - include(${CMAKE_SOURCE_DIR}/cmake/SetupKTX.cmake) - alp_setup_ktx(952d74f1d53452e4e976a2b7698ff7af6c13a9ed) -endif() add_library(zppbits INTERFACE) target_include_directories(zppbits SYSTEM INTERFACE ${zppbits_SOURCE_DIR}) @@ -127,8 +123,11 @@ qt_add_library(nucleus STATIC camera/gesture.h ) -if (ALP_BUILD_WEBGPU_APP) +if (ALP_BUILD_WEBGPU_ENGINE) + include(${CMAKE_SOURCE_DIR}/cmake/SetupKTX.cmake) + alp_setup_ktx(952d74f1d53452e4e976a2b7698ff7af6c13a9ed) target_sources(nucleus PRIVATE tile/Texture3DScheduler.h tile/Texture3DScheduler.cpp) + target_link_libraries(nucleus PUBLIC KTX::ktx) endif() if (ALP_ENABLE_AVALANCHE_WARNING_LAYER) @@ -183,9 +182,6 @@ endif() target_include_directories(nucleus PUBLIC ${CMAKE_SOURCE_DIR}) # Please keep Qt::Gui outside the nucleus. If you need it optional via a cmake based switch target_link_libraries(nucleus PUBLIC radix Qt::Core Qt::Network zppbits nucleus_version stb_slim goofy_tc cdt) -if (ALP_BUILD_WEBGPU_APP) - target_link_libraries(nucleus PUBLIC KTX::ktx) -endif() qt_add_resources(nucleus "height_data" PREFIX "/map" From a224600132ee33c9f8fe3a2c9321811358da1386 Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:40:53 +0200 Subject: [PATCH 35/38] Restore default WebGPU app build --- CMakeLists.txt | 6 +++++- nucleus/CMakeLists.txt | 12 ++++-------- nucleus/utils/ColourTexture.cpp | 9 --------- nucleus/utils/ColourTexture.h | 1 - unittests/nucleus/CMakeLists.txt | 1 - 5 files changed, 9 insertions(+), 20 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7d55d803..25bbb29d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,10 +26,14 @@ option(ALP_BUILD_GL_ENGINE "include the gl engine in the buildsystem" OFF) option(ALP_BUILD_PLAIN_RENDERER "include the plain renderer in the buildsystem" ON) option(ALP_BUILD_ALPINEAPP "include the qml app in the buildsystem" ON) option(ALP_BUILD_TEXTURE_COMPRESSION_PREVIEW "include the texture compression preview application" OFF) +set(ALP_WEBGPU_DEFAULT ON) +if (APPLE OR ANDROID) + set(ALP_WEBGPU_DEFAULT OFF) +endif() option(ALP_BUILD_WEBGPU_BASE "include the webgpu base library in the buildsystem" OFF) option(ALP_BUILD_WEBGPU_ENGINE "include the webgpu engine in the buildsystem" OFF) option(ALP_BUILD_WEBGPU_COMPUTE "include the webgpu compute library in the buildsystem" OFF) -option(ALP_BUILD_WEBGPU_APP "include the webgpu app in the buildsystem" OFF) +option(ALP_BUILD_WEBGPU_APP "include the webgpu app in the buildsystem" ${ALP_WEBGPU_DEFAULT}) option(ALP_WEBGPU_APP_ENABLE_COMPUTE "Build the webgpu_compute graph into the app" ON) diff --git a/nucleus/CMakeLists.txt b/nucleus/CMakeLists.txt index 04500c8a..77581fff 100644 --- a/nucleus/CMakeLists.txt +++ b/nucleus/CMakeLists.txt @@ -29,6 +29,8 @@ if(ALP_ENABLE_LABELS) endif() alp_add_git_repository(goofy_tc URL https://github.com/AlpineMapsOrgDependencies/Goofy_slim.git COMMITISH 13b228784960a6227bb6ca704ff34161bbac1b91 DO_NOT_ADD_SUBPROJECT) alp_add_git_repository(cdt URL https://github.com/artem-ogre/CDT.git COMMITISH 46f1ce1f495a97617d90e8c833d0d29406335fdf DO_NOT_ADD_SUBPROJECT) +include(${CMAKE_SOURCE_DIR}/cmake/SetupKTX.cmake) +alp_setup_ktx(952d74f1d53452e4e976a2b7698ff7af6c13a9ed) add_library(zppbits INTERFACE) target_include_directories(zppbits SYSTEM INTERFACE ${zppbits_SOURCE_DIR}) @@ -113,6 +115,7 @@ qt_add_library(nucleus STATIC tile/setup.h tile/GpuArrayHelper.h tile/GpuArrayHelper.cpp tile/TextureScheduler.h tile/TextureScheduler.cpp + tile/Texture3DScheduler.h tile/Texture3DScheduler.cpp tile/GeometryScheduler.h tile/GeometryScheduler.cpp utils/easing.h utils/error.h @@ -123,13 +126,6 @@ qt_add_library(nucleus STATIC camera/gesture.h ) -if (ALP_BUILD_WEBGPU_ENGINE) - include(${CMAKE_SOURCE_DIR}/cmake/SetupKTX.cmake) - alp_setup_ktx(952d74f1d53452e4e976a2b7698ff7af6c13a9ed) - target_sources(nucleus PRIVATE tile/Texture3DScheduler.h tile/Texture3DScheduler.cpp) - target_link_libraries(nucleus PUBLIC KTX::ktx) -endif() - if (ALP_ENABLE_AVALANCHE_WARNING_LAYER) target_sources(nucleus PRIVATE @@ -181,7 +177,7 @@ endif() target_include_directories(nucleus PUBLIC ${CMAKE_SOURCE_DIR}) # Please keep Qt::Gui outside the nucleus. If you need it optional via a cmake based switch -target_link_libraries(nucleus PUBLIC radix Qt::Core Qt::Network zppbits nucleus_version stb_slim goofy_tc cdt) +target_link_libraries(nucleus PUBLIC radix Qt::Core Qt::Network zppbits nucleus_version stb_slim goofy_tc cdt KTX::ktx) qt_add_resources(nucleus "height_data" PREFIX "/map" diff --git a/nucleus/utils/ColourTexture.cpp b/nucleus/utils/ColourTexture.cpp index ce6384dc..74a3ca25 100644 --- a/nucleus/utils/ColourTexture.cpp +++ b/nucleus/utils/ColourTexture.cpp @@ -24,7 +24,6 @@ #include #include #include -#include #define GOOFYTC_IMPLEMENTATION #include @@ -160,14 +159,6 @@ nucleus::utils::ColourTexture::ColourTexture(const radix::Raster& i { } -nucleus::utils::ColourTexture::ColourTexture(std::vector data, unsigned width, unsigned height, Format format) - : m_data(std::move(data)) - , m_width(width) - , m_height(height) - , m_format(format) -{ -} - nucleus::utils::MipmappedColourTexture nucleus::utils::generate_mipmapped_colour_texture( const radix::Raster& texture, ColourTexture::Format format) { diff --git a/nucleus/utils/ColourTexture.h b/nucleus/utils/ColourTexture.h index 158d3748..1df28a6b 100644 --- a/nucleus/utils/ColourTexture.h +++ b/nucleus/utils/ColourTexture.h @@ -37,7 +37,6 @@ class ColourTexture { public: explicit ColourTexture(const radix::Raster& data, Format format); - ColourTexture(std::vector data, unsigned width, unsigned height, Format format); [[nodiscard]] const uint8_t* data() const { return m_data.data(); } [[nodiscard]] size_t n_bytes() const { return m_data.size(); } [[nodiscard]] unsigned width() const { return m_width; } diff --git a/unittests/nucleus/CMakeLists.txt b/unittests/nucleus/CMakeLists.txt index 9600a4b0..867b648f 100644 --- a/unittests/nucleus/CMakeLists.txt +++ b/unittests/nucleus/CMakeLists.txt @@ -90,7 +90,6 @@ if (ALP_ENABLE_AVALANCHE_WARNING_LAYER) data/eaws_7-67-45.mvt ) endif() - target_link_libraries(unittests_nucleus PUBLIC nucleus Catch2::Catch2 Qt::Test Qt::Gui) target_compile_definitions(unittests_nucleus PUBLIC "ALP_TEST_DATA_DIR=\":/test_data/\"") From 8d4d9cf44ddd44438763e39de1b73f5143de04d9 Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Sat, 29 Aug 2026 23:44:20 +0200 Subject: [PATCH 36/38] Clean up ShaderProgram upstream diff --- gl_engine/ShaderProgram.cpp | 38 ++++++++++++++++--------------------- gl_engine/ShaderProgram.h | 5 +---- 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/gl_engine/ShaderProgram.cpp b/gl_engine/ShaderProgram.cpp index 243768d0..d72939f9 100644 --- a/gl_engine/ShaderProgram.cpp +++ b/gl_engine/ShaderProgram.cpp @@ -177,10 +177,7 @@ QString ShaderProgram::read_file_content_local(const QString& name) { // =========== MEMBER DECLARATIONS ======================= -ShaderProgram::ShaderProgram(QString vertex_shader, - QString fragment_shader, - ShaderCodeSource code_source, - const std::vector& defines) +ShaderProgram::ShaderProgram(QString vertex_shader, QString fragment_shader, ShaderCodeSource code_source, const std::vector& defines) : m_vertex_shader(vertex_shader) , m_fragment_shader(fragment_shader) , m_code_source(code_source) @@ -249,10 +246,7 @@ void ShaderProgram::set_uniform(const std::string& name, int value) void ShaderProgram::set_uniform(const std::string& name, unsigned value) { - if (!m_cached_uniforms.contains(name)) - m_cached_uniforms[name] = m_q_shader_program->uniformLocation(name.c_str()); - - QOpenGLContext::currentContext()->extraFunctions()->glUniform1ui(m_cached_uniforms.at(name), value); + set_uniform_template(name, value); } void ShaderProgram::set_uniform(const std::string& name, float value) @@ -284,7 +278,8 @@ void ShaderProgram::set_uniform_array(const std::string& name, const std::vector if (!m_cached_uniforms.contains(name)) m_cached_uniforms[name] = m_q_shader_program->uniformLocation(name.c_str()); - QOpenGLContext::currentContext()->extraFunctions()->glUniform1iv(m_cached_uniforms.at(name), GLsizei(array.size()), array.data()); + const auto uniform_location = m_cached_uniforms.at(name); + m_q_shader_program->setUniformValueArray(uniform_location, array.data(), int(array.size())); } // Helper function because i get frustrated with the shader compile errors... @@ -334,22 +329,21 @@ void ShaderProgram::reload() outputMeaningfullErrors(program->log(), vertexCode, m_vertex_shader); } else if (!program->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentCode)) { outputMeaningfullErrors(program->log(), fragmentCode, m_fragment_shader); - } else { - if (!program->link()) { + } else if (!program->link()) { #ifdef _MSC_VER - // when using msvc in github ci qDebug/Critical don't print when an assert fails - // effectively, we don't see any error - std::cerr << "error linking shader " << m_vertex_shader.toStdString() << "and" << m_fragment_shader.toStdString() << std::endl; - fflush(stderr); - fflush(stdout); + // when using msvc in github ci qDebug/Critical don't print when an assert fails + // effectively, we don't see any error + std::cerr << "error linking shader " << m_vertex_shader.toStdString() << "and" << m_fragment_shader.toStdString() << std::endl; + fflush(stderr); + fflush(stdout); #else - qCritical() << "error linking shader " << m_vertex_shader.toStdString() << "and" << m_fragment_shader.toStdString(); + qCritical() << "error linking shader " << m_vertex_shader.toStdString() << "and" << m_fragment_shader.toStdString(); #endif - } else { - m_q_shader_program = std::move(program); - m_cached_attribs.clear(); - m_cached_uniforms.clear(); - } + } else { + // NO ERROR + m_q_shader_program = std::move(program); + m_cached_attribs.clear(); + m_cached_uniforms.clear(); } } diff --git a/gl_engine/ShaderProgram.h b/gl_engine/ShaderProgram.h index 5f812a78..556549d6 100644 --- a/gl_engine/ShaderProgram.h +++ b/gl_engine/ShaderProgram.h @@ -84,10 +84,7 @@ class ShaderProgram { static void preprocess_shader_content_inplace(QString& base); public: - ShaderProgram(QString vertex_shader, - QString fragment_shader, - ShaderCodeSource code_source = ShaderCodeSource::FILE, - const std::vector& defines = {}); + ShaderProgram(QString vertex_shader, QString fragment_shader, ShaderCodeSource code_source = ShaderCodeSource::FILE, const std::vector& defines = {}); int attribute_location(const std::string& name); void bind(); From 39e19f0b9a438a2b82a3ffced24144bc06602745 Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:06:54 +0200 Subject: [PATCH 37/38] Use Framebuffer for texture compression output --- gl_engine/Framebuffer.cpp | 24 ++++++++++ gl_engine/Framebuffer.h | 4 ++ gl_engine/TextureCompressor.cpp | 37 +++++++--------- gl_engine/TextureCompressor.h | 3 +- unittests/gl_engine/texture_compressor.cpp | 51 ++++++++++++++++++++++ 5 files changed, 95 insertions(+), 24 deletions(-) diff --git a/gl_engine/Framebuffer.cpp b/gl_engine/Framebuffer.cpp index 1dfd439e..ec5998d2 100644 --- a/gl_engine/Framebuffer.cpp +++ b/gl_engine/Framebuffer.cpp @@ -56,6 +56,10 @@ QOpenGLTexture::TextureFormat internal_format_qt(Framebuffer::ColourFormat f) // return QOpenGLTexture::TextureFormat::RGBA16F; case Framebuffer::ColourFormat::R32UI: return QOpenGLTexture::TextureFormat::R32U; + case Framebuffer::ColourFormat::RG32UI: + return QOpenGLTexture::TextureFormat::RG32U; + case Framebuffer::ColourFormat::RGBA32UI: + return QOpenGLTexture::TextureFormat::RGBA32U; case Framebuffer::ColourFormat::RGBA32F: return QOpenGLTexture::TextureFormat::RGBA32F; } @@ -84,6 +88,10 @@ GLenum format(Framebuffer::ColourFormat f) // return GL_RGBA; case Framebuffer::ColourFormat::R32UI: return GL_RED_INTEGER; + case Framebuffer::ColourFormat::RG32UI: + return GL_RG_INTEGER; + case Framebuffer::ColourFormat::RGBA32UI: + return GL_RGBA_INTEGER; case Framebuffer::ColourFormat::RGBA32F: return GL_RGBA; } @@ -131,6 +139,8 @@ GLenum type(Framebuffer::ColourFormat f) // case Framebuffer::ColourFormat::RGBA16F: // return GL_HALF_FLOAT; case Framebuffer::ColourFormat::R32UI: + case Framebuffer::ColourFormat::RG32UI: + case Framebuffer::ColourFormat::RGBA32UI: return GL_UNSIGNED_INT; } Q_ASSERT(false); @@ -254,6 +264,18 @@ void Framebuffer::bind() f->glBindFramebuffer(GL_FRAMEBUFFER, m_frame_buffer); } +void Framebuffer::bind_for_drawing() +{ + QOpenGLFunctions* f = QOpenGLContext::currentContext()->functions(); + f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_frame_buffer); +} + +void Framebuffer::bind_for_reading() +{ + QOpenGLFunctions* f = QOpenGLContext::currentContext()->functions(); + f->glBindFramebuffer(GL_READ_FRAMEBUFFER, m_frame_buffer); +} + void Framebuffer::bind_colour_texture(unsigned index, unsigned location) { Q_ASSERT(index < m_colour_textures.size()); @@ -314,6 +336,8 @@ T Framebuffer::read_colour_attachment_pixel(unsigned int index, const glm::dvec2 // case Framebuffer::ColourFormat::RGB16F: // case Framebuffer::ColourFormat::RGBA16F: case Framebuffer::ColourFormat::R32UI: // fails on linux firefox + case Framebuffer::ColourFormat::RG32UI: + case Framebuffer::ColourFormat::RGBA32UI: // unsupported or untested. // you really should add a unit test if you move something down to the supported section // as the support accross platforms (webassembly, android, ios?) is patchy diff --git a/gl_engine/Framebuffer.h b/gl_engine/Framebuffer.h index b63c755b..26c1c641 100644 --- a/gl_engine/Framebuffer.h +++ b/gl_engine/Framebuffer.h @@ -55,6 +55,8 @@ class Framebuffer // RGB16F, // NOT COLOR RENDERABLE ON OPENGLES // RGBA16F, // NOT COLOR RENDERABLE ON OPENGLES R32UI, + RG32UI, + RGBA32UI, // Float32, // NOT COLOR RENDERABLE ON OPENGLES RGBA32F, // NOT COLOR RENDERABLE ON OPENGLES (weirdly it works, maybe because of extension, that qt activates?) }; @@ -78,6 +80,8 @@ class Framebuffer ~Framebuffer(); void resize(const glm::uvec2& new_size); void bind(); + void bind_for_drawing(); + void bind_for_reading(); void bind_colour_texture(unsigned index = 0, unsigned location = 0); void bind_depth_texture(unsigned location = 0); diff --git a/gl_engine/TextureCompressor.cpp b/gl_engine/TextureCompressor.cpp index 8a217b3a..06bd09b7 100644 --- a/gl_engine/TextureCompressor.cpp +++ b/gl_engine/TextureCompressor.cpp @@ -114,13 +114,12 @@ gl_engine::TextureCompressor::TextureCompressor(std::weak_ptr scratch, gl_engine::TextureCompressor::~TextureCompressor() { m_program.reset(); + m_encoding_framebuffer.reset(); m_copy_framebuffer.reset(); m_screen_quad.reset(); if (!QOpenGLContext::currentContext()) return; auto* f = QOpenGLContext::currentContext()->extraFunctions(); - f->glDeleteFramebuffers(1, &m_encoding_framebuffer); - f->glDeleteTextures(1, &m_encoded_texture); f->glDeleteBuffers(1, &m_encoded_buffer); } @@ -179,21 +178,18 @@ std::optional gl_engine::TextureCompressor::initialise() if (m_atlas_width <= 0 || m_atlas_height <= 0 || m_atlas_height > maximum_texture_size) return "Texture compression output atlas exceeds the maximum texture size"; - const auto internal_format = mode == ReadbackMode::RG32UI ? GL_RG32UI : GL_RGBA32UI; - f->glGenTextures(1, &m_encoded_texture); - f->glBindTexture(GL_TEXTURE_2D, m_encoded_texture); - f->glTexStorage2D(GL_TEXTURE_2D, 1, internal_format, m_atlas_width, m_atlas_height); - f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - - f->glGenFramebuffers(1, &m_encoding_framebuffer); - f->glBindFramebuffer(GL_FRAMEBUFFER, m_encoding_framebuffer); - f->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_encoded_texture, 0); - const auto framebuffer_status = f->glCheckFramebufferStatus(GL_FRAMEBUFFER); + const auto colour_format = mode == ReadbackMode::RG32UI + ? Framebuffer::ColourFormat::RG32UI + : Framebuffer::ColourFormat::RGBA32UI; + auto candidate = std::make_unique(Framebuffer::DepthFormat::None, + std::vector { colour_format }, + glm::uvec2 { unsigned(m_atlas_width), unsigned(m_atlas_height) }); + candidate->bind_for_reading(); + const auto framebuffer_status = f->glCheckFramebufferStatus(GL_READ_FRAMEBUFFER); if (framebuffer_status != GL_FRAMEBUFFER_COMPLETE) - return "Texture compression framebuffer is incomplete"; + return mode == ReadbackMode::RG32UI + ? "RG32UI texture compression framebuffer is incomplete" + : "RGBA32UI texture compression framebuffer is incomplete"; if (mode == ReadbackMode::RG32UI) { GLint implementation_read_format = 0; @@ -204,6 +200,7 @@ std::optional gl_engine::TextureCompressor::initialise() if (implementation_read_format != GL_RG_INTEGER || implementation_read_type != GL_UNSIGNED_INT) return "RG32UI framebuffer readback is unavailable"; } + m_encoding_framebuffer = std::move(candidate); return std::nullopt; }; @@ -218,10 +215,6 @@ std::optional gl_engine::TextureCompressor::initialise() auto selected_mode = requested_mode == ReadbackMode::RGBA32UI ? ReadbackMode::RGBA32UI : ReadbackMode::RG32UI; auto output_error = create_output(selected_mode); if (output_error && requested_mode == ReadbackMode::Auto) { - f->glDeleteFramebuffers(1, &m_encoding_framebuffer); - f->glDeleteTextures(1, &m_encoded_texture); - m_encoding_framebuffer = 0; - m_encoded_texture = 0; selected_mode = ReadbackMode::RGBA32UI; output_error = create_output(selected_mode); if (!output_error) @@ -361,7 +354,7 @@ std::expected gl_engine::Text const auto encoding_width = GLsizei(std::min(encoding_pixels, size_t(m_atlas_width))); const auto encoding_height = GLsizei((encoding_pixels + size_t(encoding_width) - 1) / size_t(encoding_width)); - f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_encoding_framebuffer); + m_encoding_framebuffer->bind_for_drawing(); f->glViewport(0, 0, encoding_width, encoding_height); m_program->bind(); m_program->set_uniform("source_texture", 7); @@ -386,7 +379,7 @@ std::expected gl_engine::Text f->glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &previous_read_framebuffer); f->glGetIntegerv(GL_READ_BUFFER, &previous_read_buffer); f->glGetIntegerv(GL_PACK_ALIGNMENT, &previous_pack_alignment); - f->glBindFramebuffer(GL_READ_FRAMEBUFFER, m_encoding_framebuffer); + m_encoding_framebuffer->bind_for_reading(); f->glReadBuffer(GL_COLOR_ATTACHMENT0); f->glPixelStorei(GL_PACK_ALIGNMENT, 1); f->glBindBuffer(GL_PIXEL_PACK_BUFFER, m_encoded_buffer); diff --git a/gl_engine/TextureCompressor.h b/gl_engine/TextureCompressor.h index 276b5e03..e72a6138 100644 --- a/gl_engine/TextureCompressor.h +++ b/gl_engine/TextureCompressor.h @@ -111,11 +111,10 @@ class TextureCompressor { unsigned m_mip_levels = 0; GLsizei m_atlas_width = 0; GLsizei m_atlas_height = 0; - GLuint m_encoded_texture = 0; GLuint m_encoded_buffer = 0; - GLuint m_encoding_framebuffer = 0; std::unique_ptr m_program; + std::unique_ptr m_encoding_framebuffer; std::unique_ptr m_copy_framebuffer; std::unique_ptr m_screen_quad; }; diff --git a/unittests/gl_engine/texture_compressor.cpp b/unittests/gl_engine/texture_compressor.cpp index d4b84315..c85e9a2a 100644 --- a/unittests/gl_engine/texture_compressor.cpp +++ b/unittests/gl_engine/texture_compressor.cpp @@ -184,12 +184,63 @@ TEST_CASE("GPU texture compression supports automatic and explicit readback mode const auto direct_result = direct.compress(layers); if (direct_result) { CHECK(direct.effective_readback_mode() == Compressor::ReadbackMode::RG32UI); + CHECK(automatic.effective_readback_mode() == Compressor::ReadbackMode::RG32UI); CHECK(reconstruct_srgb(*auto_output, resolution, 0) == reconstruct_srgb(*direct_output, resolution, 0)); } else { CHECK(direct_result.error().find("RG32UI") != std::string::npos); + CHECK(automatic.effective_readback_mode() == Compressor::ReadbackMode::RGBA32UI); } } +TEST_CASE("GPU texture compressor is reusable and preserves framebuffer bindings") +{ + constexpr unsigned resolution = 16; + const std::vector sources { test_raster(resolution) }; + auto scratch = rgba_scratch(sources, 1); + auto output = destination(gl_engine::Texture::Format::CompressedRGBA8, resolution, 2, 1); + + auto* f = QOpenGLContext::currentContext()->extraFunctions(); + std::array expected_viewport {}; + f->glGetIntegerv(GL_VIEWPORT, expected_viewport.data()); + Compressor compressor(scratch, output, { .readback_mode = Compressor::ReadbackMode::RGBA32UI }); + std::array actual_viewport {}; + f->glGetIntegerv(GL_VIEWPORT, actual_viewport.data()); + CHECK(actual_viewport == expected_viewport); + + const std::array first_layer { 0 }; + const std::array second_layer { 1 }; + + gl_engine::Framebuffer draw_framebuffer( + gl_engine::Framebuffer::DepthFormat::None, { gl_engine::Framebuffer::ColourFormat::RGBA8 }); + gl_engine::Framebuffer read_framebuffer( + gl_engine::Framebuffer::DepthFormat::None, { gl_engine::Framebuffer::ColourFormat::RGBA8 }); + draw_framebuffer.bind_for_drawing(); + read_framebuffer.bind_for_reading(); + + GLint expected_draw_framebuffer = 0; + GLint expected_read_framebuffer = 0; + f->glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &expected_draw_framebuffer); + f->glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &expected_read_framebuffer); + REQUIRE(expected_draw_framebuffer != expected_read_framebuffer); + + const auto first_result = compressor.compress(first_layer); + REQUIRE(first_result); + const auto second_result = compressor.compress(second_layer); + REQUIRE(second_result); + CHECK(second_result->bytes_written == first_result->bytes_written); + CHECK(second_result->layers_written == 1); + CHECK(second_result->mip_levels_written == 1); + + GLint actual_draw_framebuffer = 0; + GLint actual_read_framebuffer = 0; + f->glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &actual_draw_framebuffer); + f->glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &actual_read_framebuffer); + CHECK(actual_draw_framebuffer == expected_draw_framebuffer); + CHECK(actual_read_framebuffer == expected_read_framebuffer); + gl_engine::Framebuffer::unbind(); + CHECK(psnr(reconstruct_srgb(*output, resolution, 1), sources.front()) > 10.0); +} + TEST_CASE("GPU texture compressor exposes every platform algorithm") { constexpr unsigned resolution = 8; From f85bc9ed74ec7eac7a37b1ce14d4c0a1fd454e4a Mon Sep 17 00:00:00 2001 From: adam-ce <5292991+adam-ce@users.noreply.github.com> Date: Sun, 30 Aug 2026 14:18:51 +0200 Subject: [PATCH 38/38] Harden texture compression setup and errors --- apps/texture_compression_benchmark/main.cpp | 14 +++++----- gl_engine/Texture.cpp | 6 +++++ gl_engine/TextureCompressor.cpp | 3 +-- gl_engine/TextureCompressor.h | 4 +-- unittests/gl_engine/UnittestGLContext.cpp | 2 ++ unittests/gl_engine/main.cpp | 24 ++++++++--------- .../texture_compression_benchmark/main.cpp | 27 ++++++++++--------- 7 files changed, 45 insertions(+), 35 deletions(-) diff --git a/apps/texture_compression_benchmark/main.cpp b/apps/texture_compression_benchmark/main.cpp index 76298190..81b023fd 100644 --- a/apps/texture_compression_benchmark/main.cpp +++ b/apps/texture_compression_benchmark/main.cpp @@ -17,13 +17,13 @@ int main(int argc, char** argv) QQuickWindow::setGraphicsApi(QSGRendererInterface::GraphicsApi::OpenGLRhi); QSurfaceFormat format; - if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGL) { - format.setRenderableType(QSurfaceFormat::OpenGL); - format.setVersion(3, 3); - format.setProfile(QSurfaceFormat::CoreProfile); - } else { - format.setVersion(3, 0); - } +#if QT_CONFIG(opengles2) + format.setVersion(3, 0); +#else + format.setRenderableType(QSurfaceFormat::OpenGL); + format.setVersion(3, 3); + format.setProfile(QSurfaceFormat::CoreProfile); +#endif QSurfaceFormat::setDefaultFormat(format); QGuiApplication application(argc, argv); diff --git a/gl_engine/Texture.cpp b/gl_engine/Texture.cpp index e1ce7694..77a45606 100644 --- a/gl_engine/Texture.cpp +++ b/gl_engine/Texture.cpp @@ -123,6 +123,12 @@ void gl_engine::Texture::allocate_array(unsigned int width, unsigned int height, { Q_ASSERT(m_target == Target::_2dArray); Q_ASSERT(m_format != Format::Invalid); + Q_ASSERT(width > 0); + Q_ASSERT(height > 0); + Q_ASSERT(n_layers > 0); + + const auto maximum_mip_levels = 1u + unsigned(std::floor(std::log2((std::max)(width, height)))); + Q_ASSERT(mip_levels <= maximum_mip_levels); auto mip_level_count = GLsizei(mip_levels); if (mip_level_count == 0) { diff --git a/gl_engine/TextureCompressor.cpp b/gl_engine/TextureCompressor.cpp index 06bd09b7..e0f9cd86 100644 --- a/gl_engine/TextureCompressor.cpp +++ b/gl_engine/TextureCompressor.cpp @@ -113,12 +113,11 @@ gl_engine::TextureCompressor::TextureCompressor(std::weak_ptr scratch, gl_engine::TextureCompressor::~TextureCompressor() { + Q_ASSERT(QOpenGLContext::currentContext()); m_program.reset(); m_encoding_framebuffer.reset(); m_copy_framebuffer.reset(); m_screen_quad.reset(); - if (!QOpenGLContext::currentContext()) - return; auto* f = QOpenGLContext::currentContext()->extraFunctions(); f->glDeleteBuffers(1, &m_encoded_buffer); } diff --git a/gl_engine/TextureCompressor.h b/gl_engine/TextureCompressor.h index e72a6138..c1c808fa 100644 --- a/gl_engine/TextureCompressor.h +++ b/gl_engine/TextureCompressor.h @@ -71,11 +71,11 @@ class TextureCompressor { TextureCompressor(std::weak_ptr scratch, std::weak_ptr destination, Settings settings); - ~TextureCompressor(); TextureCompressor(const TextureCompressor&) = delete; - TextureCompressor(TextureCompressor&&) = delete; TextureCompressor& operator=(const TextureCompressor&) = delete; + TextureCompressor(TextureCompressor&&) = delete; TextureCompressor& operator=(TextureCompressor&&) = delete; + ~TextureCompressor(); [[nodiscard]] std::expected compress(std::span destination_layers); diff --git a/unittests/gl_engine/UnittestGLContext.cpp b/unittests/gl_engine/UnittestGLContext.cpp index e9e7d179..ac951af0 100644 --- a/unittests/gl_engine/UnittestGLContext.cpp +++ b/unittests/gl_engine/UnittestGLContext.cpp @@ -37,6 +37,7 @@ UnittestGLContext::UnittestGLContext() // Request OpenGL 3.3 core or OpenGL ES 3.0. if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGL) { qDebug("Requesting 3.3 core context"); + surface_format.setRenderableType(QSurfaceFormat::OpenGL); surface_format.setVersion(3, 3); surface_format.setProfile(QSurfaceFormat::CoreProfile); } else { @@ -47,6 +48,7 @@ UnittestGLContext::UnittestGLContext() QSurfaceFormat::setDefaultFormat(surface_format); m_context.setFormat(surface_format); + surface.setFormat(surface_format); surface.create(); const auto r = m_context.create(); Q_ASSERT(r); diff --git a/unittests/gl_engine/main.cpp b/unittests/gl_engine/main.cpp index 02be9677..2eadc5a9 100644 --- a/unittests/gl_engine/main.cpp +++ b/unittests/gl_engine/main.cpp @@ -58,24 +58,24 @@ CATCH_REGISTER_LISTENER(ProgressPrinter) int main( int argc, char* argv[] ) { std::fflush(stdout); - int argc_qt = 0; - QGuiApplication app = {argc_qt, argv}; - QSurfaceFormat fmt; fmt.setDepthBufferSize(24); fmt.setOption(QSurfaceFormat::DebugContext); - // Request OpenGL 3.3 core or OpenGL ES 3.0. - if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGL) { - qDebug("Requesting 3.3 core context"); - fmt.setVersion(3, 3); - fmt.setProfile(QSurfaceFormat::CoreProfile); - } else { - qDebug("Requesting 3.0 context"); - fmt.setVersion(3, 0); - } +#if QT_CONFIG(opengles2) + qDebug("Requesting 3.0 context"); + fmt.setVersion(3, 0); +#else + qDebug("Requesting 3.3 core context"); + fmt.setRenderableType(QSurfaceFormat::OpenGL); + fmt.setVersion(3, 3); + fmt.setProfile(QSurfaceFormat::CoreProfile); +#endif QSurfaceFormat::setDefaultFormat(fmt); + int argc_qt = 0; + QGuiApplication app = {argc_qt, argv}; + // Catch::Session().run(m_argc, m_argv); is in UnittestGlWindow::initializeGL() // to my understanding this is necessary for webassembly, because stuff is started // asynchronously, and the gl context is not yet available when main is running. diff --git a/unittests/texture_compression_benchmark/main.cpp b/unittests/texture_compression_benchmark/main.cpp index 8c3c3969..16481ecc 100644 --- a/unittests/texture_compression_benchmark/main.cpp +++ b/unittests/texture_compression_benchmark/main.cpp @@ -621,7 +621,8 @@ class BenchmarkWindow final : public QOpenGLWindow { state.scratch->generate_mipmaps(); const auto result = algorithm.compressor->compress( std::span(state.destination_layers).first(state.batch_size)); - Q_ASSERT(result); + if (!result) + qFatal("Texture compression failed: %s", result.error().c_str()); } auto* functions = QOpenGLContext::currentContext()->extraFunctions(); @@ -708,7 +709,8 @@ class BenchmarkWindow final : public QOpenGLWindow { state.scratch->generate_mipmaps(); const auto result = algorithm.compressor->compress( std::span(state.destination_layers).first(active_batch_size)); - Q_ASSERT(result); + if (!result) + qFatal("Texture compression failed: %s", result.error().c_str()); for (unsigned layer = 0; layer < active_batch_size; ++layer) { accumulate_quality(accumulator, reconstruct(state, layer), @@ -856,21 +858,22 @@ class BenchmarkWindow final : public QOpenGLWindow { int main(int argc, char* argv[]) { - QGuiApplication application(argc, argv); - QCoreApplication::setApplicationName(QStringLiteral("TextureCompressionBenchmark")); - QCoreApplication::setOrganizationName(QStringLiteral("AlpineMaps.org")); - QSurfaceFormat format; format.setDepthBufferSize(24); format.setOption(QSurfaceFormat::DebugContext); - if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGL) { - format.setVersion(3, 3); - format.setProfile(QSurfaceFormat::CoreProfile); - } else { - format.setVersion(3, 0); - } +#if QT_CONFIG(opengles2) + format.setVersion(3, 0); +#else + format.setRenderableType(QSurfaceFormat::OpenGL); + format.setVersion(3, 3); + format.setProfile(QSurfaceFormat::CoreProfile); +#endif QSurfaceFormat::setDefaultFormat(format); + QGuiApplication application(argc, argv); + QCoreApplication::setApplicationName(QStringLiteral("TextureCompressionBenchmark")); + QCoreApplication::setOrganizationName(QStringLiteral("AlpineMaps.org")); + BenchmarkWindow window; window.show(); return application.exec();