diff --git a/zstd/scripts/generate_histogram.py b/zstd/scripts/generate_histogram.py new file mode 100644 index 0000000..b335b0a --- /dev/null +++ b/zstd/scripts/generate_histogram.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +""" +Generate histogram PNGs from zstdgpu_demo performance CSV output. + +Usage: + python generate_histogram.py --input --output [--title ] + +Consumes the wide-format CSV emitted by zstdgpu_demo's --out-csv flag: + RunIdx, Stage 0 (us), Stage 0 :: <scope> (us), ..., Readback 0 (us), + Stage 1 (us), ..., Stage 2 (us), Bandwidth (GB/s) + +Plots a histogram of the 'Bandwidth (GB/s)' column. Per-stage timing columns +are preserved in the CSV but not plotted here (the histogram is the summary +view; users wanting per-stage detail can read the CSV directly). +""" + +import argparse +import csv +import math +import sys + + +def try_import_matplotlib(): + try: + import matplotlib + matplotlib.use("Agg") # Non-interactive backend for CI + import matplotlib.pyplot as plt + return plt + except ImportError: + print( + "WARNING: matplotlib not installed. Install with: pip install matplotlib", + file=sys.stderr, + ) + return None + + +# Match Pavel's --out-csv column header verbatim ("Bandwidth (GB/s)"). +# Kept case-insensitive and whitespace-tolerant in case the schema spelling +# drifts upstream — the eyeballed match is "bandwidth". +def _is_bandwidth_column(col_name: str) -> bool: + return col_name is not None and "bandwidth" in col_name.strip().lower() + + +def main(): + parser = argparse.ArgumentParser(description="Generate histogram from zstdgpu perf CSV") + parser.add_argument("--input", required=True, help="Path to input CSV file") + parser.add_argument("--output", required=True, help="Path to output PNG file") + parser.add_argument("--title", default="Bandwidth", help="Chart title") + args = parser.parse_args() + + plt = try_import_matplotlib() + if plt is None: + return 1 + + data = [] + skipped_non_finite = 0 + bandwidth_col = None + with open(args.input, newline="") as f: + reader = csv.DictReader(f) + if reader.fieldnames is None: + print(f"CSV has no header row: {args.input}", file=sys.stderr) + return 1 + # Pick the bandwidth column by name (resilient to header drift). + for col in reader.fieldnames: + if _is_bandwidth_column(col): + bandwidth_col = col + break + if bandwidth_col is None: + print( + f"No Bandwidth column found in {args.input} " + f"(headers: {reader.fieldnames})", + file=sys.stderr, + ) + return 1 + + for row in reader: + raw = row.get(bandwidth_col, "") + if raw is None or raw == "": + continue + try: + val = float(raw) + except ValueError: + # Pavel may write empty strings for skipped iterations; ignore. + continue + if math.isfinite(val): + data.append(val) + else: + skipped_non_finite += 1 + + if skipped_non_finite > 0: + print( + f"WARNING: Skipped {skipped_non_finite} non-finite (Inf/NaN) value(s) from {args.input}", + file=sys.stderr, + ) + + if not data: + print( + f"No finite Bandwidth (GB/s) data found in {args.input}", + file=sys.stderr, + ) + return 1 + + plt.figure() + plt.hist(data, bins=20) + plt.xlabel("Bandwidth (GB/s)") + plt.ylabel("Count") + plt.title(args.title) + plt.savefig(args.output) + plt.close() + print(f"Generated: {args.output}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/zstd/zstd.sln b/zstd/zstd.sln index 7c5888c..e0eb457 100644 --- a/zstd/zstd.sln +++ b/zstd/zstd.sln @@ -11,6 +11,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "zstdgpu_tests", "zstdgpu_te EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "googletest_static", "ThirdParty\googletest_static.vcxproj", "{49811F10-3D14-403E-859D-40DFCBB35C7B}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "zstdgpu_ci_tests", "zstdgpu_ci_tests\zstdgpu_ci_tests.vcxproj", "{17F60A53-4A7F-4107-AF0A-914497018D67}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -69,6 +71,18 @@ Global {49811F10-3D14-403E-859D-40DFCBB35C7B}.Release|x64.Build.0 = Release|x64 {49811F10-3D14-403E-859D-40DFCBB35C7B}.Release|x86.ActiveCfg = Release|Win32 {49811F10-3D14-403E-859D-40DFCBB35C7B}.Release|x86.Build.0 = Release|Win32 + {17F60A53-4A7F-4107-AF0A-914497018D67}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {17F60A53-4A7F-4107-AF0A-914497018D67}.Debug|ARM64.Build.0 = Debug|ARM64 + {17F60A53-4A7F-4107-AF0A-914497018D67}.Debug|x64.ActiveCfg = Debug|x64 + {17F60A53-4A7F-4107-AF0A-914497018D67}.Debug|x64.Build.0 = Debug|x64 + {17F60A53-4A7F-4107-AF0A-914497018D67}.Debug|x86.ActiveCfg = Debug|Win32 + {17F60A53-4A7F-4107-AF0A-914497018D67}.Debug|x86.Build.0 = Debug|Win32 + {17F60A53-4A7F-4107-AF0A-914497018D67}.Release|ARM64.ActiveCfg = Release|ARM64 + {17F60A53-4A7F-4107-AF0A-914497018D67}.Release|ARM64.Build.0 = Release|ARM64 + {17F60A53-4A7F-4107-AF0A-914497018D67}.Release|x64.ActiveCfg = Release|x64 + {17F60A53-4A7F-4107-AF0A-914497018D67}.Release|x64.Build.0 = Release|x64 + {17F60A53-4A7F-4107-AF0A-914497018D67}.Release|x86.ActiveCfg = Release|Win32 + {17F60A53-4A7F-4107-AF0A-914497018D67}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/zstd/zstdgpu_ci_tests/main.cpp b/zstd/zstdgpu_ci_tests/main.cpp new file mode 100644 index 0000000..298211d --- /dev/null +++ b/zstd/zstdgpu_ci_tests/main.cpp @@ -0,0 +1,234 @@ +/** + * Copyright (c) Microsoft. All rights reserved. + * This code is licensed under the MIT License (MIT). + * THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF + * ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY + * IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR + * PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT. + */ + +// Entry point for the Zstd GPU CI tests. Thin GTest wrapper that shells out +// to zstdgpu_demo.exe to validate Zstd GPU decompression shaders. +// +// main() parses custom CLI flags (--content-path, --demo-path, etc.), +// validates them (hard failure with non-zero exit on any misconfiguration — +// never silently skip), discovers .zst files under the content path exactly +// once, then hands off to GTest which runs the parameterized suite defined +// in zstdgpu_ci_tests.cpp. Each test spawns the demo as a child process. +// +// If the content path is missing, unreadable, or contains no .zst files, or +// if the demo executable is missing, the process exits non-zero before any +// test runs. This intentionally prevents a "green" run with zero coverage. +// +// This file also owns the g_testConfig storage and the file discovery +// implementation declared in zstdgpu_ci_tests.h. + +#include "zstdgpu_ci_tests.h" +#include <gtest/gtest.h> +#include <algorithm> +#include <cstring> +#include <filesystem> +#include <iostream> +#include <string> +#include <system_error> + +// Global config, declared extern in zstdgpu_ci_tests.h. Set once in main(), +// read from test bodies. +TestConfig g_testConfig; + +// File discovery +// +// Uses the non-throwing overloads of recursive_directory_iterator so a single +// unreadable subdirectory anywhere in the tree does not abort the entire walk +// (which would take down the process before any test runs — see review +// feedback on the throwing overload). Per-entry errors are logged as warnings +// and the walk continues. + +std::vector<std::string> DiscoverZstFiles(const std::string& contentPath) +{ + std::vector<std::string> files; + + if (contentPath.empty() || !std::filesystem::exists(contentPath) || !std::filesystem::is_directory(contentPath)) + { + return files; + } + + std::error_code ec; + auto iter = std::filesystem::recursive_directory_iterator( + contentPath, + std::filesystem::directory_options::skip_permission_denied, + ec); + if (ec) + { + std::cerr << "Warning: could not open '" << contentPath << "' for scanning: " + << ec.message() << "\n"; + return files; + } + + auto endIter = std::filesystem::recursive_directory_iterator(); + while (iter != endIter) + { + std::error_code entryEc; + if (iter->is_regular_file(entryEc) && !entryEc) + { + if (iter->path().extension() == ".zst") + { + files.push_back(iter->path().string()); + } + } + else if (entryEc) + { + std::cerr << "Warning: skipping '" << iter->path().string() << "': " + << entryEc.message() << "\n"; + } + iter.increment(ec); + if (ec) + { + std::cerr << "Warning: recursive walk aborted early after '" + << iter->path().string() << "': " << ec.message() << "\n"; + break; + } + } + + std::sort(files.begin(), files.end()); + return files; +} + +// CLI and entry point + +// QOL for diagnostics. For running manually +// Activate with --help-ci to avoid conflicting with GTest's own --help output. +static void PrintUsage(const char* exe) +{ + std::cout << "Usage: " << exe << " [gtest_options] [options]\n" + << "\n" + << "Options:\n" + << " --content-path <dir> Directory containing .zst test files (required)\n" + << " --demo-path <path> Path to zstdgpu_demo.exe (required)\n" + << " --log-dir <dir> Directory for logs and CSV output\n" + << " --log-file <path> Consolidated text log file\n" + << " --run-count <N> Perf test iteration count (default: 40)\n" + << " --timeout <seconds> Per-test process timeout (default: no timeout)\n" + << std::endl; +} + +// Prints an error prefixed with "Error:" to stderr and returns the exit code so +// main() can 'return Fail(...)' in a single expression. Keeps validation blocks +// short and consistent. +static int Fail(const std::string& msg) +{ + std::cerr << "Error: " << msg << std::endl; + return 1; +} + +// Parse custom flags out of argv before we hand argv to GTest. GTest's own +// InitGoogleTest() runs later and will consume its own flags (e.g. +// --gtest_filter). Returns true on success; on --help-ci, prints usage and +// sets shouldExit=true so main() can return 0 cleanly. +static bool ParseArgs(int argc, char** argv, TestConfig& config, bool& shouldExit) +{ + shouldExit = false; + for (int i = 1; i < argc; ++i) + { + if (std::strcmp(argv[i], "--content-path") == 0 && i + 1 < argc) + { + config.contentPath = argv[++i]; + } + else if (std::strcmp(argv[i], "--demo-path") == 0 && i + 1 < argc) + { + config.demoPath = argv[++i]; + } + else if (std::strcmp(argv[i], "--log-dir") == 0 && i + 1 < argc) + { + config.logDir = argv[++i]; + } + else if (std::strcmp(argv[i], "--log-file") == 0 && i + 1 < argc) + { + config.logFile = argv[++i]; + } + else if (std::strcmp(argv[i], "--run-count") == 0 && i + 1 < argc) + { + config.runCount = std::atoi(argv[++i]); + if (config.runCount <= 0) + config.runCount = 40; + } + else if (std::strcmp(argv[i], "--timeout") == 0 && i + 1 < argc) + { + config.timeoutSeconds = std::atoi(argv[++i]); + if (config.timeoutSeconds < 0) + config.timeoutSeconds = 0; + } + else if (std::strcmp(argv[i], "--help-ci") == 0) + { + PrintUsage(argv[0]); + shouldExit = true; + return true; + } + } + return true; +} + +// Fail-loud validation + one-shot filesystem discovery. Any misconfiguration +// means the run cannot produce meaningful test coverage, so we return a +// non-zero exit code before any test instantiates. Silent skipping would let +// broken pipelines pass green with zero coverage. +// +// On success also creates the log directory (defaulting to cwd) so downstream +// writes don't have to check. +static int ValidateAndDiscover(TestConfig& config) +{ + if (config.demoPath.empty()) + return Fail("--demo-path is required."); + if (!std::filesystem::exists(config.demoPath)) + return Fail("--demo-path '" + config.demoPath + "' does not exist."); + if (!std::filesystem::is_regular_file(config.demoPath)) + return Fail("--demo-path '" + config.demoPath + "' is not a regular file."); + + if (config.contentPath.empty()) + return Fail("--content-path is required."); + if (!std::filesystem::exists(config.contentPath)) + return Fail("--content-path '" + config.contentPath + "' does not exist."); + if (!std::filesystem::is_directory(config.contentPath)) + return Fail("--content-path '" + config.contentPath + "' is not a directory."); + + // Discover exactly once. Cached on TestConfig for GetTestFiles() to reuse + // when instantiating the parameterized fixture — avoids walking the tree + // twice. + config.discoveredFiles = DiscoverZstFiles(config.contentPath); + if (config.discoveredFiles.empty()) + return Fail("--content-path '" + config.contentPath + "' contains no .zst files."); + + std::cout << "Discovered " << config.discoveredFiles.size() << " .zst file(s) at '" + << config.contentPath << "'.\n"; + + if (config.logDir.empty()) + { + config.logDir = std::filesystem::current_path().string(); + } + if (!std::filesystem::exists(config.logDir)) + { + std::filesystem::create_directories(config.logDir); + } + + return 0; +} + +int main(int argc, char** argv) +{ + TestConfig config; + + bool shouldExit = false; + if (!ParseArgs(argc, argv, config, shouldExit)) + return 1; + if (shouldExit) + return 0; // --help-ci printed usage and asked to exit cleanly + + if (int rc = ValidateAndDiscover(config); rc != 0) + return rc; + + g_testConfig = std::move(config); + + testing::InitGoogleTest(&argc, argv); + testing::GTEST_FLAG(catch_exceptions) = false; + return RUN_ALL_TESTS(); +} diff --git a/zstd/zstdgpu_ci_tests/zstdgpu_ci_tests.cpp b/zstd/zstdgpu_ci_tests/zstdgpu_ci_tests.cpp new file mode 100644 index 0000000..ee61532 --- /dev/null +++ b/zstd/zstdgpu_ci_tests/zstdgpu_ci_tests.cpp @@ -0,0 +1,461 @@ +/** + * Copyright (c) Microsoft. All rights reserved. + * This code is licensed under the MIT License (MIT). + * THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF + * ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY + * IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR + * PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT. + */ + +// Test definitions and demo runner for the Zstd GPU CI tests. +// +// Parameterized test suite (ZstdGpuDemoTests) instantiated once per .zst file +// under the content path. Each file produces 6 scenarios: +// +// Correctness (ASSERT — hard fail): +// - SimulationCheck : --sim-gpu with CPU+GPU validation +// - D3D12DebugLayer : --d3d-dbg +// - ExternalMemory : --ext-mem +// - GraphicsQueue : --d3d-gfx +// +// Performance (EXPECT — soft fail, also verify CSV output was written): +// - OverallThroughput: --prf-lvl 0 → results/throughput_<stem>.csv +// - PerStageTiming : --prf-lvl 2 → results/stages_<stem>.csv + +#include "zstdgpu_ci_tests.h" +#include <gtest/gtest.h> +#include <array> +#include <filesystem> +#include <fstream> +#include <iostream> +#include <sstream> +#include <thread> +#include <Windows.h> + +// Internal types + forward declarations +// +// These are used only inside this translation unit; keeping them out of the +// header limits the wrapper's public surface to the two things main.cpp +// actually needs (TestConfig, DiscoverZstFiles). + +namespace +{ + // Captures the outcome of a single demo process invocation. + struct DemoResult + { + int exitCode = -1; // Process exit code (0 = success) + std::string stdOut; // Captured stdout + stderr + std::string launchError; // Error message if the process failed to launch + std::string commandLine; // The exact command line that was executed + bool timedOut = false; // True if the process was killed due to timeout + }; + + DemoResult RunDemo( + const std::string& demoPath, + const std::vector<std::string>& args, + int timeoutSeconds); + + std::vector<std::string> BuildCorrectnessArgs( + const std::string& zstFile, + const std::vector<std::string>& scenarioFlags); + + std::vector<std::string> BuildPerformanceArgs( + const std::string& zstFile, + int profilingLevel, + int runCount, + const std::string& csvOutputPath); +} + +// Helpers + +// Returns the list of .zst files to parameterize over. +// GTest evaluates this lazily when the test suite is instantiated (after main +// has parsed CLI args, validated inputs, and cached the discovered file list +// in TestConfig). We just return the cached list here — the actual filesystem +// walk happened once, up front, in main(). +static std::vector<std::string> GetTestFiles() +{ + return g_testConfig.discoveredFiles; +} + +// Converts a full file path to a valid GTest parameter name. +// GTest names must be alphanumeric + underscore, no leading digits, AND UNIQUE +// across the full INSTANTIATE_TEST_SUITE_P set. Using just the filename stem +// collides when the same leaf name appears across different subdirectories +// (e.g. firefly_albedo.DDS.zst exists under BC1/, BC1mip0/, block4K_*, etc.), +// causing a fatal gtest assertion at startup. Use the path relative to +// --content-path so different folders produce different names. +static std::string SanitizeTestName(const testing::TestParamInfo<std::string>& info) +{ + std::filesystem::path full(info.param); + std::filesystem::path rel; + if (!g_testConfig.contentPath.empty()) + { + std::error_code ec; + rel = std::filesystem::relative(full, g_testConfig.contentPath, ec); + if (ec || rel.empty() || rel.string().rfind("..", 0) == 0) + { + rel = full.filename(); // fallback: out-of-tree, just use leaf + } + } + else + { + rel = full.filename(); + } + + // Drop the trailing .zst extension for readability; everything else stays. + std::string name = rel.string(); + const std::string ext = ".zst"; + if (name.size() >= ext.size() && + name.compare(name.size() - ext.size(), ext.size(), ext) == 0) + { + name.resize(name.size() - ext.size()); + } + + std::string result; + result.reserve(name.size()); + for (char c : name) + { + result += std::isalnum(static_cast<unsigned char>(c)) ? c : '_'; + } + if (!result.empty() && std::isdigit(static_cast<unsigned char>(result[0]))) + { + result = "_" + result; + } + return result.empty() ? "Unknown" : result; +} + +// Appends per-test output to the consolidated log file (--log-file). +static void WriteToLogFile(const std::string& zstFile, const DemoResult& result) +{ + if (g_testConfig.logFile.empty()) + return; + + std::ofstream log(g_testConfig.logFile, std::ios::app); + if (!log) + { + std::cerr << "Warning: could not open --log-file '" + << g_testConfig.logFile << "' for append.\n"; + return; + } + auto* testInfo = ::testing::UnitTest::GetInstance()->current_test_info(); + log << "=== " << testInfo->test_suite_name() << "." << testInfo->name() << " ===\n"; + log << "File: " << zstFile << "\n"; + log << "Exit code: " << result.exitCode << "\n"; + log << result.stdOut << "\n"; +} + +// Test runners + +// Run a correctness scenario. Spawns zstdgpu_demo.exe with the given .zst file and scenario flags, then asserts exit code == 0. +// main() has already validated the demo path exists, so we don't re-check here. +// stdout is printed via the unconditional [DEMO OUT] block below and does NOT +// appear a second time in the ASSERT_EQ failure message — that would duplicate +// the same text in the log. +static void RunCorrectnessTest(const std::string& zstFile, const std::vector<std::string>& scenarioFlags) +{ + auto args = BuildCorrectnessArgs(zstFile, scenarioFlags); + auto result = RunDemo(g_testConfig.demoPath, args, g_testConfig.timeoutSeconds); + + // Write to log file before assertions so logs are captured even if an ASSERT aborts early. + WriteToLogFile(zstFile, result); + + // Log the output regardless of pass/fail. This IS the demo stdout capture — + // the assertion messages below intentionally do NOT reprint result.stdOut. + std::cout << "[DEMO CMD] " << result.commandLine << "\n"; + if (!result.stdOut.empty()) + { + std::cout << "[DEMO OUT] " << result.stdOut << "\n"; + } + + ASSERT_FALSE(result.timedOut) + << "Demo process timed out after " << g_testConfig.timeoutSeconds << " seconds.\n" + << "Command: " << result.commandLine; + + ASSERT_TRUE(result.launchError.empty()) + << "Failed to launch demo: " << result.launchError << "\n" + << "Command: " << result.commandLine; + + ASSERT_EQ(result.exitCode, 0) + << "Demo process returned non-zero exit code: " << result.exitCode << "\n" + << "Command: " << result.commandLine + << " (stdout already printed above as [DEMO OUT])"; +} + +// Run a performance scenario. Spawns zstdgpu_demo.exe with profiling flags and requests CSV output. Uses EXPECT (not ASSERT) to verify the demo executed successfully and produced CSV output. +// main() has already validated the demo path exists. +static void RunPerformanceTest(const std::string& zstFile, int profilingLevel) +{ + // Build CSV output path matching spec convention: + // prf-lvl 0 → results/throughput_<stem>.csv + // prf-lvl 2 → results/stages_<stem>.csv + std::string stem = std::filesystem::path(zstFile).stem().string(); + std::string prefix = (profilingLevel == 0) ? "throughput" : "stages"; + std::filesystem::path resultsDir = std::filesystem::path(g_testConfig.logDir) / "results"; + if (!std::filesystem::exists(resultsDir)) + { + std::filesystem::create_directories(resultsDir); + } + std::string csvPath = (resultsDir / (prefix + "_" + stem + ".csv")).string(); + + auto args = BuildPerformanceArgs(zstFile, profilingLevel, g_testConfig.runCount, csvPath); + auto result = RunDemo(g_testConfig.demoPath, args, g_testConfig.timeoutSeconds); + + // Write to log file before assertions so logs are captured even if a check fails. + WriteToLogFile(zstFile, result); + + // Log the output regardless of pass/fail. This IS the demo stdout capture — + // the assertion messages below intentionally do NOT reprint result.stdOut. + std::cout << "[DEMO CMD] " << result.commandLine << "\n"; + if (!result.stdOut.empty()) + { + std::cout << "[DEMO OUT] " << result.stdOut << "\n"; + } + + EXPECT_FALSE(result.timedOut) + << "Demo process timed out after " << g_testConfig.timeoutSeconds << " seconds.\n" + << "Command: " << result.commandLine; + + EXPECT_TRUE(result.launchError.empty()) + << "Failed to launch demo: " << result.launchError << "\n" + << "Command: " << result.commandLine; + + EXPECT_EQ(result.exitCode, 0) + << "Demo process returned non-zero exit code: " << result.exitCode << "\n" + << "Command: " << result.commandLine + << " (stdout already printed above as [DEMO OUT])"; + + EXPECT_TRUE(std::filesystem::exists(csvPath)) << "CSV not created: " << csvPath; + + if (std::filesystem::exists(csvPath)) + { + std::cout << "[PERF CSV] Written to: " << csvPath << "\n"; + } +} + +// Test fixture and test cases + +// Test fixture parameterized over .zst file paths (spec: ZstdGpuDemoTests). +// Both correctness and performance tests share this fixture — correctness tests +// use ASSERT (hard fail), performance tests use EXPECT (soft fail). +class ZstdGpuDemoTests : public ::testing::TestWithParam<std::string> +{ +}; + +// --- Correctness tests --- + +TEST_P(ZstdGpuDemoTests, SimulationCheck) +{ + RunCorrectnessTest(GetParam(), {"--chk-gpu", "--chk-cpu", "--sim-gpu"}); +} + +TEST_P(ZstdGpuDemoTests, D3D12DebugLayer) +{ + RunCorrectnessTest(GetParam(), {"--chk-gpu", "--d3d-dbg"}); +} + +TEST_P(ZstdGpuDemoTests, ExternalMemory) +{ + RunCorrectnessTest(GetParam(), {"--chk-gpu", "--ext-mem"}); +} + +TEST_P(ZstdGpuDemoTests, GraphicsQueue) +{ + RunCorrectnessTest(GetParam(), {"--chk-gpu", "--d3d-gfx"}); +} + +// --- Performance tests --- + +TEST_P(ZstdGpuDemoTests, OverallThroughput) +{ + RunPerformanceTest(GetParam(), 0); +} + +TEST_P(ZstdGpuDemoTests, PerStageTiming) +{ + RunPerformanceTest(GetParam(), 2); +} + +INSTANTIATE_TEST_SUITE_P( + ContentTests, + ZstdGpuDemoTests, + ::testing::ValuesIn(GetTestFiles()), + SanitizeTestName); + +// Demo runner implementation +// +// Spawns zstdgpu_demo.exe as a child process via CreateProcess with anonymous +// pipes. A background thread drains stdout to avoid pipe-buffer deadlocks. If +// the child exceeds the configured timeout, it is terminated and CancelIoEx +// releases the reader thread so the wrapper does not itself hang. +// +// All GPU / D3D12 dependencies live in the demo process; the test binary +// itself has no GPU dependency. + +namespace +{ + +// Builds a command line string with proper quoting for arguments containing spaces. +static std::string BuildCommandLine(const std::string& exe, const std::vector<std::string>& args) +{ + std::ostringstream cmd; + cmd << "\"" << exe << "\""; + for (const auto& arg : args) + { + cmd << " "; + if (arg.find(' ') != std::string::npos) + cmd << "\"" << arg << "\""; + else + cmd << arg; + } + return cmd.str(); +} + +DemoResult RunDemo( + const std::string& demoPath, + const std::vector<std::string>& args, + int timeoutSeconds) +{ + DemoResult result; + result.commandLine = BuildCommandLine(demoPath, args); + + // Create an anonymous pipe for capturing the child process's stdout/stderr. + SECURITY_ATTRIBUTES sa{}; + sa.nLength = sizeof(sa); + sa.bInheritHandle = TRUE; + + HANDLE hReadPipe = nullptr; + HANDLE hWritePipe = nullptr; + if (!CreatePipe(&hReadPipe, &hWritePipe, &sa, 0)) + { + result.launchError = "Failed to create pipe for demo process."; + return result; + } + + // Prevent the read end from being inherited by the child process. + SetHandleInformation(hReadPipe, HANDLE_FLAG_INHERIT, 0); + + // Redirect child's stdout and stderr to the write end of the pipe. + STARTUPINFOA si{}; + si.cb = sizeof(si); + si.dwFlags = STARTF_USESTDHANDLES; + si.hStdOutput = hWritePipe; + si.hStdError = hWritePipe; + + PROCESS_INFORMATION pi{}; + + std::vector<char> cmdBuf(result.commandLine.begin(), result.commandLine.end()); + cmdBuf.push_back('\0'); + + if (!CreateProcessA( + nullptr, + cmdBuf.data(), + nullptr, + nullptr, + TRUE, // inherit handles + 0, + nullptr, + nullptr, + &si, + &pi)) + { + CloseHandle(hReadPipe); + CloseHandle(hWritePipe); + result.launchError = "Failed to launch demo process. Error: " + std::to_string(GetLastError()); + return result; + } + + // Close the write end in the parent so ReadFile on the read end returns + // EOF when the child exits. + CloseHandle(hWritePipe); + + // Read the child's output on a background thread to prevent pipe buffer + // deadlocks (the pipe has a finite buffer; if it fills, the child blocks). + std::string capturedOutput; + std::thread readerThread([&capturedOutput, hReadPipe]() { + std::array<char, 4096> buf; + DWORD bytesRead = 0; + while (ReadFile(hReadPipe, buf.data(), static_cast<DWORD>(buf.size()), &bytesRead, nullptr) && bytesRead > 0) + { + capturedOutput.append(buf.data(), bytesRead); + } + }); + + // Wait for the child process, enforcing the timeout. + DWORD waitMs = (timeoutSeconds > 0) ? static_cast<DWORD>(timeoutSeconds) * 1000 : INFINITE; + DWORD waitResult = WaitForSingleObject(pi.hProcess, waitMs); + + if (waitResult == WAIT_TIMEOUT) + { + result.timedOut = true; + TerminateProcess(pi.hProcess, 1); + WaitForSingleObject(pi.hProcess, 5000); + + // Cancel any pending ReadFile on hReadPipe so the reader thread exits + // and readerThread.join() below returns. Without this, TerminateProcess + // on a wedged child can leave the child's inherited write-end of the + // pipe orphaned in kernel state briefly, and the reader thread's + // blocking ReadFile never returns — hanging the entire test runner + // indefinitely. CancelIoEx targets outstanding I/O on the handle from + // any thread. Safe to call even if no I/O is pending (returns FALSE + // with ERROR_NOT_FOUND, harmless). + CancelIoEx(hReadPipe, nullptr); + } + + DWORD exitCode = 0; + GetExitCodeProcess(pi.hProcess, &exitCode); + result.exitCode = static_cast<int>(exitCode); + + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + + // Wait for the reader thread to finish draining the pipe, then clean up. + readerThread.join(); + CloseHandle(hReadPipe); + + result.stdOut = std::move(capturedOutput); + return result; +} + +// Builds argument list for correctness tests: decompress once (--run-cnt 1) +// with GPU and CPU validation enabled, plus scenario-specific flags. +std::vector<std::string> BuildCorrectnessArgs( + const std::string& zstFile, + const std::vector<std::string>& scenarioFlags) +{ + std::vector<std::string> args; + args.push_back("--zst"); + args.push_back(zstFile); + args.push_back("--run-cnt"); + args.push_back("1"); + for (const auto& flag : scenarioFlags) + { + args.push_back(flag); + } + return args; +} + +// Builds argument list for performance tests: run N iterations at the specified +// profiling level, optionally writing per-run timing data to a CSV file. +std::vector<std::string> BuildPerformanceArgs( + const std::string& zstFile, + int profilingLevel, + int runCount, + const std::string& csvOutputPath) +{ + std::vector<std::string> args; + args.push_back("--zst"); + args.push_back(zstFile); + args.push_back("--prf-lvl"); + args.push_back(std::to_string(profilingLevel)); + args.push_back("--run-cnt"); + args.push_back(std::to_string(runCount)); + if (!csvOutputPath.empty()) + { + args.push_back("--out-csv"); + args.push_back(csvOutputPath); + } + return args; +} + +} // namespace diff --git a/zstd/zstdgpu_ci_tests/zstdgpu_ci_tests.h b/zstd/zstdgpu_ci_tests/zstdgpu_ci_tests.h new file mode 100644 index 0000000..aeec97c --- /dev/null +++ b/zstd/zstdgpu_ci_tests/zstdgpu_ci_tests.h @@ -0,0 +1,46 @@ +/** + * Copyright (c) Microsoft. All rights reserved. + * This code is licensed under the MIT License (MIT). + * THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF + * ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY + * IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR + * PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT. + */ + +// Shared header for the Zstd GPU CI tests. +// Exposes only what genuinely crosses translation-unit boundaries: +// the TestConfig struct, a global instance, and file discovery. +// The demo runner, DemoResult, and argument builders are internal +// to zstdgpu_ci_tests.cpp and stay hidden as static functions there. + +#pragma once + +#include <string> +#include <vector> + +// Test configuration — parsed from CLI in main(), read by tests. + +struct TestConfig +{ + std::string contentPath; // Directory containing .zst test files + std::string demoPath; // Full path to zstdgpu_demo.exe + std::string logDir; // Directory for logs, CSVs, and GTest XML output + std::string logFile; // Consolidated text log file path (--log-file) + int runCount = 40; // Number of iterations for performance tests + int timeoutSeconds = 0; // Max seconds before killing a demo process (0 = no timeout) + + // Cached list of .zst files discovered under contentPath. Populated once + // in main() after validation; consumed by GetTestFiles() at fixture + // instantiation. Avoids walking the tree twice. + std::vector<std::string> discoveredFiles; +}; + +// Global config, set once in main() before RUN_ALL_TESTS(), then read-only +// from test bodies. A plain extern global instead of a getter/setter is enough +// here — the "set once, then read" contract is enforced by the main() call +// sequence and there is no benefit to hiding the storage. +extern TestConfig g_testConfig; + +// File discovery — scans a directory for *.zst files. Returns sorted full paths. +// Called by main() during startup. +std::vector<std::string> DiscoverZstFiles(const std::string& contentPath); diff --git a/zstd/zstdgpu_ci_tests/zstdgpu_ci_tests.vcxproj b/zstd/zstdgpu_ci_tests/zstdgpu_ci_tests.vcxproj new file mode 100644 index 0000000..f756ce4 --- /dev/null +++ b/zstd/zstdgpu_ci_tests/zstdgpu_ci_tests.vcxproj @@ -0,0 +1,239 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <ItemGroup Label="ProjectConfigurations"> + <ProjectConfiguration Include="Debug|ARM64"> + <Configuration>Debug</Configuration> + <Platform>ARM64</Platform> + </ProjectConfiguration> + <ProjectConfiguration Include="Debug|Win32"> + <Configuration>Debug</Configuration> + <Platform>Win32</Platform> + </ProjectConfiguration> + <ProjectConfiguration Include="Debug|x64"> + <Configuration>Debug</Configuration> + <Platform>x64</Platform> + </ProjectConfiguration> + <ProjectConfiguration Include="Release|ARM64"> + <Configuration>Release</Configuration> + <Platform>ARM64</Platform> + </ProjectConfiguration> + <ProjectConfiguration Include="Release|Win32"> + <Configuration>Release</Configuration> + <Platform>Win32</Platform> + </ProjectConfiguration> + <ProjectConfiguration Include="Release|x64"> + <Configuration>Release</Configuration> + <Platform>x64</Platform> + </ProjectConfiguration> + </ItemGroup> + <PropertyGroup Label="Globals"> + <VCProjectVersion>16.0</VCProjectVersion> + <Keyword>Win32Proj</Keyword> + <ProjectGuid>{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}</ProjectGuid> + <RootNamespace>zstdgpu_ci_tests</RootNamespace> + <WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion> + </PropertyGroup> + <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" /> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration"> + <ConfigurationType>Application</ConfigurationType> + <UseDebugLibraries>true</UseDebugLibraries> + <PlatformToolset>v143</PlatformToolset> + <CharacterSet>Unicode</CharacterSet> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration"> + <ConfigurationType>Application</ConfigurationType> + <UseDebugLibraries>true</UseDebugLibraries> + <PlatformToolset>v143</PlatformToolset> + <CharacterSet>Unicode</CharacterSet> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" Label="Configuration"> + <ConfigurationType>Application</ConfigurationType> + <UseDebugLibraries>true</UseDebugLibraries> + <PlatformToolset>v143</PlatformToolset> + <CharacterSet>Unicode</CharacterSet> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration"> + <ConfigurationType>Application</ConfigurationType> + <UseDebugLibraries>false</UseDebugLibraries> + <PlatformToolset>v143</PlatformToolset> + <WholeProgramOptimization>true</WholeProgramOptimization> + <CharacterSet>Unicode</CharacterSet> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration"> + <ConfigurationType>Application</ConfigurationType> + <UseDebugLibraries>false</UseDebugLibraries> + <PlatformToolset>v143</PlatformToolset> + <WholeProgramOptimization>true</WholeProgramOptimization> + <CharacterSet>Unicode</CharacterSet> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="Configuration"> + <ConfigurationType>Application</ConfigurationType> + <UseDebugLibraries>false</UseDebugLibraries> + <PlatformToolset>v143</PlatformToolset> + <WholeProgramOptimization>true</WholeProgramOptimization> + <CharacterSet>Unicode</CharacterSet> + </PropertyGroup> + <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" /> + <ImportGroup Label="ExtensionSettings"> + </ImportGroup> + <ImportGroup Label="Shared"> + </ImportGroup> + <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> + <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> + </ImportGroup> + <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'"> + <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> + </ImportGroup> + <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'"> + <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> + </ImportGroup> + <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> + <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> + </ImportGroup> + <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'"> + <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> + </ImportGroup> + <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'"> + <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> + </ImportGroup> + <PropertyGroup Label="UserMacros" /> + <!-- Output to same directory structure as other projects --> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> + <OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir> + <IntDir>$(Platform)\$(Configuration)\</IntDir> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'"> + <OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir> + <IntDir>$(Platform)\$(Configuration)\</IntDir> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'"> + <OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir> + <IntDir>$(Platform)\$(Configuration)\</IntDir> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> + <OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir> + <IntDir>$(Platform)\$(Configuration)\</IntDir> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'"> + <OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir> + <IntDir>$(Platform)\$(Configuration)\</IntDir> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'"> + <OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir> + <IntDir>$(Platform)\$(Configuration)\</IntDir> + </PropertyGroup> + <!-- Compiler settings --> + <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> + <ClCompile> + <WarningLevel>Level3</WarningLevel> + <SDLCheck>true</SDLCheck> + <PreprocessorDefinitions>_DEBUG;_CONSOLE;WIN32_LEAN_AND_MEAN;NOMINMAX;%(PreprocessorDefinitions)</PreprocessorDefinitions> + <ConformanceMode>true</ConformanceMode> + <LanguageStandard>stdcpp17</LanguageStandard> + <AdditionalIncludeDirectories>..\ThirdParty\googletest\googletest\include</AdditionalIncludeDirectories> + </ClCompile> + <Link> + <SubSystem>Console</SubSystem> + <GenerateDebugInformation>true</GenerateDebugInformation> + </Link> + </ItemDefinitionGroup> + <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'"> + <ClCompile> + <WarningLevel>Level3</WarningLevel> + <SDLCheck>true</SDLCheck> + <PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;WIN32_LEAN_AND_MEAN;NOMINMAX;%(PreprocessorDefinitions)</PreprocessorDefinitions> + <ConformanceMode>true</ConformanceMode> + <LanguageStandard>stdcpp17</LanguageStandard> + <AdditionalIncludeDirectories>..\ThirdParty\googletest\googletest\include</AdditionalIncludeDirectories> + </ClCompile> + <Link> + <SubSystem>Console</SubSystem> + <GenerateDebugInformation>true</GenerateDebugInformation> + </Link> + </ItemDefinitionGroup> + <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'"> + <ClCompile> + <WarningLevel>Level3</WarningLevel> + <SDLCheck>true</SDLCheck> + <PreprocessorDefinitions>_DEBUG;_CONSOLE;WIN32_LEAN_AND_MEAN;NOMINMAX;%(PreprocessorDefinitions)</PreprocessorDefinitions> + <ConformanceMode>true</ConformanceMode> + <LanguageStandard>stdcpp17</LanguageStandard> + <AdditionalIncludeDirectories>..\ThirdParty\googletest\googletest\include</AdditionalIncludeDirectories> + </ClCompile> + <Link> + <SubSystem>Console</SubSystem> + <GenerateDebugInformation>true</GenerateDebugInformation> + </Link> + </ItemDefinitionGroup> + <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> + <ClCompile> + <WarningLevel>Level3</WarningLevel> + <FunctionLevelLinking>true</FunctionLevelLinking> + <IntrinsicFunctions>true</IntrinsicFunctions> + <SDLCheck>true</SDLCheck> + <PreprocessorDefinitions>NDEBUG;_CONSOLE;WIN32_LEAN_AND_MEAN;NOMINMAX;%(PreprocessorDefinitions)</PreprocessorDefinitions> + <ConformanceMode>true</ConformanceMode> + <LanguageStandard>stdcpp17</LanguageStandard> + <AdditionalIncludeDirectories>..\ThirdParty\googletest\googletest\include</AdditionalIncludeDirectories> + </ClCompile> + <Link> + <SubSystem>Console</SubSystem> + <EnableCOMDATFolding>true</EnableCOMDATFolding> + <OptimizeReferences>true</OptimizeReferences> + <GenerateDebugInformation>true</GenerateDebugInformation> + </Link> + </ItemDefinitionGroup> + <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'"> + <ClCompile> + <WarningLevel>Level3</WarningLevel> + <FunctionLevelLinking>true</FunctionLevelLinking> + <IntrinsicFunctions>true</IntrinsicFunctions> + <SDLCheck>true</SDLCheck> + <PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;WIN32_LEAN_AND_MEAN;NOMINMAX;%(PreprocessorDefinitions)</PreprocessorDefinitions> + <ConformanceMode>true</ConformanceMode> + <LanguageStandard>stdcpp17</LanguageStandard> + <AdditionalIncludeDirectories>..\ThirdParty\googletest\googletest\include</AdditionalIncludeDirectories> + </ClCompile> + <Link> + <SubSystem>Console</SubSystem> + <EnableCOMDATFolding>true</EnableCOMDATFolding> + <OptimizeReferences>true</OptimizeReferences> + <GenerateDebugInformation>true</GenerateDebugInformation> + </Link> + </ItemDefinitionGroup> + <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'"> + <ClCompile> + <WarningLevel>Level3</WarningLevel> + <FunctionLevelLinking>true</FunctionLevelLinking> + <IntrinsicFunctions>true</IntrinsicFunctions> + <SDLCheck>true</SDLCheck> + <PreprocessorDefinitions>NDEBUG;_CONSOLE;WIN32_LEAN_AND_MEAN;NOMINMAX;%(PreprocessorDefinitions)</PreprocessorDefinitions> + <ConformanceMode>true</ConformanceMode> + <LanguageStandard>stdcpp17</LanguageStandard> + <AdditionalIncludeDirectories>..\ThirdParty\googletest\googletest\include</AdditionalIncludeDirectories> + </ClCompile> + <Link> + <SubSystem>Console</SubSystem> + <EnableCOMDATFolding>true</EnableCOMDATFolding> + <OptimizeReferences>true</OptimizeReferences> + <GenerateDebugInformation>true</GenerateDebugInformation> + </Link> + </ItemDefinitionGroup> + <!-- Source files --> + <ItemGroup> + <ClCompile Include="main.cpp" /> + <ClCompile Include="zstdgpu_ci_tests.cpp" /> + </ItemGroup> + <ItemGroup> + <ClInclude Include="zstdgpu_ci_tests.h" /> + </ItemGroup> + <!-- Project references: only googletest --> + <ItemGroup> + <ProjectReference Include="..\ThirdParty\googletest_static.vcxproj"> + <Project>{49811f10-3d14-403e-859d-40dfcbb35c7b}</Project> + </ProjectReference> + </ItemGroup> + <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" /> + <ImportGroup Label="ExtensionTargets"> + </ImportGroup> +</Project> diff --git a/zstd/zstdgpu_demo/main.cpp b/zstd/zstdgpu_demo/main.cpp index 421cb4f..681519c 100644 --- a/zstd/zstdgpu_demo/main.cpp +++ b/zstd/zstdgpu_demo/main.cpp @@ -52,6 +52,16 @@ extern "C" { #include <assert.h> +// Redirect the shader library's ZSTDGPU_BREAK to a demo-local handler BEFORE the +// shader header expands the macro inline in this translation unit. The library's +// default __debugbreak() would kill the demo with STATUS_BREAKPOINT (0x80000003) +// on bad input, before g_correctnessFailureCount could propagate a clean non-zero +// exit. This override is local to main.cpp's TU — the library binary is unchanged +// and other consumers see the original behavior. Handler defined below alongside +// g_correctnessFailureCount. +static void zstdgpu_DemoOnBreak(const char* file, int line); +#define ZSTDGPU_BREAK() zstdgpu_DemoOnBreak(__FILE__, __LINE__) + #include "zstdgpu_reference_store.h" #include "zstdgpu_shaders.h" #include "zstdgpu.h" @@ -133,6 +143,17 @@ static void saveFile(const wchar_t *fileName, const void *data, uint32_t dataSiz } } +// Correctness-failure counter — wmain returns non-zero if > 0. +static uint32_t g_correctnessFailureCount = 0; + +// Handler for the ZSTDGPU_BREAK macro override at the top of this file. +static void zstdgpu_DemoOnBreak(const char* file, int line) +{ + debugPrint(L"[ZGBRK] %hs:%d\n", file, line); + ++g_correctnessFailureCount; +} + + /*********************************************************************************************************************** * * @@ -230,7 +251,10 @@ static void zstdgpu_Init_FinaliseSequenceOffsets_SRT(zstdgpu_FinaliseSequenceOff do \ { \ if (ZSTDGPU_ENUM_CONST(Validate_Success) != zstdgpu_ReferenceStore_Validate_##name) \ + { \ debugPrint(L"[FAIL] Validation of '"#name"' failed in function: " __FUNCTION__ ", file: " __FILE__ ", line: " STRINGIZE(__LINE__) "\n");\ + ++g_correctnessFailureCount; \ + } \ } \ while(0) @@ -238,7 +262,10 @@ static void zstdgpu_Init_FinaliseSequenceOffsets_SRT(zstdgpu_FinaliseSequenceOff do \ { \ if (!(cnd)) \ + { \ debugPrint(L"[FAIL] Validation of '"#cnd"' failed in function: " __FUNCTION__ ", file: " __FILE__ ", line: " STRINGIZE(__LINE__) "\n");\ + ++g_correctnessFailureCount; \ + } \ } \ while(0) @@ -837,6 +864,16 @@ static void zstdgpu_Validate_GpuDecompressOnCpu(zstdgpu_ResourceDataCpu & zstdCp VALIDATE(DecompressedLiterals(&zstdCpu)); } + // If any upstream VALIDATE failed (parse, FseTables, etc.), sentinel FSE-table + // indices (0x3FFFFFFE / 0x3FFFFFFF) may have propagated to inoutSeqRefs. Feeding + // them into DecompressSequences would either OOB-index inFseInfos (AV) or trip + // an internal ZSTDGPU_BREAK. Stop here instead. + if (g_correctnessFailureCount > 0) + { + debugPrint(L"[FAIL] Skipping DecompressSequences and downstream stages: %u prior validation failure(s).\n", g_correctnessFailureCount); + return; + } + { zstdgpu_DecompressSequences_SRT srt; zstdgpu_Init_DecompressSequences_SRT(srt, zstdCpu); @@ -1215,6 +1252,7 @@ int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE, _In_ LPWSTR lp if (fbInfo.frameCount != vcnt) { debugPrint(L"[FAIL] Some frames don't carry uncompressed size. Early Out.\n"); + ++g_correctnessFailureCount; free(zstdOutFrameRefs); free(zstdInFrameRefs); @@ -1585,6 +1623,7 @@ int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE, _In_ LPWSTR lp if (failedFrameCount > 0) { + g_correctnessFailureCount += failedFrameCount; const char *ref = (char*)zstdReferenceUncompressedData; const char *tst = (char*)zstdUnCompressedFramesMemory.bufMem[0]; @@ -1840,5 +1879,6 @@ int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE, _In_ LPWSTR lp device->SetStablePowerState(FALSE); zstdgpu_Demo_PlatformTerm(device); debugPrint(L"Finished.\n"); - return 0; + + return g_correctnessFailureCount > 0 ? 2 : 0; }