diff --git a/.github/scripts/test-linux-software.sh b/.github/scripts/test-linux-software.sh new file mode 100755 index 0000000..c7d542a --- /dev/null +++ b/.github/scripts/test-linux-software.sh @@ -0,0 +1,151 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" +RESULTS_DIR="${RESULTS_DIR:-$ROOT_DIR/test-results/linux-software}" +WESTON_BINARY="${WESTON_BINARY:-weston}" +WESTON_SHELL="${WESTON_SHELL:-kiosk-shell.so}" +mkdir -p "$RESULTS_DIR" +RESULTS_DIR="$(realpath "$RESULTS_DIR")" +for tool in "$WESTON_BINARY" vulkaninfo python3 timeout; do + command -v "$tool" >/dev/null || { echo "Missing test dependency: $tool" >&2; exit 2; } +done + +# Select the CPU ICD explicitly. Never require /dev/dri or a physical HDR display. +icd="" +for candidate in /usr/share/vulkan/icd.d/lvp_icd.json \ + "/usr/share/vulkan/icd.d/lvp_icd.$(uname -m).json"; do + if [[ -f "$candidate" ]]; then icd="$candidate"; break; fi +done +[[ -n "$icd" ]] || { echo 'Mesa lavapipe ICD not installed' >&2; exit 2; } +export VK_DRIVER_FILES="$icd" VK_ICD_FILENAMES="$icd" +export LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe LP_NUM_THREADS=2 +unset DISPLAY WAYLAND_DISPLAY WAYLAND_SOCKET +unset VK_INSTANCE_LAYERS VK_LAYER_PATH VK_ADD_LAYER_PATH VK_LOADER_LAYERS_ENABLE +unset ANGLE_FEATURE_OVERRIDES_ENABLED ANGLE_FEATURE_OVERRIDES_DISABLED + +runtime="$(mktemp -d "${TMPDIR:-/tmp}/angle-software.XXXXXX")" +export XDG_RUNTIME_DIR="$runtime" XDG_CACHE_HOME="$runtime/cache" +mkdir -p "$XDG_CACHE_HOME" +weston_pid="" +stop_weston() { + if [[ -n "$weston_pid" ]]; then + kill "$weston_pid" 2>/dev/null || true + for ((attempt=0; attempt<50; ++attempt)); do + kill -0 "$weston_pid" 2>/dev/null || break + sleep 0.1 + done + if kill -0 "$weston_pid" 2>/dev/null; then kill -KILL "$weston_pid" 2>/dev/null || true; fi + wait "$weston_pid" 2>/dev/null || true + weston_pid="" + fi +} +cleanup() { + stop_weston + rm -rf -- "$runtime" +} +trap cleanup EXIT +trap 'exit 130' INT +trap 'exit 143' TERM +report() { + printf '%s\n' "$*" | tee -a "$RESULTS_DIR/summary.txt" + if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then printf '%s\n\n' "$*" >> "$GITHUB_STEP_SUMMARY"; fi +} +start_weston() { + local mode="$1" color_management="$2" + export WAYLAND_DISPLAY="angle-$mode" + cat > "$runtime/weston-$mode.ini" < "$RESULTS_DIR/weston-$mode.stderr" 2>&1 & + weston_pid=$! + for ((attempt=0; attempt<100; ++attempt)); do + if ! kill -0 "$weston_pid" 2>/dev/null; then + cat "$RESULTS_DIR/weston-$mode.log" "$RESULTS_DIR/weston-$mode.stderr" >&2 + return 1 + fi + [[ ! -S "$runtime/$WAYLAND_DISPLAY" ]] || return 0 + sleep 0.1 + done + echo 'Headless Weston did not become ready' >&2 + return 1 +} +run_probe() { + local mode="$1" + shift + HDR_AUDIT=1 HDR_AUDIT_LOG="$RESULTS_DIR/$mode-audit.log" \ + WAYLAND_DEBUG=client bash "$ROOT_DIR/scripts/test-linux-hdr.sh" "$@" \ + > "$RESULTS_DIR/$mode-probe.log" 2>&1 +} + +: > "$RESULTS_DIR/summary.txt" +"$WESTON_BINARY" --version | tee "$RESULTS_DIR/weston-version.txt" +report 'Physical HDR output: NOT TESTED (CPU rendering and headless compositor).' +start_weston sdr false +timeout 30 vulkaninfo --summary > "$RESULTS_DIR/vulkan-summary.txt" 2>&1 +grep -q 'PHYSICAL_DEVICE_TYPE_CPU' "$RESULTS_DIR/vulkan-summary.txt" || { + echo 'The test did not select a CPU Vulkan device' >&2; exit 1; +} + +# Requiring HDR on a genuinely SDR-only compositor must report unavailable. +status=0 +run_probe sdr --require-hdr || status=$? +if [[ "$status" != 77 ]]; then + cat "$RESULTS_DIR/sdr-probe.log" >&2 + echo "Expected SDR-only exit 77, got $status" >&2 + exit 1 +fi +grep -q 'LINUX_HDR_SUMMARY hdr_pass=0 hdr_skip=2 sdr=PASS' "$RESULTS_DIR/sdr-probe.log" +grep -q 'AUDIT_SUMMARY phases=3 hdr_phases=0' "$RESULTS_DIR/sdr-probe.log" +report 'PASS: SDR rendering, invalid colorspace rejection, and explicit HDR unavailability.' +stop_weston + +# Older distribution compositors need not support the stable color protocol. +version="$(sed -nE 's/^weston ([0-9]+)\..*/\1/p' "$RESULTS_DIR/weston-version.txt")" +[[ "$version" =~ ^[0-9]+$ ]] || { echo 'Cannot determine Weston version' >&2; exit 1; } +if (( version < 15 )); then + report 'SKIP: HDR presentation requires a Weston version with stable color management.' + exit 0 +fi +start_weston hdr true + +# Determine expectations independently of ANGLE. An ANGLE regression must fail, +# not turn into a successful skip just because EGL stopped advertising HDR. +timeout 30 vulkaninfo > "$RESULTS_DIR/vulkan-wayland.txt" 2>&1 +python3 - "$RESULTS_DIR/vulkan-wayland.txt" > "$RESULTS_DIR/hdr-requirements.txt" <<'PY' +import pathlib, re, sys +text = pathlib.Path(sys.argv[1]).read_text(errors='replace') +surfaces = re.findall(r'Surface type = (\S+)', text) +if not surfaces or any(surface != 'VK_KHR_wayland_surface' for surface in surfaces): + raise SystemExit('Expected an isolated Vulkan Wayland surface query') +pairs = set(re.findall(r'format = (\S+)\s+colorSpace = (\S+)', text)) +requirements = [] +if ('FORMAT_R16G16B16A16_SFLOAT', 'COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT') in pairs: + requirements.append('--require-scrgb') +if any((fmt, 'COLOR_SPACE_HDR10_ST2084_EXT') in pairs for fmt in ( + 'FORMAT_A2B10G10R10_UNORM_PACK32', 'FORMAT_A2R10G10B10_UNORM_PACK32')): + requirements.append('--require-hdr10') +print(' '.join(requirements)) +PY +read -r -a requirements < "$RESULTS_DIR/hdr-requirements.txt" +if (( ${#requirements[@]} == 0 )); then + report 'SKIP: the software Vulkan/Wayland stack advertises no usable scRGB or HDR10 pair.' + exit 0 +fi +status=0 +run_probe hdr "${requirements[@]}" || status=$? +if [[ "$status" != 0 ]]; then + cat "$RESULTS_DIR/hdr-probe.log" >&2 + echo "Advertised software HDR presentation failed: exit $status" >&2 + exit 1 +fi +grep -E '^(PHASE_PASS|LINUX_HDR_SUMMARY|AUDIT_SUMMARY)' "$RESULTS_DIR/hdr-probe.log" +report "PASS: advertised software HDR modes (${requirements[*]}), native-buffer readback, HDR-to-SDR transitions, and Vulkan presentation audit." diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dd75bc5..6a539dd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,12 +3,14 @@ name: angle-natives-build on: push: branches: [master] + pull_request: + branches: [master] + workflow_dispatch: release: types: [published] permissions: contents: read - packages: write env: LINUX_CONTAINER_IMAGE: registry.gitlab.steamos.cloud/steamrt/sniper/sdk @@ -164,8 +166,90 @@ jobs: path: angle-artifacts/${{ matrix.classifier }} if-no-files-found: error + test-linux-logic: + needs: resolve + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Checkout pinned ANGLE sources without build dependencies + uses: actions/checkout@v4 + with: + repository: google/angle + ref: ${{ needs.resolve.outputs.ANGLE_COMMIT }} + path: build/angle + persist-credentials: false + + - name: Check patches and enumeration logic + shell: bash + run: | + set -euo pipefail + for patch in patches/*.patch; do + git -C build/angle apply --check "$GITHUB_WORKSPACE/$patch" + git -C build/angle apply "$GITHUB_WORKSPACE/$patch" + done + python3 tests/test-linux-hdr-logic.py + python3 -m py_compile scripts/verify-linux-hdr-audit.py + bash -n scripts/test-linux-hdr.sh + bash -n scripts/build-hdr-audit-layer.sh + bash -n .github/scripts/test-linux-software.sh + + test-linux-software: + needs: [resolve, build-linux] + runs-on: ubuntu-latest + timeout-minutes: 15 + container: + image: ubuntu:26.04 + defaults: + run: + shell: bash + env: + DEBIAN_FRONTEND: noninteractive + ANGLE_DIR: ${{ github.workspace }}/build/angle + ANGLE_NATIVE_DIR: ${{ github.workspace }}/angle-artifacts/natives-linux/native/angle/linux/x86_64 + steps: + - name: Install software rendering and test dependencies + run: | + apt-get update + apt-get install -y --no-install-recommends \ + ca-certificates git build-essential python3 pkg-config \ + libwayland-dev wayland-protocols libvulkan-dev vulkan-tools \ + libegl-mesa0 libgl1-mesa-dri mesa-vulkan-drivers weston + + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Checkout pinned public EGL and GLES headers + uses: actions/checkout@v4 + with: + repository: google/angle + ref: ${{ needs.resolve.outputs.ANGLE_COMMIT }} + path: build/angle + persist-credentials: false + + - name: Download the Linux libraries built by this run + uses: actions/download-artifact@v4 + with: + name: natives-linux + path: angle-artifacts/natives-linux + + - name: Test Wayland presentation with lavapipe and headless Weston + run: bash .github/scripts/test-linux-software.sh + + - name: Upload software test logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: linux-software-test-logs + path: test-results/linux-software/ + if-no-files-found: warn + package: - needs: [resolve, build-linux, build-windows, build-macos] + needs: [resolve, build-linux, build-windows, build-macos, test-linux-logic, test-linux-software] runs-on: ubuntu-latest container: image: registry.gitlab.steamos.cloud/steamrt/sniper/sdk @@ -204,6 +288,10 @@ jobs: if-no-files-found: error publish: + if: >- + github.repository == 'NostrGameEngine/angle-builder' && + (github.event_name == 'release' || + (github.event_name == 'push' && github.ref == 'refs/heads/master')) needs: [resolve, package] runs-on: ubuntu-latest container: diff --git a/.gitignore b/.gitignore index de42f40..fa1852f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ dist generated .cache x.git -.gradle \ No newline at end of file +.gradle +test-results/ diff --git a/patches/angle-linux-wayland-hdr.patch b/patches/angle-linux-wayland-hdr.patch new file mode 100644 index 0000000..3997c34 --- /dev/null +++ b/patches/angle-linux-wayland-hdr.patch @@ -0,0 +1,599 @@ +diff --git a/BUILD.gn b/BUILD.gn +index 66aa7ab..fece7b8 100644 +--- a/BUILD.gn ++++ b/BUILD.gn +@@ -1240,29 +1240,25 @@ angle_source_set("angle_gl_enum_utils") { + } + + if (angle_use_wayland) { +- config("angle_wayland_config") { +- if ("$wayland_gn_dir" == "") { +- libs = [ +- "wayland-client", +- "wayland-egl", +- ] ++ if (angle_use_system_wayland) { ++ import("//build/config/linux/pkg_config.gni") ++ pkg_config("angle_wayland_config") { ++ packages = [ "wayland-client", "wayland-egl" ] ++ extra_args = [ "wayland-client >= 1.18.0" ] ++ } ++ } else { ++ config("angle_wayland_config") { ++ if ("$wayland_gn_dir" == "") { ++ libs = [ "wayland-client", "wayland-egl" ] ++ } ++ include_dirs = [ "$wayland_dir/egl", "$wayland_dir" ] + } +- +- include_dirs = [ +- "$wayland_dir/egl", +- "$wayland_dir", +- ] + } + + group("angle_wayland") { + public_configs = [ ":angle_wayland_config" ] +- +- if ("$wayland_gn_dir" != "") { +- # Use third-party targets +- public_deps = [ +- "$wayland_gn_dir:wayland_client", +- "$wayland_gn_dir:wayland_egl", +- ] ++ if (!angle_use_system_wayland && "$wayland_gn_dir" != "") { ++ public_deps = [ "$wayland_gn_dir:wayland_client", "$wayland_gn_dir:wayland_egl" ] + } + } + } +diff --git a/gni/angle.gni b/gni/angle.gni +index bde26da..12803d2 100644 +--- a/gni/angle.gni ++++ b/gni/angle.gni +@@ -20,6 +20,10 @@ if (!defined(checkout_angle_partition_alloc)) { + } + + declare_args() { ++ # Use the same shared Wayland client ABI as external EGL callers (SDL/GLFW). ++ # Chromium can keep its internally consistent bundled Wayland by default. ++ angle_use_system_wayland = false ++ + # Extract native libs in ANGLE apk. Useful for flamegraph generation. + angle_extract_native_libs = false + +diff --git a/src/libANGLE/renderer/vulkan/BUILD.gn b/src/libANGLE/renderer/vulkan/BUILD.gn +index 4eef711..0587f2e 100644 +--- a/src/libANGLE/renderer/vulkan/BUILD.gn ++++ b/src/libANGLE/renderer/vulkan/BUILD.gn +@@ -176,11 +176,13 @@ template("angle_vulkan_backend_template") { + ] + + if (angle_use_wayland) { +- public_configs += [ "$angle_root:angle_wayland_config" ] +- deps += [ +- "$wayland_gn_dir:wayland_client", +- "$wayland_gn_dir:wayland_egl", +- ] ++ deps += [ "$angle_root:angle_wayland" ] ++ # The stable wl_egl_window backend ABI header is not installed by some ++ # distros. Import only this header directory, never a private client lib. ++ if (!defined(include_dirs)) { ++ include_dirs = [] ++ } ++ include_dirs += [ "$wayland_dir/egl" ] + } + + data_deps = [] +diff --git a/src/libANGLE/renderer/vulkan/DisplayVk.cpp b/src/libANGLE/renderer/vulkan/DisplayVk.cpp +index 072ab0b..5f07f11 100644 +--- a/src/libANGLE/renderer/vulkan/DisplayVk.cpp ++++ b/src/libANGLE/renderer/vulkan/DisplayVk.cpp +@@ -32,65 +32,86 @@ namespace rx + namespace + { + // Query surface format and colorspace support. +-void GetSupportedFormatColorspaces(VkPhysicalDevice physicalDevice, +- const angle::FeaturesVk &featuresVk, +- VkSurfaceKHR surface, +- std::vector *surfaceFormatsOut) ++VkResult GetSupportedFormatColorspaces(VkPhysicalDevice physicalDevice, ++ const angle::FeaturesVk &featuresVk, ++ VkSurfaceKHR surface, ++ std::vector *surfaceFormatsOut) + { + ASSERT(surfaceFormatsOut); + surfaceFormatsOut->clear(); +- +- constexpr VkSurfaceFormat2KHR kSurfaceFormat2Initializer = { +- VK_STRUCTURE_TYPE_SURFACE_FORMAT_2_KHR, +- nullptr, ++ constexpr VkSurfaceFormat2KHR kInitializer = { ++ VK_STRUCTURE_TYPE_SURFACE_FORMAT_2_KHR, nullptr, + {VK_FORMAT_UNDEFINED, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}}; + +- if (featuresVk.supportsSurfaceCapabilities2Extension.enabled) +- { +- VkPhysicalDeviceSurfaceInfo2KHR surfaceInfo2 = {}; +- surfaceInfo2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SURFACE_INFO_2_KHR; +- surfaceInfo2.surface = surface; +- uint32_t surfaceFormatCount = 0; +- +- // Query the count first +- VkResult result = VK_CALL(vkGetPhysicalDeviceSurfaceFormats2KHR, physicalDevice, +- &surfaceInfo2, &surfaceFormatCount, nullptr); +- ASSERT(result == VK_SUCCESS); +- ASSERT(surfaceFormatCount > 0); +- +- // Query the VkSurfaceFormat2KHR list +- std::vector surfaceFormats2(surfaceFormatCount, +- kSurfaceFormat2Initializer); +- result = VK_CALL(vkGetPhysicalDeviceSurfaceFormats2KHR, physicalDevice, &surfaceInfo2, +- &surfaceFormatCount, surfaceFormats2.data()); +- ASSERT(result == VK_SUCCESS); +- +- *surfaceFormatsOut = std::move(surfaceFormats2); +- } +- else ++ // A display change can grow the list between the count and data queries. Retry ++ // VK_INCOMPLETE, but do not loop indefinitely on a misbehaving driver. ++ for (int attempt = 0; attempt < 4; ++attempt) + { +- uint32_t surfaceFormatCount = 0; +- // Query the count first +- VkResult result = VK_CALL(vkGetPhysicalDeviceSurfaceFormatsKHR, physicalDevice, surface, +- &surfaceFormatCount, nullptr); +- ASSERT(result == VK_SUCCESS); +- +- // Query the VkSurfaceFormatKHR list +- std::vector surfaceFormats(surfaceFormatCount); +- result = VK_CALL(vkGetPhysicalDeviceSurfaceFormatsKHR, physicalDevice, surface, +- &surfaceFormatCount, surfaceFormats.data()); +- ASSERT(result == VK_SUCCESS); +- +- // Copy over data from std::vector to std::vector +- std::vector surfaceFormats2(surfaceFormatCount, +- kSurfaceFormat2Initializer); +- for (size_t index = 0; index < surfaceFormatCount; index++) ++ uint32_t count = 0; ++ VkResult result; ++ if (featuresVk.supportsSurfaceCapabilities2Extension.enabled) + { +- surfaceFormats2[index].surfaceFormat.format = surfaceFormats[index].format; ++ VkPhysicalDeviceSurfaceInfo2KHR info = {}; ++ info.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SURFACE_INFO_2_KHR; ++ info.surface = surface; ++ result = VK_CALL(vkGetPhysicalDeviceSurfaceFormats2KHR, physicalDevice, &info, ++ &count, nullptr); ++ if (result != VK_SUCCESS) ++ { ++ return result; ++ } ++ if (count == 0) ++ { ++ return VK_SUCCESS; ++ } ++ std::vector formats(count, kInitializer); ++ result = VK_CALL(vkGetPhysicalDeviceSurfaceFormats2KHR, physicalDevice, &info, ++ &count, formats.data()); ++ if (result == VK_INCOMPLETE) ++ { ++ continue; ++ } ++ if (result != VK_SUCCESS) ++ { ++ return result; ++ } ++ formats.resize(count); ++ *surfaceFormatsOut = std::move(formats); + } +- +- *surfaceFormatsOut = std::move(surfaceFormats2); ++ else ++ { ++ result = VK_CALL(vkGetPhysicalDeviceSurfaceFormatsKHR, physicalDevice, surface, ++ &count, nullptr); ++ if (result != VK_SUCCESS) ++ { ++ return result; ++ } ++ if (count == 0) ++ { ++ return VK_SUCCESS; ++ } ++ std::vector formats(count); ++ result = VK_CALL(vkGetPhysicalDeviceSurfaceFormatsKHR, physicalDevice, surface, ++ &count, formats.data()); ++ if (result == VK_INCOMPLETE) ++ { ++ continue; ++ } ++ if (result != VK_SUCCESS) ++ { ++ return result; ++ } ++ std::vector formats2(count, kInitializer); ++ for (size_t index = 0; index < count; ++index) ++ { ++ // Preserve the complete pair, including HDR colorSpace values. ++ formats2[index].surfaceFormat = formats[index]; ++ } ++ *surfaceFormatsOut = std::move(formats2); ++ } ++ return VK_SUCCESS; + } ++ return VK_INCOMPLETE; + } + + vk::UseDebugLayers ShouldLoadDebugLayers(const egl::AttributeMap &attribs) +@@ -321,15 +342,14 @@ ShareGroupImpl *DisplayVk::createShareGroup(const egl::ShareGroupState &state) + + bool DisplayVk::isConfigFormatSupported(VkFormat format) const + { +- // Requires VK_GOOGLE_surfaceless_query extension to be supported. +- ASSERT(mRenderer->getFeatures().supportsSurfacelessQueryExtension.enabled); +- ++ // The cache may come from VK_GOOGLE_surfaceless_query or a native WSI probe. + // A format is considered supported if it is supported in atleast 1 colorspace. + using ColorspaceFormatSetItem = + const std::pair>; + for (ColorspaceFormatSetItem &colorspaceFormatSetItem : mSupportedColorspaceFormatsMap) + { +- if (colorspaceFormatSetItem.second.count(format) > 0) ++ if (colorspaceFormatSetItem.second.count(format) > 0 || ++ colorspaceFormatSetItem.second.count(VK_FORMAT_UNDEFINED) > 0) + { + return true; + } +@@ -342,37 +362,31 @@ bool DisplayVk::isSurfaceFormatColorspacePairSupported(VkSurfaceKHR surface, + VkFormat format, + VkColorSpaceKHR colorspace) const + { +- if (mSupportedColorspaceFormatsMap.size() > 0) ++ // A temporary native surface is suitable for discovering EGL capabilities, ++ // but it must never authorize a format/colorspace on a different real window. ++ if (!mSurfaceFormatsAreSurfaceSpecific && !mSupportedColorspaceFormatsMap.empty()) + { +- return mSupportedColorspaceFormatsMap.count(colorspace) > 0 && +- mSupportedColorspaceFormatsMap.at(colorspace).count(format) > 0; ++ auto entry = mSupportedColorspaceFormatsMap.find(colorspace); ++ return entry != mSupportedColorspaceFormatsMap.end() && ++ (entry->second.count(format) > 0 || ++ entry->second.count(VK_FORMAT_UNDEFINED) > 0); + } +- else +- { +- const angle::FeaturesVk &featuresVk = mRenderer->getFeatures(); +- std::vector surfaceFormats; +- GetSupportedFormatColorspaces(mRenderer->getPhysicalDevice(), featuresVk, surface, +- &surfaceFormats); + +- if (!featuresVk.supportsSurfaceCapabilities2Extension.enabled) +- { +- if (surfaceFormats.size() == 1u && +- surfaceFormats[0].surfaceFormat.format == VK_FORMAT_UNDEFINED) +- { +- return true; +- } +- } +- +- for (const VkSurfaceFormat2KHR &surfaceFormat : surfaceFormats) ++ std::vector formats; ++ if (GetSupportedFormatColorspaces(mRenderer->getPhysicalDevice(), mRenderer->getFeatures(), ++ surface, &formats) != VK_SUCCESS) ++ { ++ return false; ++ } ++ for (const VkSurfaceFormat2KHR &entry : formats) ++ { ++ const VkSurfaceFormatKHR &pair = entry.surfaceFormat; ++ if (pair.colorSpace == colorspace && ++ (pair.format == format || pair.format == VK_FORMAT_UNDEFINED)) + { +- if (surfaceFormat.surfaceFormat.format == format && +- surfaceFormat.surfaceFormat.colorSpace == colorspace) +- { +- return true; +- } ++ return true; + } + } +- + return false; + } + +@@ -381,33 +395,36 @@ bool DisplayVk::isColorspaceSupported(VkColorSpaceKHR colorspace) const + return mSupportedColorspaceFormatsMap.count(colorspace) > 0; + } + +-void DisplayVk::initSupportedSurfaceFormatColorspaces() ++bool DisplayVk::cacheSupportedSurfaceFormatColorspaces(VkSurfaceKHR surface) + { +- const angle::FeaturesVk &featuresVk = mRenderer->getFeatures(); +- if (featuresVk.supportsSurfacelessQueryExtension.enabled && +- featuresVk.supportsSurfaceCapabilities2Extension.enabled) ++ std::vector formats; ++ VkResult result = GetSupportedFormatColorspaces(mRenderer->getPhysicalDevice(), ++ mRenderer->getFeatures(), surface, &formats); ++ if (result != VK_SUCCESS || formats.empty()) + { +- // Use the VK_GOOGLE_surfaceless_query extension to query supported surface formats and +- // colorspaces by using a VK_NULL_HANDLE for the VkSurfaceKHR handle. +- std::vector surfaceFormats; +- GetSupportedFormatColorspaces(mRenderer->getPhysicalDevice(), featuresVk, VK_NULL_HANDLE, +- &surfaceFormats); +- for (const VkSurfaceFormat2KHR &surfaceFormat : surfaceFormats) +- { +- // Cache supported VkFormat and VkColorSpaceKHR for later use +- VkFormat format = surfaceFormat.surfaceFormat.format; +- VkColorSpaceKHR colorspace = surfaceFormat.surfaceFormat.colorSpace; +- +- ASSERT(format != VK_FORMAT_UNDEFINED); +- +- mSupportedColorspaceFormatsMap[colorspace].insert(format); +- } +- +- ASSERT(mSupportedColorspaceFormatsMap.size() > 0); ++ WARN() << "Unable to query Vulkan surface formats: " << result; ++ return false; + } +- else ++ mSupportedColorspaceFormatsMap.clear(); ++ for (const VkSurfaceFormat2KHR &entry : formats) ++ { ++ mSupportedColorspaceFormatsMap[entry.surfaceFormat.colorSpace].insert( ++ entry.surfaceFormat.format); ++ } ++ mSurfaceFormatsAreSurfaceSpecific = surface != VK_NULL_HANDLE; ++ return true; ++} ++ ++void DisplayVk::initSupportedSurfaceFormatColorspaces() ++{ ++ mSupportedColorspaceFormatsMap.clear(); ++ mSurfaceFormatsAreSurfaceSpecific = false; ++ const angle::FeaturesVk &features = mRenderer->getFeatures(); ++ if (features.supportsSurfacelessQueryExtension.enabled && ++ features.supportsSurfaceCapabilities2Extension.enabled) + { +- mSupportedColorspaceFormatsMap.clear(); ++ // A null surface is legal only when VK_GOOGLE_surfaceless_query is enabled. ++ cacheSupportedSurfaceFormatColorspaces(VK_NULL_HANDLE); + } + } + +diff --git a/src/libANGLE/renderer/vulkan/DisplayVk.h b/src/libANGLE/renderer/vulkan/DisplayVk.h +index e688eef..e14c30d 100644 +--- a/src/libANGLE/renderer/vulkan/DisplayVk.h ++++ b/src/libANGLE/renderer/vulkan/DisplayVk.h +@@ -131,6 +131,15 @@ class DisplayVk : public DisplayImpl, public vk::GlobalOps, public ThreadSafeDis + ThreadSafeDisplayImpl *getThreadSafeDisplayImpl() override { return this; } + + protected: ++ virtual void initSupportedSurfaceFormatColorspaces(); ++ bool cacheSupportedSurfaceFormatColorspaces(VkSurfaceKHR surface); ++ void clearSupportedSurfaceFormatColorspaces() ++ { ++ mSupportedColorspaceFormatsMap.clear(); ++ mSurfaceFormatsAreSurfaceSpecific = false; ++ } ++ bool hasSupportedSurfaceFormats() const { return !mSupportedColorspaceFormatsMap.empty(); } ++ + void generateExtensions(egl::DisplayExtensions *outExtensions) const override; + + private: +@@ -141,7 +150,6 @@ class DisplayVk : public DisplayImpl, public vk::GlobalOps, public ThreadSafeDis + virtual angle::Result waitNativeImpl(); + + bool isColorspaceSupported(VkColorSpaceKHR colorspace) const; +- void initSupportedSurfaceFormatColorspaces(); + + // vk::GlobalOps + void putBlob(const angle::BlobCacheKey &key, const angle::MemoryBuffer &value) override; +@@ -153,6 +161,9 @@ class DisplayVk : public DisplayImpl, public vk::GlobalOps, public ThreadSafeDis + + angle::ScratchBuffer mScratchBuffer; + ++ // A real-surface probe describes only that surface; re-query application windows. ++ bool mSurfaceFormatsAreSurfaceSpecific = false; ++ + // Map of supported colorspace and associated surface format set. + angle::HashMap> mSupportedColorspaceFormatsMap; + }; +diff --git a/src/libANGLE/renderer/vulkan/linux/wayland/DisplayVkWayland.cpp b/src/libANGLE/renderer/vulkan/linux/wayland/DisplayVkWayland.cpp +index ac737b4..7dc906a 100644 +--- a/src/libANGLE/renderer/vulkan/linux/wayland/DisplayVkWayland.cpp ++++ b/src/libANGLE/renderer/vulkan/linux/wayland/DisplayVkWayland.cpp +@@ -12,6 +12,9 @@ + + #include + ++#include ++#include ++ + #include "common/angleutils.h" + #include "common/linux/dma_buf_utils.h" + #include "common/system_utils.h" +@@ -23,6 +26,107 @@ + namespace rx + { + ++namespace ++{ ++// Keep registry dispatch on a private queue. EGL initialization must not dispatch ++// the application's default queue (SDL, GLFW or another toolkit owns it). ++class ScopedWaylandFormatQuery ++{ ++ public: ++ explicit ScopedWaylandFormatQuery(wl_display *display) : mDisplay(display) {} ++ ++ ~ScopedWaylandFormatQuery() ++ { ++ if (mVkSurface != VK_NULL_HANDLE) ++ { ++ VK_CALL(vkDestroySurfaceKHR, mInstance, mVkSurface, nullptr); ++ } ++ if (mSurface) ++ { ++ wl_surface_destroy(mSurface); ++ } ++ if (mCompositor) ++ { ++ wl_compositor_destroy(mCompositor); ++ } ++ if (mRegistry) ++ { ++ wl_registry_destroy(mRegistry); ++ } ++ if (mWrapper) ++ { ++ wl_proxy_wrapper_destroy(mWrapper); ++ } ++ if (mQueue) ++ { ++ wl_event_queue_destroy(mQueue); ++ } ++ } ++ ++ bool initialize(VkInstance instance) ++ { ++ mInstance = instance; ++ mQueue = wl_display_create_queue(mDisplay); ++ if (!mQueue) ++ { ++ return false; ++ } ++ mWrapper = static_cast(wl_proxy_create_wrapper(mDisplay)); ++ if (!mWrapper) ++ { ++ return false; ++ } ++ wl_proxy_set_queue(reinterpret_cast(mWrapper), mQueue); ++ mRegistry = wl_display_get_registry(mWrapper); ++ if (!mRegistry) ++ { ++ return false; ++ } ++ static const wl_registry_listener listener = {HandleGlobal, HandleGlobalRemove}; ++ if (wl_registry_add_listener(mRegistry, &listener, this) < 0 || ++ wl_display_roundtrip_queue(mDisplay, mQueue) < 0 || !mCompositor) ++ { ++ return false; ++ } ++ mSurface = wl_compositor_create_surface(mCompositor); ++ if (!mSurface) ++ { ++ return false; ++ } ++ VkWaylandSurfaceCreateInfoKHR info = {}; ++ info.sType = VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR; ++ info.display = mDisplay; ++ info.surface = mSurface; ++ return VK_CALL(vkCreateWaylandSurfaceKHR, mInstance, &info, nullptr, &mVkSurface) == ++ VK_SUCCESS; ++ } ++ ++ VkSurfaceKHR get() const { return mVkSurface; } ++ ++ private: ++ static void HandleGlobal(void *data, wl_registry *registry, uint32_t name, ++ const char *interface, uint32_t version) ++ { ++ auto *self = static_cast(data); ++ if (!self->mCompositor && std::strcmp(interface, wl_compositor_interface.name) == 0) ++ { ++ self->mCompositor = static_cast( ++ wl_registry_bind(registry, name, &wl_compositor_interface, std::min(version, 4u))); ++ } ++ } ++ static void HandleGlobalRemove(void *, wl_registry *, uint32_t) {} ++ ++ wl_display *mDisplay; ++ wl_display *mWrapper = nullptr; ++ wl_event_queue *mQueue = nullptr; ++ wl_registry *mRegistry = nullptr; ++ wl_compositor *mCompositor = nullptr; ++ wl_surface *mSurface = nullptr; ++ VkInstance mInstance = VK_NULL_HANDLE; ++ VkSurfaceKHR mVkSurface = VK_NULL_HANDLE; ++}; ++} // namespace ++ + DisplayVkWayland::DisplayVkWayland(const egl::DisplayState &state) + : DisplayVkLinux(state), mOwnDisplay(false), mWaylandDisplay(nullptr) + {} +@@ -86,7 +190,21 @@ SurfaceImpl *DisplayVkWayland::createWindowSurfaceVk(const egl::SurfaceState &st + + egl::ConfigSet DisplayVkWayland::generateConfigs() + { +- const std::array kColorFormats = {GL_BGRA8_EXT}; ++ // Retain the existing SDR config. HDR candidates also need real Vulkan ++ // attachment/transfer support; WSI support is checked separately below. ++ std::vector kColorFormats = {GL_BGRA8_EXT}; ++ for (GLenum candidate : {GL_RGB10_A2_EXT, GL_RGBA16F_EXT}) ++ { ++ const vk::Format &format = mRenderer->getFormat(candidate); ++ if (mRenderer->hasImageFormatFeatureBits( ++ format.getActualRenderableImageFormatID(), ++ VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT | ++ VK_FORMAT_FEATURE_BLIT_SRC_BIT | VK_FORMAT_FEATURE_BLIT_DST_BIT | ++ VK_FORMAT_FEATURE_TRANSFER_SRC_BIT | VK_FORMAT_FEATURE_TRANSFER_DST_BIT)) ++ { ++ kColorFormats.push_back(candidate); ++ } ++ } + + std::vector depthStencilFormats( + egl_vk::kConfigDepthStencilFormats, +@@ -103,7 +221,38 @@ egl::ConfigSet DisplayVkWayland::generateConfigs() + + void DisplayVkWayland::checkConfigSupport(egl::Config *config) + { +- // In wayland there is no native visual ID or type ++ // Wayland has no native visual ID. A renderable format is not necessarily ++ // presentable; retain pbuffer support but remove WINDOW_BIT when unsupported. ++ const VkFormat format = mRenderer->getFormat(config->renderTargetFormat) ++ .getActualRenderableImageVkFormat(mRenderer); ++ if (hasSupportedSurfaceFormats()) ++ { ++ if (!isConfigFormatSupported(format)) ++ { ++ config->surfaceType &= ~EGL_WINDOW_BIT; ++ } ++ } ++ else if (config->renderTargetFormat != GL_BGRA8_EXT) ++ { ++ // Discovery failure must not regress the old SDR path or invent HDR WSI. ++ config->surfaceType &= ~EGL_WINDOW_BIT; ++ } ++} ++ ++void DisplayVkWayland::initSupportedSurfaceFormatColorspaces() ++{ ++ // Always use this display's actual Wayland WSI. On failure, retain no stale ++ // capabilities from an earlier initialization or a platform-agnostic query. ++ clearSupportedSurfaceFormatColorspaces(); ++ ++ // Desktop ICDs need not implement VK_GOOGLE_surfaceless_query. Ask the actual ++ // Wayland WSI using a temporary, unmapped native surface instead. ++ ScopedWaylandFormatQuery query(mWaylandDisplay); ++ if (!query.initialize(mRenderer->getInstance()) || ++ !cacheSupportedSurfaceFormatColorspaces(query.get())) ++ { ++ WARN() << "Wayland surface format discovery failed; no additional HDR capabilities assumed"; ++ } + } + + const char *DisplayVkWayland::getWSIExtension() const +diff --git a/src/libANGLE/renderer/vulkan/linux/wayland/DisplayVkWayland.h b/src/libANGLE/renderer/vulkan/linux/wayland/DisplayVkWayland.h +index fc54cf4..f17f9f2 100644 +--- a/src/libANGLE/renderer/vulkan/linux/wayland/DisplayVkWayland.h ++++ b/src/libANGLE/renderer/vulkan/linux/wayland/DisplayVkWayland.h +@@ -38,6 +38,8 @@ class DisplayVkWayland : public DisplayVkLinux + angle::NativeWindowSystem getWindowSystem() const override; + + private: ++ void initSupportedSurfaceFormatColorspaces() override; ++ + bool mOwnDisplay; + wl_display *mWaylandDisplay; + }; diff --git a/release-linux-arm64.gn b/release-linux-arm64.gn index 78717c2..6cb9dd6 100644 --- a/release-linux-arm64.gn +++ b/release-linux-arm64.gn @@ -13,3 +13,6 @@ angle_enable_gl = false angle_enable_null = false angle_shared_libvulkan = false use_sysroot = true + +# Share the caller/toolkit Wayland ABI; do not bundle a private client copy. +angle_use_system_wayland = true diff --git a/release-linux-x64.gn b/release-linux-x64.gn index 8d89f3b..5245066 100644 --- a/release-linux-x64.gn +++ b/release-linux-x64.gn @@ -12,3 +12,6 @@ angle_enable_vulkan = true angle_enable_gl = false angle_enable_null = false angle_shared_libvulkan = false + +# Share the caller/toolkit Wayland ABI; do not bundle a private client copy. +angle_use_system_wayland = true diff --git a/scripts/build-hdr-audit-layer.sh b/scripts/build-hdr-audit-layer.sh new file mode 100755 index 0000000..aecb869 --- /dev/null +++ b/scripts/build-hdr-audit-layer.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +output="${1:?Provide test output directory}" +mkdir -p "$output"; output="$(realpath "$output")" +include_args=() +headers="${VULKAN_HEADERS_DIR:-${ANGLE_DIR:-$ROOT_DIR/build/angle}/third_party/vulkan-headers/src/include}" +if [[ -f "$headers/vulkan/vk_layer.h" ]]; then include_args=(-I "$headers"); fi +"${CXX:-c++}" "${include_args[@]}" -std=c++17 -O2 -Wall -Wextra -shared -fPIC -fvisibility=hidden -pthread \ + "$ROOT_DIR/tests/vulkan-hdr-audit-layer.cpp" -o "$output/libVkLayer_NGE_hdr_audit.so" +python3 - "$output" <<'PY' +import json, pathlib, sys +p=pathlib.Path(sys.argv[1]) +manifest={"file_format_version":"1.2.0", "layer":{ + "name":"VK_LAYER_NGE_hdr_audit", "type":"GLOBAL", + "library_path":str(p/"libVkLayer_NGE_hdr_audit.so"), "api_version":"1.3.0", + "implementation_version":"1", "description":"NGE HDR observation-only swapchain audit"}} +(p/"VkLayer_NGE_hdr_audit.json").write_text(json.dumps(manifest,indent=2)+"\n") +PY diff --git a/scripts/test-linux-hdr.sh b/scripts/test-linux-hdr.sh new file mode 100755 index 0000000..a521845 --- /dev/null +++ b/scripts/test-linux-hdr.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -euo pipefail +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +[[ "$(uname -s)" == Linux ]] || { echo 'This probe requires Linux/Wayland' >&2; exit 2; } +export ANGLE_DIR="${ANGLE_DIR:-$ROOT_DIR/build/angle}" +arch="$(uname -m)"; [[ "$arch" == x86_64 ]] && arch=x64; [[ "$arch" == aarch64 ]] && arch=arm64 +native_dir="${ANGLE_NATIVE_DIR:-$ANGLE_DIR/out/release-linux-$arch}" +native_dir="$(realpath "$native_dir")" +[[ -f "$native_dir/libEGL.so" && -f "$native_dir/libGLESv2.so" ]] || { echo "Build ANGLE first: $native_dir" >&2; exit 2; } +for tool in pkg-config wayland-scanner "${CXX:-c++}" "${CC:-cc}" timeout python3 realpath sha256sum tee; do + command -v "$tool" >/dev/null || { echo "Missing required tool: $tool" >&2; exit 2; } +done +pkg-config --exists wayland-client wayland-egl || { echo 'Install Wayland client development files' >&2; exit 2; } +protocol_dir="${WAYLAND_PROTOCOLS_DIR:-$(pkg-config --variable=pkgdatadir wayland-protocols 2>/dev/null || true)}" +xml="$protocol_dir/stable/xdg-shell/xdg-shell.xml" +if [[ ! -f "$xml" ]]; then xml="$ANGLE_DIR/third_party/wayland-protocols/src/stable/xdg-shell/xdg-shell.xml"; fi +[[ -f "$xml" ]] || { echo 'Install wayland-protocols or set WAYLAND_PROTOCOLS_DIR' >&2; exit 2; } +test_dir="$ROOT_DIR/build/linux-hdr-test-$arch" +mkdir -p "$test_dir" +wayland-scanner client-header "$xml" "$test_dir/xdg-shell-client-protocol.h" +wayland-scanner private-code "$xml" "$test_dir/xdg-shell-protocol.c" +read -r -a wl_cflags <<< "$(pkg-config --cflags wayland-client wayland-egl)" +read -r -a wl_libs <<< "$(pkg-config --libs wayland-client wayland-egl)" +"${CC:-cc}" -fPIC "${wl_cflags[@]}" -c "$test_dir/xdg-shell-protocol.c" -o "$test_dir/xdg-shell-protocol.o" +"${CXX:-c++}" -std=c++17 -O2 -Wall -Wextra -DWL_EGL_PLATFORM -I "$ANGLE_DIR/include" -I "$test_dir" \ + "${wl_cflags[@]}" "$ROOT_DIR/tests/linux-hdr-probe.cpp" "$test_dir/xdg-shell-protocol.o" \ + -L "$native_dir" -Wl,-rpath,"$native_dir" -lEGL -lGLESv2 "${wl_libs[@]}" -o "$test_dir/linux-hdr-probe" +export LD_LIBRARY_PATH="$native_dir${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" +printf 'ANGLE_NATIVE_DIR=%s\nWAYLAND_DISPLAY=%s\n' "$native_dir" "${WAYLAND_DISPLAY:-not set}" +sha256sum "$native_dir/libEGL.so" "$native_dir/libGLESv2.so" +# For compositor-side evidence use WAYLAND_DEBUG=client and retain stderr. This +# never alters advertised HDR capabilities. Set HDR_AUDIT=1 for Vulkan audit layer. +if [[ "${HDR_AUDIT:-0}" == 1 ]]; then + "$ROOT_DIR/scripts/build-hdr-audit-layer.sh" "$test_dir" + export VK_LAYER_PATH="$test_dir:/usr/share/vulkan/explicit_layer.d:/etc/vulkan/explicit_layer.d${VK_LAYER_PATH:+:$VK_LAYER_PATH}" + export VK_INSTANCE_LAYERS="VK_LAYER_NGE_hdr_audit${VK_INSTANCE_LAYERS:+:$VK_INSTANCE_LAYERS}" + log="${HDR_AUDIT_LOG:-$test_dir/hdr-audit-$(date -u +%Y%m%d-%H%M%S)-$$.log}" + mkdir -p "$(dirname "$log")" + set +e + timeout "${TEST_TIMEOUT_SECONDS:-90}" "$test_dir/linux-hdr-probe" "$@" 2>&1 | tee "$log" + pipeline_status=("${PIPESTATUS[@]}") + probe_status=${pipeline_status[0]} + python3 "$ROOT_DIR/scripts/verify-linux-hdr-audit.py" "$log" + audit_status=$? + set -e + # A missing optional HDR mode must never hide an audit or log-write failure. + [[ "${pipeline_status[1]}" == 0 ]] || exit 1 + [[ "$audit_status" == 0 ]] || exit "$audit_status" + exit "$probe_status" +fi +exec timeout "${TEST_TIMEOUT_SECONDS:-90}" "$test_dir/linux-hdr-probe" "$@" diff --git a/scripts/verify-linux-hdr-audit.py b/scripts/verify-linux-hdr-audit.py new file mode 100755 index 0000000..9de6ffd --- /dev/null +++ b/scripts/verify-linux-hdr-audit.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Check actual downstream Vulkan swapchain/present records against probe phases. +This validates software presentation only, never physical monitor emission. +""" +import argparse +import pathlib +import re +import sys + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('log', type=pathlib.Path) +parser.add_argument('--require-hdr', action='store_true') +args = parser.parse_args() +lines = args.log.read_text(errors='replace').splitlines() +latest_swapchain = None +current_phase = None +presents = 0 +hdr_passes = 0 +phase_passes = 0 +failures = [] +loaded = False +for number, line in enumerate(lines, 1): + if 'HDR_AUDIT_INSTANCE created observation_only=1' in line: + loaded = True + match = re.search(r'HDR_AUDIT_SWAPCHAIN format=(\d+) colorspace=(\d+) extent=\d+x\d+ result=(-?\d+)', line) + if match: + latest_swapchain = tuple(map(int, match.groups())) + match = re.search(r'PHASE_BEGIN (\S+)', line) + if match: + current_phase = match.group(1) + presents = 0 + match = re.search(r'HDR_AUDIT_PRESENT swapchains=(\d+) result=(-?\d+)', line) + if match and current_phase: + count, result = map(int, match.groups()) + if result not in (0, 1000001003): + failures.append(f'line {number}: vkQueuePresentKHR returned {result}') + else: + presents += count + match = re.search(r'PHASE_PASS (\S+)', line) + if not match: + continue + name = match.group(1) + if name != current_phase: + failures.append(f'line {number}: phase boundary mismatch') + continue + if latest_swapchain is None or latest_swapchain[2] != 0 or presents < 4: + failures.append(f'{name}: missing successful real swapchain/presentation evidence') + continue + fmt, space, _ = latest_swapchain + if name == 'scRGB_FP16': + valid = fmt == 97 and space == 1000104002 # R16G16B16A16_SFLOAT, EXTENDED_SRGB_LINEAR + elif name == 'HDR10_PQ_RGB10A2': + valid = fmt in (58, 64) and space == 1000104008 # 10-bit UNORM, HDR10_ST2084 + elif name.startswith('SDR_'): + valid = space == 0 + else: + valid = False + if not valid: + failures.append(f'{name}: unexpected Vulkan format={fmt}, colorspace={space}') + else: + phase_passes += 1 + hdr_passes += name in ('scRGB_FP16', 'HDR10_PQ_RGB10A2') + print(f'AUDIT_PASS {name} VkFormat={fmt} VkColorSpace={space} presents={presents}') + current_phase = None +if not loaded: + failures.append('Observation layer was not loaded; no native Vulkan evidence') +if phase_passes == 0: + failures.append('No completed presentation phases') +if not any('LINUX_HDR_SUMMARY ' in line for line in lines): + failures.append('Probe did not reach its summary') +if any('LINUX_HDR_FAIL ' in line for line in lines): + failures.append('Probe reported a failure') +validation_errors = sum('Validation Error:' in line for line in lines) +if validation_errors: + failures.append(f'Khronos layer reported {validation_errors} validation errors; not a clean validation run') +if failures: + for failure in failures: + print(f'AUDIT_FAIL {failure}', file=sys.stderr) + sys.exit(1) +print(f'AUDIT_SUMMARY phases={phase_passes} hdr_phases={hdr_passes} physical_hdr=NOT_TESTED') +if args.require_hdr and hdr_passes == 0: + print('AUDIT_SKIP no HDR software presentation was available') + sys.exit(77) diff --git a/tests/linux-hdr-probe.cpp b/tests/linux-hdr-probe.cpp new file mode 100644 index 0000000..44bc108 --- /dev/null +++ b/tests/linux-hdr-probe.cpp @@ -0,0 +1,394 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Real ANGLE/EGL + Wayland window probe. A successful software presentation is +// NOT proof of light output from a physical HDR monitor. +#include +#include +#include +#include +#include +#include "xdg-shell-client-protocol.h" +#include +#include +#include +#include +#include +#include +#include + +static void Require(bool ok, const std::string &message) +{ + if (!ok) + { + char suffix[64]; + std::snprintf(suffix, sizeof(suffix), " (EGL=0x%x)", eglGetError()); + throw std::runtime_error(message + suffix); + } +} +// Read eglGetError only after the operation has failed, inside Require(). +static const char *EglError(const char *operation) { return operation; } +static bool HasExtension(const char *extensions, const char *name) +{ + if (!extensions) return false; + std::string list = std::string(" ") + extensions + " "; + return list.find(std::string(" ") + name + " ") != std::string::npos; +} + +struct WaylandConnection +{ + wl_display *display = nullptr; + wl_registry *registry = nullptr; + wl_compositor *compositor = nullptr; + xdg_wm_base *shell = nullptr; + bool colorManagement = false; + static void Ping(void *, xdg_wm_base *shell, uint32_t serial) { xdg_wm_base_pong(shell, serial); } + static void Global(void *data, wl_registry *registry, uint32_t name, const char *iface, uint32_t version) + { + auto &self = *static_cast(data); + if (std::strcmp(iface, "wl_compositor") == 0) + self.compositor = static_cast(wl_registry_bind(registry, name, &wl_compositor_interface, std::min(version, 4u))); + else if (std::strcmp(iface, "xdg_wm_base") == 0) + { + self.shell = static_cast(wl_registry_bind(registry, name, &xdg_wm_base_interface, 1)); + static const xdg_wm_base_listener listener = {Ping}; + xdg_wm_base_add_listener(self.shell, &listener, &self); + } + else if (std::strcmp(iface, "wp_color_manager_v1") == 0) + { + self.colorManagement = true; + std::printf("WAYLAND_COLOR_MANAGER version=%u\n", version); + } + } + static void Removed(void *, wl_registry *, uint32_t) {} + void Initialize() + { + display = wl_display_connect(nullptr); + Require(display != nullptr, "Cannot connect to Wayland; set WAYLAND_DISPLAY and XDG_RUNTIME_DIR"); + registry = wl_display_get_registry(display); + Require(registry != nullptr, "Cannot get Wayland registry"); + static const wl_registry_listener listener = {Global, Removed}; + Require(wl_registry_add_listener(registry, &listener, this) == 0, "Registry listener failed"); + Require(wl_display_roundtrip(display) >= 0 && compositor && shell, "Wayland compositor/xdg-shell unavailable"); + } + ~WaylandConnection() + { + if (shell) xdg_wm_base_destroy(shell); + if (compositor) wl_compositor_destroy(compositor); + if (registry) wl_registry_destroy(registry); + if (display) wl_display_disconnect(display); + } +}; +struct NativeWindow +{ + wl_surface *surface = nullptr; + xdg_surface *xdgSurface = nullptr; + xdg_toplevel *toplevel = nullptr; + wl_egl_window *eglWindow = nullptr; + bool configured = false; + bool closed = false; + static void Configure(void *data, xdg_surface *surface, uint32_t serial) + { + xdg_surface_ack_configure(surface, serial); + static_cast(data)->configured = true; + } + static void Resize(void *, xdg_toplevel *, int32_t, int32_t, wl_array *) {} + static void Close(void *data, xdg_toplevel *) { static_cast(data)->closed = true; } + void Initialize(WaylandConnection &wl) + { + surface = wl_compositor_create_surface(wl.compositor); + Require(surface != nullptr, "Cannot create wl_surface"); + xdgSurface = xdg_wm_base_get_xdg_surface(wl.shell, surface); + Require(xdgSurface != nullptr, "Cannot create xdg_surface"); + static const xdg_surface_listener surfaceListener = {Configure}; + xdg_surface_add_listener(xdgSurface, &surfaceListener, this); + toplevel = xdg_surface_get_toplevel(xdgSurface); + Require(toplevel != nullptr, "Cannot create xdg_toplevel"); + static const xdg_toplevel_listener toplevelListener = {Resize, Close, nullptr, nullptr}; + xdg_toplevel_add_listener(toplevel, &toplevelListener, this); + xdg_toplevel_set_title(toplevel, "ANGLE Linux HDR / SDR validation"); + xdg_toplevel_set_app_id(toplevel, "org.ngengine.angle.hdr-probe"); + wl_surface_commit(surface); + while (!configured && !closed) + Require(wl_display_roundtrip(wl.display) >= 0, "Wayland configure failed"); + Require(!closed, "Probe window closed before test"); + eglWindow = wl_egl_window_create(surface, 320, 180); + Require(eglWindow != nullptr, "Cannot create wl_egl_window"); + } + ~NativeWindow() + { + if (eglWindow) wl_egl_window_destroy(eglWindow); + if (toplevel) xdg_toplevel_destroy(toplevel); + if (xdgSurface) xdg_surface_destroy(xdgSurface); + if (surface) wl_surface_destroy(surface); + } +}; +struct EglDisplay +{ + EGLDisplay display = EGL_NO_DISPLAY; + ~EglDisplay() { if (display != EGL_NO_DISPLAY) eglTerminate(display); } +}; +struct EglDrawable +{ + EGLDisplay display; + EGLContext context = EGL_NO_CONTEXT; + EGLSurface surface = EGL_NO_SURFACE; + explicit EglDrawable(EGLDisplay value) : display(value) {} + ~EglDrawable() + { + eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + if (context != EGL_NO_CONTEXT) eglDestroyContext(display, context); + if (surface != EGL_NO_SURFACE) eglDestroySurface(display, surface); + } +}; +static EGLint Attribute(EGLDisplay display, EGLConfig config, EGLint attribute) +{ + EGLint value = -1; + Require(eglGetConfigAttrib(display, config, attribute, &value), EglError("eglGetConfigAttrib")); + return value; +} +static EGLConfig Choose(EGLDisplay display, bool floating, int rgbBits, int surfaceType) +{ + const EGLint attributes[] = { + EGL_SURFACE_TYPE, surfaceType, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT_KHR, + EGL_RED_SIZE, rgbBits, EGL_GREEN_SIZE, rgbBits, EGL_BLUE_SIZE, rgbBits, + EGL_ALPHA_SIZE, 0, EGL_DEPTH_SIZE, 0, EGL_STENCIL_SIZE, 0, + EGL_SAMPLE_BUFFERS, 0, EGL_SAMPLES, 0, + EGL_COLOR_COMPONENT_TYPE_EXT, floating ? EGL_COLOR_COMPONENT_TYPE_FLOAT_EXT : EGL_COLOR_COMPONENT_TYPE_FIXED_EXT, + EGL_NONE}; + EGLint count = 0; + Require(eglChooseConfig(display, attributes, nullptr, 0, &count), EglError("eglChooseConfig count")); + if (count == 0) return nullptr; + std::vector configs(count); + Require(eglChooseConfig(display, attributes, configs.data(), count, &count), EglError("eglChooseConfig")); + for (int i = 0; i < count; ++i) + if (Attribute(display, configs[i], EGL_SAMPLES) == 0) + return configs[i]; + return nullptr; +} +static GLuint Compile(GLenum type, const char *source) +{ + GLuint shader = glCreateShader(type); + glShaderSource(shader, 1, &source, nullptr); + glCompileShader(shader); + GLint status = 0; + glGetShaderiv(shader, GL_COMPILE_STATUS, &status); + if (!status) + { + char error[2048] = {}; + glGetShaderInfoLog(shader, sizeof(error), nullptr, error); + glDeleteShader(shader); + throw std::runtime_error(std::string("Shader compile failed: ") + error); + } + return shader; +} +static GLuint Program() +{ + const char *vertex = R"(#version 300 es +void main() { + vec2 p = vec2((gl_VertexID << 1) & 2, gl_VertexID & 2); + gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0); +})"; + const char *fragment = R"(#version 300 es +precision highp float; +uniform float signal; +uniform int pq; +out vec4 color; +void main() { + float value = signal; + if (pq != 0) { + float p = pow(max(value / 10000.0, 0.0), 2610.0 / 16384.0); + value = pow((3424.0 / 4096.0 + (2413.0 / 128.0) * p) / + (1.0 + (2392.0 / 128.0) * p), 2523.0 / 32.0); + } + color = vec4(value, value, value, 1.0); +})"; + GLuint vs = Compile(GL_VERTEX_SHADER, vertex); + GLuint fs = Compile(GL_FRAGMENT_SHADER, fragment); + GLuint program = glCreateProgram(); + glAttachShader(program, vs); glAttachShader(program, fs); glLinkProgram(program); + glDeleteShader(vs); glDeleteShader(fs); + GLint status = 0; glGetProgramiv(program, GL_LINK_STATUS, &status); + Require(status != 0, "Program link failed"); + return program; +} +static double PqReference(double nits) +{ + const double p = std::pow(nits / 10000.0, 2610.0 / 16384.0); + return std::pow((3424.0 / 4096.0 + (2413.0 / 128.0) * p) / + (1.0 + (2392.0 / 128.0) * p), 2523.0 / 32.0); +} +static float ReadPixel() +{ + GLint type = 0, format = 0; + glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_TYPE, &type); + glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_FORMAT, &format); + float result = 0; + if (type == GL_FLOAT) + { + GLfloat color[4] = {}; + glReadPixels(8, 8, 1, 1, format, type, color); result = color[0]; + } + else if (type == GL_HALF_FLOAT) + { + GLushort color[4] = {}; + glReadPixels(8, 8, 1, 1, format, type, color); + unsigned exponent = (color[0] >> 10) & 31; + unsigned mantissa = color[0] & 1023; + result = exponent ? std::ldexp(float(1024 + mantissa), int(exponent) - 25) + : std::ldexp(float(mantissa), -24); + if (color[0] & 0x8000) result = -result; + } + else if (type == GL_UNSIGNED_INT_2_10_10_10_REV) + { + GLuint pixel = 0; + glReadPixels(8, 8, 1, 1, format, type, &pixel); result = (pixel & 1023) / 1023.f; + } + else + { + GLubyte color[4] = {}; + glReadPixels(8, 8, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, color); result = color[0] / 255.f; + } + Require(glGetError() == GL_NO_ERROR, "Default-framebuffer readback failed"); + return result; +} +static void RunPhase(EGLDisplay display, EGLConfig config, NativeWindow &window, + WaylandConnection &wl, const char *name, EGLint colorspace, bool floating, bool pq) +{ + EglDrawable drawable(display); + EGLint surfaceAttributes[] = {EGL_GL_COLORSPACE_KHR, colorspace, EGL_NONE}; + drawable.surface = eglCreateWindowSurface(display, config, + reinterpret_cast(window.eglWindow), surfaceAttributes); + Require(drawable.surface != EGL_NO_SURFACE, EglError("eglCreateWindowSurface")); + EGLint actual = 0; + Require(eglQuerySurface(display, drawable.surface, EGL_GL_COLORSPACE_KHR, &actual) && actual == colorspace, + "EGL surface colorspace differs from requested colorspace"); + const EGLint contextAttributes[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE}; + drawable.context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttributes); + Require(drawable.context != EGL_NO_CONTEXT, EglError("eglCreateContext")); + Require(eglMakeCurrent(display, drawable.surface, drawable.surface, drawable.context), EglError("eglMakeCurrent")); + const char *renderer = reinterpret_cast(glGetString(GL_RENDERER)); + Require(renderer && std::strstr(renderer, "ANGLE"), "Probe did not load ANGLE GLES renderer"); + std::printf("PHASE_BEGIN %s renderer=%s colorspace=0x%x rgba=%d/%d/%d/%d component=0x%x\n", + name, renderer, actual, Attribute(display, config, EGL_RED_SIZE), Attribute(display, config, EGL_GREEN_SIZE), + Attribute(display, config, EGL_BLUE_SIZE), Attribute(display, config, EGL_ALPHA_SIZE), + Attribute(display, config, EGL_COLOR_COMPONENT_TYPE_EXT)); + const float signal = pq ? 1000.f : floating ? 2.f : colorspace == EGL_GL_COLORSPACE_SRGB_KHR ? .18f : .5f; + const double expected = pq ? PqReference(signal) : + colorspace == EGL_GL_COLORSPACE_SRGB_KHR ? 1.055 * std::pow(double(signal), 1.0 / 2.4) - .055 : signal; + GLuint program = Program(); + glUseProgram(program); glUniform1f(glGetUniformLocation(program, "signal"), signal); + glUniform1i(glGetUniformLocation(program, "pq"), pq ? 1 : 0); + glViewport(0, 0, 320, 180); + glDisable(GL_DITHER); + glDrawArrays(GL_TRIANGLES, 0, 3); + Require(glGetError() == GL_NO_ERROR, "Probe draw failed"); + float observed = ReadPixel(); + Require(std::isfinite(observed) && std::abs(observed - expected) < .005, + "Native drawable clips HDR values or applies incorrect transfer function"); + if (floating) Require(observed > 1.f, "Floating native drawable clipped extended-range content"); + for (int frame = 0; frame < 4; ++frame) + { + glDrawArrays(GL_TRIANGLES, 0, 3); + Require(eglSwapBuffers(display, drawable.surface), EglError("eglSwapBuffers")); + Require(wl_display_roundtrip(wl.display) >= 0, "Compositor rejected presentation"); + Require(!window.closed, "Window closed during validation"); + } + glFinish(); glDeleteProgram(program); + Require(glGetError() == GL_NO_ERROR, "GL error after presentation"); + std::printf("PHASE_PASS %s readback=%.7f expected=%.7f swaps=4 scope=software-presentation\n", name, observed, expected); +} +int main(int argc, char **argv) +{ + std::setvbuf(stdout, nullptr, _IONBF, 0); + bool requireHdr = false, requireScrgb = false, requireHdr10 = false; + for (int i = 1; i < argc; ++i) + { + if (!std::strcmp(argv[i], "--require-hdr")) requireHdr = true; + else if (!std::strcmp(argv[i], "--require-scrgb")) requireScrgb = true; + else if (!std::strcmp(argv[i], "--require-hdr10")) requireHdr10 = true; + else { std::fprintf(stderr, "Unknown argument: %s\n", argv[i]); return 2; } + } + try + { + WaylandConnection wl; wl.Initialize(); + EglDisplay owner; + auto getDisplay = reinterpret_cast(eglGetProcAddress("eglGetPlatformDisplayEXT")); + Require(getDisplay != nullptr, "Missing platform EGL entry point"); + owner.display = getDisplay(EGL_PLATFORM_WAYLAND_EXT, wl.display, nullptr); + Require(owner.display != EGL_NO_DISPLAY && eglInitialize(owner.display, nullptr, nullptr), EglError("eglInitialize Wayland")); + Require(eglBindAPI(EGL_OPENGL_ES_API), EglError("eglBindAPI")); + EGLDisplay display = owner.display; + const char *extensions = eglQueryString(display, EGL_EXTENSIONS); + std::printf("EGL_VENDOR %s\nEGL_VERSION %s\nEGL_EXTENSIONS %s\n", eglQueryString(display, EGL_VENDOR), + eglQueryString(display, EGL_VERSION), extensions ? extensions : ""); + Require(HasExtension(extensions, "EGL_KHR_gl_colorspace"), "No EGL colorspace support"); + EGLint configCount = 0; + Require(eglGetConfigs(display, nullptr, 0, &configCount), EglError("eglGetConfigs")); + std::vector configs(configCount); + Require(eglGetConfigs(display, configs.data(), configCount, &configCount), EglError("eglGetConfigs list")); + int floatWindows = 0, tenBitWindows = 0; + for (int i = 0; i < configCount; ++i) + { + EGLConfig c = configs[i]; + int type = Attribute(display, c, EGL_COLOR_COMPONENT_TYPE_EXT); + int red = Attribute(display, c, EGL_RED_SIZE), surface = Attribute(display, c, EGL_SURFACE_TYPE); + if (Attribute(display, c, EGL_SAMPLES) == 0) + std::printf("CONFIG id=%d rgba=%d/%d/%d/%d type=0x%x surface=0x%x depth=%d stencil=%d\n", + Attribute(display,c,EGL_CONFIG_ID), red, Attribute(display,c,EGL_GREEN_SIZE), Attribute(display,c,EGL_BLUE_SIZE), + Attribute(display,c,EGL_ALPHA_SIZE), type, surface, Attribute(display,c,EGL_DEPTH_SIZE), Attribute(display,c,EGL_STENCIL_SIZE)); + if (surface & EGL_WINDOW_BIT) + { + if (type == EGL_COLOR_COMPONENT_TYPE_FLOAT_EXT && red >= 16) ++floatWindows; + if (type == EGL_COLOR_COMPONENT_TYPE_FIXED_EXT && red >= 10) ++tenBitWindows; + } + } + std::printf("CONFIG_SUMMARY total=%d fp16_window=%d rgb10_window=%d\n", configCount, floatWindows, tenBitWindows); + NativeWindow window; window.Initialize(wl); + EGLConfig sdr = Choose(display, false, 8, EGL_WINDOW_BIT); + Require(sdr != nullptr, "No SDR EGL window config"); + RunPhase(display, sdr, window, wl, "SDR_before", EGL_GL_COLORSPACE_LINEAR_KHR, false, false); + int hdrPasses = 0, hdrSkipped = 0; + bool scrgbPassed = false, hdr10Passed = false; + for (int index = 0; index < 2; ++index) + { + bool floating = index == 0; + const char *name = floating ? "scRGB_FP16" : "HDR10_PQ_RGB10A2"; + const char *extension = floating ? "EGL_EXT_gl_colorspace_scrgb_linear" : "EGL_EXT_gl_colorspace_bt2020_pq"; + EGLConfig config = Choose(display, floating, floating ? 16 : 10, EGL_WINDOW_BIT); + if (!HasExtension(extensions, extension) || !config) + { + ++hdrSkipped; + std::printf("HDR_SKIP %s extension=%d window_config=%d (not counted as HDR PASS)\n", + name, HasExtension(extensions, extension), config != nullptr); + continue; + } + RunPhase(display, config, window, wl, name, + floating ? EGL_GL_COLORSPACE_SCRGB_LINEAR_EXT : EGL_GL_COLORSPACE_BT2020_PQ_EXT, + floating, !floating); + ++hdrPasses; + if (floating) scrgbPassed = true; + else hdr10Passed = true; + RunPhase(display, sdr, window, wl, "SDR_after_HDR_same_native_window", EGL_GL_COLORSPACE_LINEAR_KHR, false, false); + } + const EGLint invalidAttributes[] = {EGL_GL_COLORSPACE_KHR, 0x7ffffffe, EGL_NONE}; + EGLSurface invalid = eglCreateWindowSurface(display, sdr, + reinterpret_cast(window.eglWindow), invalidAttributes); + if (invalid != EGL_NO_SURFACE) eglDestroySurface(display, invalid); + Require(invalid == EGL_NO_SURFACE, "Invalid colorspace incorrectly accepted"); + std::printf("INVALID_COLORSPACE_REJECTION_PASS egl_error=0x%x\n", eglGetError()); + RunPhase(display, sdr, window, wl, "SDR_after_rejection", EGL_GL_COLORSPACE_LINEAR_KHR, false, false); + RunPhase(display, sdr, window, wl, "SDR_sRGB_encoding", EGL_GL_COLORSPACE_SRGB_KHR, false, false); + std::printf("LINUX_HDR_SUMMARY hdr_pass=%d hdr_skip=%d sdr=PASS physical_hdr=NOT_TESTED\n", hdrPasses, hdrSkipped); + if ((requireHdr && hdrPasses == 0) || (requireScrgb && !scrgbPassed) || + (requireHdr10 && !hdr10Passed)) + { + std::puts("REQUIRED_HDR_UNAVAILABLE: returning 77, not success"); + return 77; + } + return 0; + } + catch (const std::exception &error) + { + std::fprintf(stderr, "LINUX_HDR_FAIL %s\n", error.what()); + return 1; + } +} diff --git a/tests/test-linux-hdr-logic.py b/tests/test-linux-hdr-logic.py new file mode 100644 index 0000000..e053f65 --- /dev/null +++ b/tests/test-linux-hdr-logic.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +"""Compile the actual ANGLE query helper in a mocked Vulkan unit-test harness. +This checks enumeration/error handling ONLY; it is not an HDR runtime test. +""" +from pathlib import Path +import re, subprocess, sys +import argparse, os +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('--angle-dir', type=Path) +args = parser.parse_args() +root = Path(__file__).resolve().parents[1] +angle_dir = args.angle_dir or Path(os.environ.get('ANGLE_DIR', str(root / 'build/angle'))) +src = angle_dir / 'src/libANGLE/renderer/vulkan/DisplayVk.cpp' +s = src.read_text() +m = re.search(r'(?:VkResult|void) GetSupportedFormatColorspaces\(', s) +if not m: + sys.exit('Query helper not found') +begin = s.index('{', m.start()); depth = 1; end = begin + 1 +while depth: + if s[end] == '{': depth += 1 + elif s[end] == '}': depth -= 1 + end += 1 +helper = s[m.start():end] +if not helper.startswith('VkResult'): + sys.exit('Query helper has not yet been updated to return VkResult') +header = r''' +#include +#include +#include +#include +#include +#include +// Minimal Vulkan ABI-independent types for this logic-only test. +using VkPhysicalDevice = uintptr_t; +using VkSurfaceKHR = uintptr_t; +using VkResult = int; +constexpr VkResult VK_SUCCESS=0, VK_INCOMPLETE=5, VK_ERROR_SURFACE_LOST_KHR=-1000000000; +constexpr int VK_STRUCTURE_TYPE_SURFACE_FORMAT_2_KHR=1; +constexpr int VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SURFACE_INFO_2_KHR=2; +constexpr int VK_FORMAT_UNDEFINED=0, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR=0; +struct VkSurfaceFormatKHR {int format; int colorSpace;}; +struct VkSurfaceFormat2KHR {int sType; void *pNext; VkSurfaceFormatKHR surfaceFormat;}; +struct VkPhysicalDeviceSurfaceInfo2KHR {int sType; void *pNext; VkSurfaceKHR surface;}; +namespace angle {struct Feature {bool enabled=false;}; struct FeaturesVk {Feature supportsSurfaceCapabilities2Extension;};} +#define ASSERT(x) ((void)0) +#define VK_CALL(name, ...) name(__VA_ARGS__) +static int scenario=0, calls=0; +static VkSurfaceFormatKHR pairs[]={{97,1000104002},{64,1000104008},{44,0}}; +static VkResult StubQuery(uint32_t *count, VkSurfaceFormatKHR *out) { + ++calls; + if(scenario==1 && !out) return VK_ERROR_SURFACE_LOST_KHR; + if(scenario==2 && out) return VK_ERROR_SURFACE_LOST_KHR; + if(!out) { + *count=scenario==5 ? 0 : (scenario==4 && calls==1) ? 2 : 3; + return VK_SUCCESS; + } + auto n=std::min(*count, uint32_t(scenario==3 ? 2 : 3)); + for(uint32_t i=0;i tmp(*n); + VkResult r=StubQuery(n,tmp.data()); + for(uint32_t i=0;i<*n;++i) out[i].surfaceFormat=tmp[i]; + return r; +} +''' +tail = r''' +int main() { + int fails=0; + for(bool v2 : {false,true}) { + angle::FeaturesVk f; f.supportsSurfaceCapabilities2Extension.enabled=v2; + for(scenario=0;scenario<7;++scenario) { + calls=0; + std::vector out; + VkResult result=GetSupportedFormatColorspaces(1,f,2,&out); + bool good=false; + if(scenario==1 || scenario==2) good=result==VK_ERROR_SURFACE_LOST_KHR && out.empty(); + else if(scenario==5) good=result==VK_SUCCESS && out.empty() && calls==1; + else if(scenario==6) good=result==VK_INCOMPLETE && out.empty() && calls==8; + else { + const size_t expected=scenario==3?2:3; + good=result==VK_SUCCESS && out.size()==expected; + for(size_t i=0;good && i +#include +#include +#include +#include +#include +#include + +namespace { +constexpr const char *LayerName = "VK_LAYER_NGE_hdr_audit"; +struct InstanceState { + VkInstance instance = VK_NULL_HANDLE; + PFN_vkGetInstanceProcAddr gipa = nullptr; + PFN_GetPhysicalDeviceProcAddr gpdpa = nullptr; + PFN_vkDestroyInstance destroy = nullptr; +}; +struct DeviceState { + PFN_vkGetDeviceProcAddr gdpa = nullptr; + PFN_vkDestroyDevice destroy = nullptr; + PFN_vkCreateSwapchainKHR createSwapchain = nullptr; + PFN_vkQueuePresentKHR present = nullptr; +}; +std::mutex StateMutex; +std::unordered_map Instances; +std::unordered_map Devices; +template void *Key(T handle) { return handle ? *reinterpret_cast(handle) : nullptr; } +InstanceState Instance(void *key) { + std::lock_guard lock(StateMutex); + auto it = Instances.find(key); return it == Instances.end() ? InstanceState{} : it->second; +} +DeviceState Device(void *key) { + std::lock_guard lock(StateMutex); + auto it = Devices.find(key); return it == Devices.end() ? DeviceState{} : it->second; +} +} +static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL NgeGipa(VkInstance, const char *); +static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL NgeGdpa(VkDevice, const char *); +static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL NgeGpdpa(VkInstance, const char *); + +static VKAPI_ATTR VkResult VKAPI_CALL NgeCreateInstance(const VkInstanceCreateInfo *info, + const VkAllocationCallbacks *allocator, VkInstance *instance) +{ + auto *chain = reinterpret_cast(const_cast(info->pNext)); + while (chain && !(chain->sType == VK_STRUCTURE_TYPE_LOADER_INSTANCE_CREATE_INFO && chain->function == VK_LAYER_LINK_INFO)) + chain = reinterpret_cast(const_cast(chain->pNext)); + if (!chain || !chain->u.pLayerInfo) return VK_ERROR_INITIALIZATION_FAILED; + auto *link = chain->u.pLayerInfo; + auto gipa = link->pfnNextGetInstanceProcAddr; + auto gpdpa = link->pfnNextGetPhysicalDeviceProcAddr; + chain->u.pLayerInfo = link->pNext; + auto create = reinterpret_cast(gipa(VK_NULL_HANDLE, "vkCreateInstance")); + if (!create) return VK_ERROR_INITIALIZATION_FAILED; + VkResult result = create(info, allocator, instance); + if (result == VK_SUCCESS) { + std::lock_guard lock(StateMutex); + Instances[Key(*instance)] = {*instance, gipa, gpdpa, + reinterpret_cast(gipa(*instance, "vkDestroyInstance"))}; + std::fprintf(stderr, "HDR_AUDIT_INSTANCE created observation_only=1\n"); + } + return result; +} +static VKAPI_ATTR void VKAPI_CALL NgeDestroyInstance(VkInstance instance, const VkAllocationCallbacks *allocator) +{ + void *key = Key(instance); auto state = Instance(key); + { std::lock_guard lock(StateMutex); Instances.erase(key); } + if (state.destroy) state.destroy(instance, allocator); +} +static VKAPI_ATTR VkResult VKAPI_CALL NgeCreateDevice(VkPhysicalDevice physical, + const VkDeviceCreateInfo *info, const VkAllocationCallbacks *allocator, VkDevice *device) +{ + auto state = Instance(Key(physical)); + auto *chain = reinterpret_cast(const_cast(info->pNext)); + while (chain && !(chain->sType == VK_STRUCTURE_TYPE_LOADER_DEVICE_CREATE_INFO && chain->function == VK_LAYER_LINK_INFO)) + chain = reinterpret_cast(const_cast(chain->pNext)); + if (!chain || !chain->u.pLayerInfo || !state.instance) return VK_ERROR_INITIALIZATION_FAILED; + auto *link = chain->u.pLayerInfo; + auto gdpa = link->pfnNextGetDeviceProcAddr; + auto create = reinterpret_cast(link->pfnNextGetInstanceProcAddr(state.instance, "vkCreateDevice")); + chain->u.pLayerInfo = link->pNext; + if (!create) return VK_ERROR_INITIALIZATION_FAILED; + VkResult result = create(physical, info, allocator, device); + if (result == VK_SUCCESS) { + std::lock_guard lock(StateMutex); + Devices[Key(*device)] = {gdpa, + reinterpret_cast(gdpa(*device, "vkDestroyDevice")), + reinterpret_cast(gdpa(*device, "vkCreateSwapchainKHR")), + reinterpret_cast(gdpa(*device, "vkQueuePresentKHR"))}; + std::fprintf(stderr, "HDR_AUDIT_DEVICE created\n"); + } + return result; +} +static VKAPI_ATTR void VKAPI_CALL NgeDestroyDevice(VkDevice device, const VkAllocationCallbacks *allocator) +{ + void *key = Key(device); auto state = Device(key); + { std::lock_guard lock(StateMutex); Devices.erase(key); } + if (state.destroy) state.destroy(device, allocator); +} +static VKAPI_ATTR VkResult VKAPI_CALL NgeCreateSwapchain(VkDevice device, + const VkSwapchainCreateInfoKHR *info, const VkAllocationCallbacks *allocator, VkSwapchainKHR *swapchain) +{ + auto state = Device(Key(device)); + if (!state.createSwapchain) return VK_ERROR_EXTENSION_NOT_PRESENT; + VkResult result = state.createSwapchain(device, info, allocator, swapchain); + std::fprintf(stderr, "HDR_AUDIT_SWAPCHAIN format=%d colorspace=%d extent=%ux%u result=%d\n", + int(info->imageFormat), int(info->imageColorSpace), info->imageExtent.width, info->imageExtent.height, int(result)); + return result; +} +static VKAPI_ATTR VkResult VKAPI_CALL NgePresent(VkQueue queue, const VkPresentInfoKHR *info) +{ + auto state = Device(Key(queue)); + if (!state.present) return VK_ERROR_EXTENSION_NOT_PRESENT; + VkResult result = state.present(queue, info); + std::fprintf(stderr, "HDR_AUDIT_PRESENT swapchains=%u result=%d\n", info->swapchainCount, int(result)); + return result; +} +static VKAPI_ATTR VkResult VKAPI_CALL NgeInstanceExtensions(const char *layer, uint32_t *count, VkExtensionProperties *) +{ + if (!layer || std::strcmp(layer, LayerName)) return VK_ERROR_LAYER_NOT_PRESENT; + *count = 0; return VK_SUCCESS; +} +static VKAPI_ATTR VkResult VKAPI_CALL NgeDeviceExtensions(VkPhysicalDevice physical, const char *layer, + uint32_t *count, VkExtensionProperties *properties) +{ + if (layer) { + if (std::strcmp(layer, LayerName)) return VK_ERROR_LAYER_NOT_PRESENT; + *count = 0; return VK_SUCCESS; + } + auto state = Instance(Key(physical)); + auto function = state.gipa ? reinterpret_cast( + state.gipa(state.instance, "vkEnumerateDeviceExtensionProperties")) : nullptr; + return function ? function(physical, layer, count, properties) : VK_ERROR_INITIALIZATION_FAILED; +} +static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL NgeGipa(VkInstance instance, const char *name) +{ +#define HOOK(n, f) if (!std::strcmp(name, n)) return reinterpret_cast(f) + HOOK("vkGetInstanceProcAddr", NgeGipa); + HOOK("vkGetDeviceProcAddr", NgeGdpa); + HOOK("vkCreateInstance", NgeCreateInstance); + HOOK("vkDestroyInstance", NgeDestroyInstance); + HOOK("vkCreateDevice", NgeCreateDevice); + HOOK("vkDestroyDevice", NgeDestroyDevice); + HOOK("vkCreateSwapchainKHR", NgeCreateSwapchain); + HOOK("vkQueuePresentKHR", NgePresent); + HOOK("vkEnumerateInstanceExtensionProperties", NgeInstanceExtensions); + HOOK("vkEnumerateDeviceExtensionProperties", NgeDeviceExtensions); + auto state = Instance(Key(instance)); + return state.gipa ? state.gipa(instance, name) : nullptr; +} +static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL NgeGdpa(VkDevice device, const char *name) +{ + HOOK("vkGetDeviceProcAddr", NgeGdpa); + HOOK("vkDestroyDevice", NgeDestroyDevice); + HOOK("vkCreateSwapchainKHR", NgeCreateSwapchain); + HOOK("vkQueuePresentKHR", NgePresent); + auto state = Device(Key(device)); + return state.gdpa ? state.gdpa(device, name) : nullptr; +} +#undef HOOK +static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL NgeGpdpa(VkInstance instance, const char *name) +{ + auto state = Instance(Key(instance)); + return state.gpdpa ? state.gpdpa(instance, name) : state.gipa ? state.gipa(instance, name) : nullptr; +} +extern "C" __attribute__((visibility("default"))) VKAPI_ATTR VkResult VKAPI_CALL +vkNegotiateLoaderLayerInterfaceVersion(VkNegotiateLayerInterface *version) +{ + if (!version || version->loaderLayerInterfaceVersion < 2) return VK_ERROR_INITIALIZATION_FAILED; + version->loaderLayerInterfaceVersion = 2; + version->pfnGetInstanceProcAddr = NgeGipa; + version->pfnGetDeviceProcAddr = NgeGdpa; + version->pfnGetPhysicalDeviceProcAddr = NgeGpdpa; + return VK_SUCCESS; +}