Add opt-in D3D9 single-sample workaround and fix HUD texel format

This commit is contained in:
Leask Wong committed 2026-10-11 22:34:06 -04:00
1 parent 2204da404a
commit 88dfddd18a
7 files changed
+461

No files matched your search

@@ -0,0 +1,108 @@
# American Nightmare on Pixel: rendering workaround and remaining limits
Local engineering results from 2026-10-11/12. Steam game 202750 was tested on a
Pixel 11 Pro Fold using the Pixel Lab runtime. These observations do not establish
compatibility across the Pixel family or a fix for the system resets.
## Rendering result
The bright dotted triangle edges over textured rocks, boards and the player are
not an application wireframe setting: traced input and driver pipelines all used
`VK_POLYGON_MODE_FILL`, with no dynamic polygon mode. The original affected path
used 2x MSAA, RGBA16F color and D24S8 depth. The game's minimum UI AA setting and
`d3d11.disableMsaa` did not disable its D3D9 multisampling.
A real single-sample D3D9 comparison removed the visible bright triangle edges.
Trial 16 verified 114 input and 114 driver pipelines, all single-sample. Trial 17
retained correct meshes/textures through movement and turning, with 119 pipelines
in each phase, all single-sample. This isolates a useful workaround to the MSAA
path; it does not identify the precise faulty operation or prove a vendor bug.
The override also changes effective surface descriptors, so the application may
select different AA-aware shader/rendering paths.
`tools/dxvk/patches/0004-d3d9-force-single-sample.patch` adds an explicit per-game
`d3d9.forceSingleSample` option, default **False**. After validating the original
sample request, D3D9 surface normalization selects `D3DMULTISAMPLE_NONE` and quality
0. GetDesc, image creation, resolves and sample-dependent state use this effective
description. This intentionally disables MSAA, per-sample masks and
alpha-to-coverage. It is a compatibility override, not transparent MSAA emulation
or a universal PowerVR default. Existing low32, BC-format, present synchronization
and tiler workarounds must remain intact.
## Independent HUD correction
The diagnostic frametimes HUD bound an R8_UINT storage texel buffer to a shader
whose SPIR-V image format was Unknown. This required formatless buffer writes
which the device did not report for that format, producing VUID 07029.
`0003-hud-explicit-r8-texel-format.patch` declares `r8ui` to match the actual host
view. No capabilities are fabricated. The build now compiles and validates the
actual shader, and checks binding 3 for DimBuffer/storage/R8ui. The original shader
fails that contract; the patched shader passes. Real-game trial 15 used the new
DLLs, removed 07029, and retained the original triangle edges. This HUD bug is
separate from the rendering workaround, and is not a proven reset cause.
VUID 06479 for color-format comparison descriptors remains under investigation.
The D3D9 shader uses a conditional comparison path; static validation output alone
does not prove that the offending branch executes. No warning suppression or
false format-feature claims are proposed. Trial 14's LOG+FAIL with a duplicate
limit blocked only the first message of each kind, not every invalid API call;
subsequent trials used logging only. Do not describe these runs as fully blocked
validation or GPU-assisted validation.
## Responsiveness, performance and stability
Paired screenshots prove movement and camera changes, rather than relying on an
input receipt. Trial 17 completed eight samples without a black viewport. Its
cold sample was 3.2 FPS; later recognized samples were 19.1-29.9 FPS, with roughly
2.54 CPU-core equivalents. Camera/workload, capture overhead and thermals were
not controlled; this is not a reliable A/B benchmark.
The planned eight-minute trial 17 stopped at a focus/session guard after the last
confirmed sample at 315.6 seconds. Steam owned focus; the target game was still
available for a single client quit. Its temporary settings were successfully
restored, and the phone boot counter remained 12. The focus change's cause is
unknown. The run is **incomplete**, not an eight-minute pass or a demonstrated
game crash. Controls, focus ownership and sustained stability still need broader
coverage beyond these short tests.
## Native guards, resets and diagnostic controls
The Pixel base is e589e3ae21959c9c74c490d917cebf256acc8ac8. PR561's 9e6fe314 and
3c363910 guards were integrated in an isolated worktree; 26 selected production-C
regressions and a full APK/runtime verification passed. Pixel AHB support remains
present. The existing Lab app was updated without uninstalling or resetting its
Steam account. This APK preceded a second system reset, so the native guards are
not a verified reset fix.
Two resets changed the boot counter 10 -> 11 -> 12. After personal unlock, narrow
kernel/firmware projections confirmed GNOC/access-control details for the first,
and `reset,warm,gnoc_error` with a fresh GNOC numeric record for the second. The
specific faulty component/operation and any relation to rendering are unknown.
`tools/pixel_reset_diagnostics.py` exports only fixed fault categories/numeric
fields, lock state, boot counter and entry times; never arbitrary log text or
account data. Seven host tests cover its gating and narrow output.
Private opt-in diagnostics captured maintenance5 inline modules, which the older
shim path missed. Materialization passed nine actual-C ASan/UBSan cases; benign
pipeline-chain planning passed twelve. Real-game NoContraction, scaled vertex
conversion and force-clip rewrites were actually applied and did not remove the
edges. None is a production default. Sixty-two valid bounded, owned offscreen GPU
cases passed exact pixel readback, fences and validation, covering interpolation,
seams, perspective meshes, interface widening, scaled fetch, HDR/depth prepasses,
real clipping and nonconstant depth. Invalid initial fixtures are excluded. These
passing controls do not exonerate every driver/rendering path.
## Reproducibility and recovery
The runtime artifact supplies both architectures, exact DLL hashes, public patched
DXVK sources, all four patches, wrapper and build recipe. It contains no game
assets, bytecode, saves, account files, screenshots or raw device logs. Source
patches apply cleanly to pinned DXVK revision 25ca63f17f34bdc05a39873ee63907d3cbbfa030.
All five runtime DLL targets were built for both architectures.
Stop further load tests after a full reset. Wait for personal unlock and inspect a
fresh narrow projection before another launch. Temporary profile restoration checks
baseline/current hashes and unchanged shared/other-game settings; it never rolls
back saved game progress. Persistent selection must affect only 202750 and leave
private shader audit layers, validation settings and frametimes HUD disabled.
Routine Heartbeat must not launch games or automatically resume this engineering.
+1
View File
@@ -17,6 +17,7 @@ for patch in "$root"/tools/dxvk/patches/*.patch; do
git -C "$work/source" apply "$patch"
done
python3 "$root/tools/dxvk/check_present_sync.py" "$work/source/src/dxvk/dxvk_presenter.cpp"
python3 "$root/tools/dxvk/check_hud_storage_format.py" "$work/source/src/dxvk/hud/shaders/hud_frame_time_eval.comp"
for arch in 32 64; do
meson setup "$work/build$arch" "$work/source" --cross-file "$work/source/build-win$arch.txt" \
--buildtype release --prefix "$out" --bindir "x$arch" --libdir "x$arch" -Db_ndebug=if-release --strip
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""Compile the actual HUD shader and verify its binding-3 R8_UINT contract.
The frame-time HUD binds an R8_UINT texel-buffer view. An Unknown SPIR-V image
format requires STORAGE_WRITE_WITHOUT_FORMAT, which PowerVR does not report
for that buffer format. Static spirv-val alone accepts both declarations;
this check validates the shader against the known host-side binding contract.
"""
import argparse
from pathlib import Path
import shutil
import struct
import subprocess
import tempfile
def check(shader):
compiler = shutil.which('glslangValidator')
validator = shutil.which('spirv-val')
if not compiler or not validator:
raise RuntimeError('glslangValidator and spirv-val required')
with tempfile.TemporaryDirectory(prefix='dxvk-hud-contract-') as directory:
output = Path(directory) / 'hud.spv'
for command in ([compiler, '-V', '--target-env', 'vulkan1.3', str(shader), '-o', str(output)],
[validator, '--target-env', 'vulkan1.3', str(output)]):
process = subprocess.run(command, capture_output=True, text=True, timeout=30)
if process.returncode:
raise RuntimeError('HUD shader compilation/SPIR-V validation failed')
raw = output.read_bytes()
if len(raw) < 20 or len(raw) % 4:
raise RuntimeError('Malformed SPIR-V')
words = struct.unpack('<' + str(len(raw) // 4) + 'I', raw)
if words[0] != 0x07230203:
raise RuntimeError('Unexpected SPIR-V magic')
bindings, sets, variables, pointers, images = {}, {}, {}, {}, {}
offset = 5
while offset < len(words):
size, opcode = words[offset] >> 16, words[offset] & 0xffff
if not size or size > len(words) - offset:
raise RuntimeError('Malformed SPIR-V instruction')
instruction = words[offset:offset + size]
if opcode == 71 and size == 4: # OpDecorate
if instruction[2] == 33:
bindings[instruction[1]] = instruction[3]
elif instruction[2] == 34:
sets[instruction[1]] = instruction[3]
elif opcode == 59 and size >= 4: # OpVariable
variables[instruction[2]] = instruction[1]
elif opcode == 32 and size == 4: # OpTypePointer
pointers[instruction[1]] = instruction[3]
elif opcode == 25 and size >= 9: # OpTypeImage
images[instruction[1]] = (instruction[3], instruction[7], instruction[8])
offset += size
targets = [images.get(pointers.get(variables.get(identifier)))
for identifier, binding in bindings.items()
if binding == 3 and sets.get(identifier) == 0]
if targets != [(5, 2, 39)]: # DimBuffer, storage image, ImageFormatR8ui
raise RuntimeError('HUD binding 3 must compile to a storage texel buffer with ImageFormatR8ui')
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('shader', type=Path)
args = parser.parse_args()
try:
check(args.shader)
except RuntimeError as error:
parser.exit(1, str(error) + '\n')
print('HUD binding 3: SPIR-V valid, explicit R8ui storage texel-buffer format')
if __name__ == '__main__':
main()
@@ -0,0 +1,12 @@
--- a/src/dxvk/hud/shaders/hud_frame_time_eval.comp
+++ b/src/dxvk/hud/shaders/hud_frame_time_eval.comp
@@ -41,7 +41,8 @@
draw_info_t draw_infos[];
};
-layout(binding = 3)
+// The bound text view is VK_FORMAT_R8_UINT; do not require formatless writes.
+layout(binding = 3, r8ui)
uniform writeonly uimageBuffer text_buffer;
layout(push_constant)
@@ -0,0 +1,66 @@
diff --git a/dxvk.conf b/dxvk.conf
index ba4bc20..26d3127 100644
--- a/dxvk.conf
+++ b/dxvk.conf
@@ -336,6 +336,15 @@
# d3d11.disableMsaa = False
+# Forces D3D9 surfaces to single-sample after validating the original request.
+# The effective surface description reports D3DMULTISAMPLE_NONE; MSAA quality,
+# alpha-to-coverage and per-sample masks are consequently disabled. This is an
+# opt-in compatibility override for games with broken MSAA rendering.
+#
+# Supported values: True, False
+
+# d3d9.forceSingleSample = False
+
# Forces insertion of memory and control barriers after writes to group-shared
# memory in compute shaders. This is only intended to be used as a workaround
diff --git a/src/d3d9/d3d9_common_texture.cpp b/src/d3d9/d3d9_common_texture.cpp
index 9d5fe38..629fea3 100644
--- a/src/d3d9/d3d9_common_texture.cpp
+++ b/src/d3d9/d3d9_common_texture.cpp
@@ -212,6 +212,15 @@ namespace dxvk {
return D3DERR_INVALIDCALL;
}
+ // Keep original request validation above. This opt-in override changes
+ // the effective surface description as well as the backing image.
+ // All surface descriptors and resolves then see the actual sample count.
+ if (ResourceType == D3DRTYPE_SURFACE && options->forceSingleSample
+ && sampleCount != VK_SAMPLE_COUNT_1_BIT) {
+ pDesc->MultiSample = D3DMULTISAMPLE_NONE;
+ pDesc->MultisampleQuality = 0;
+ }
+
// Using MANAGED pool with DYNAMIC usage is illegal
if (IsPoolManaged(pDesc->Pool) && (pDesc->Usage & D3DUSAGE_DYNAMIC))
return D3DERR_INVALIDCALL;
diff --git a/src/d3d9/d3d9_options.cpp b/src/d3d9/d3d9_options.cpp
index e46864e..0cab7dc 100644
--- a/src/d3d9/d3d9_options.cpp
+++ b/src/d3d9/d3d9_options.cpp
@@ -59,6 +59,7 @@ namespace dxvk {
this->memoryTrackTest = config.getOption<bool> ("d3d9.memoryTrackTest", false);
this->forceSamplerTypeSpecConstants = config.getOption<bool> ("d3d9.forceSamplerTypeSpecConstants", false);
this->forceSampleRateShading = config.getOption<bool> ("d3d9.forceSampleRateShading", false);
+ this->forceSingleSample = config.getOption<bool> ("d3d9.forceSingleSample", false);
this->forceAspectRatio = config.getOption<std::string> ("d3d9.forceAspectRatio", "");
this->forceRefreshRate = config.getOption<int32_t> ("d3d9.forceRefreshRate", 0u);
this->modeCountCompatibility = config.getOption<bool> ("d3d9.modeCountCompatibility", false);
diff --git a/src/d3d9/d3d9_options.h b/src/d3d9/d3d9_options.h
index e69a39a..4478420 100644
--- a/src/d3d9/d3d9_options.h
+++ b/src/d3d9/d3d9_options.h
@@ -113,6 +113,9 @@ namespace dxvk {
/// Forces sample rate shading
bool forceSampleRateShading;
+ /// Optional compatibility override: report and render surfaces as single-sample.
+ bool forceSingleSample;
+
/// Allow D3DLOCK_DISCARD
bool allowDiscard;
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""Read-only reboot projection. Never writes raw Android logs or account data.
This is evidence collection, not a GPU fault injector or a root-cause oracle.
The optional kernel projection reads only SYSTEM_LAST_KMSG after user unlock.
It emits fixed categories and numeric hardware fields, never arbitrary log lines.
"""
import argparse
from datetime import datetime, timezone
import json
from pathlib import Path
import re
import subprocess
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
ENTRY = re.compile(r'^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+(SYSTEM_LAST_KMSG)\b', re.M)
CATEGORIES = ('GNOC', 'PVRSRV', 'watchdog', 'IOMMU', 'S2MPU', 'access control', 'Kernel panic', 'HWR', 'GPU fault')
def primary_state(text):
match = re.search(r'Started users state: \[0=(RUNNING_[A-Z_]+)', text)
return match.group(1) if match else 'unknown'
def safe_reason(text):
value = text.strip()
return value if re.fullmatch(r'[A-Za-z0-9_, .():-]{1,160}', value) else 'unknown'
def kernel_projection(text, device_timezone, after=None):
entries = list(ENTRY.finditer(text))
result = {'metadata_dates': [m.group(1) for m in entries], 'raw_body_saved': False,
'root_cause_proven': False}
if not entries:
result['status'] = 'no_accessible_entry'
return result
latest = max(enumerate(entries), key=lambda row: row[1].group(1))
index, match = latest
body = text[match.end():entries[index + 1].start() if index + 1 < len(entries) else len(text)]
result['entry_date_device_local'] = match.group(1)
try:
local = datetime.strptime(match.group(1), '%Y-%m-%d %H:%M:%S').replace(tzinfo=ZoneInfo(device_timezone))
result['entry_date_utc'] = local.astimezone(timezone.utc).isoformat()
except (ValueError, ZoneInfoNotFoundError):
result['status'] = 'unknown_device_timezone'
return result
if after is not None and local < after:
result['status'] = 'stale_entry_not_attributed_to_current_trial'
return result
result['status'] = 'projected_entry_not_a_root_cause'
result['categories'] = {key: len(re.findall(re.escape(key), body, re.I)) for key in CATEGORIES}
patterns = {
'gnoc_addresses': r'Error from GNOC at addr (0x[0-9a-fA-F]+)',
'gnoc_error_types': r'GNOC error type (0x[0-9a-fA-F]+)',
'firmware_reboot_codes': r'Reboot reason:\s+(0x[0-9a-fA-F]+)',
'access_control_addresses': r'Access Control Error \((0x[0-9a-fA-F]+)',
}
result['hardware_fields'] = {key: sorted(set(re.findall(pattern, body))) for key, pattern in patterns.items()}
reasons = re.findall(r'Last boot reason:\s*([A-Za-z0-9_,.-]+)', body)
result['last_boot_reasons'] = sorted(set(reasons))
return result
def collect(adb, serial, include_kernel=False, after=None):
def shell(*args):
try:
process = subprocess.run([adb, '-s', serial, 'shell', *args], capture_output=True,
text=True, timeout=20)
except (OSError, subprocess.TimeoutExpired):
return None, ''
# No command stderr is copied to output: it may contain unrelated data.
return process.returncode, process.stdout if process.returncode == 0 else ''
result = {'at': datetime.now(timezone.utc).isoformat(), 'read_only': True,
'raw_logs_saved': False, 'credentials_collected': False}
code, value = shell('settings', 'get', 'global', 'boot_count')
result['boot_count'] = int(value.strip()) if code == 0 and value.strip().isdigit() else None
code, value = shell('getprop', 'ro.boot.bootreason')
result['bootloader_reason'] = safe_reason(value)
code, value = shell('getprop', 'sys.boot.reason')
result['system_boot_reason'] = safe_reason(value)
code, value = shell('dumpsys', 'user')
result['primary_user_state'] = primary_state(value)
code, value = shell('getprop', 'persist.sys.timezone')
device_timezone = value.strip()
try:
ZoneInfo(device_timezone)
result['device_timezone'] = device_timezone
except (ValueError, ZoneInfoNotFoundError):
result['device_timezone'] = 'unknown'
code, value = shell('dumpsys', 'dropbox', 'SYSTEM_LAST_KMSG')
result['kernel_metadata_dates'] = [m.group(1) for m in ENTRY.finditer(value)]
if include_kernel:
if result['primary_user_state'] != 'RUNNING_UNLOCKED':
result['kernel'] = {'status': 'requires_user_unlock', 'raw_body_saved': False}
else:
code, value = shell('dumpsys', 'dropbox', '--print', 'SYSTEM_LAST_KMSG')
result['kernel'] = kernel_projection(value, result['device_timezone'], after)
result['kernel']['read_exit_code'] = code
return result
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--adb', default='adb')
parser.add_argument('--serial', required=True)
parser.add_argument('--kernel', action='store_true')
parser.add_argument('--after', help='Earliest relevant UTC ISO timestamp; excludes stale kernel entries')
parser.add_argument('--output', type=Path, help='New JSON file; refuses to overwrite')
args = parser.parse_args()
after = datetime.fromisoformat(args.after.replace('Z', '+00:00')) if args.after else None
if after is not None and after.tzinfo is None:
parser.error('--after must include a timezone')
result = collect(args.adb, args.serial, args.kernel, after)
output = json.dumps(result, indent=2) + '\n'
if args.output:
with args.output.open('x') as stream:
stream.write(output)
args.output.chmod(0o600)
print(output, end='')
if __name__ == '__main__':
main()
@@ -0,0 +1,77 @@
"""Test freshness, narrow output, and lock handling without accessing a device."""
from datetime import datetime, timezone
import importlib.util
import json
from pathlib import Path
import subprocess
import unittest
from unittest.mock import patch
spec = importlib.util.spec_from_file_location('reset_projection', Path(__file__).resolve().parents[1] / 'pixel_reset_diagnostics.py')
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
class ResetProjectionTest(unittest.TestCase):
def test_only_primary_state(self):
self.assertEqual(module.primary_state('Private space secret\nStarted users state: [0=RUNNING_UNLOCKED, 10=STOPPED]'), 'RUNNING_UNLOCKED')
self.assertEqual(module.primary_state('State: RUNNING_UNLOCKED'), 'unknown')
def test_stale_body_not_used(self):
raw = '2026-10-10 20:10:09 SYSTEM_LAST_KMSG (text, 1024 bytes)\nGNOC error type 0x2000'
result = module.kernel_projection(raw, 'America/Toronto', datetime(2026, 10, 11, 21, tzinfo=timezone.utc))
self.assertEqual(result['status'], 'stale_entry_not_attributed_to_current_trial')
self.assertNotIn('categories', result)
def test_latest_entry_and_no_arbitrary_body(self):
raw = ('2026-10-10 20:10:09 SYSTEM_LAST_KMSG (text)\nGNOC error type 0x1111\n'
'2026-10-11 17:35:01 SYSTEM_LAST_KMSG (text)\n'
'PRIVATE_TEST_VALUE=a_secret_that_must_not_be_emitted\n'
'Error from GNOC at addr 0x28220000\nGNOC error type 0x2000\n'
'Reboot reason: 0xc005 - Secret arbitrary description\nLast boot reason: reset,warm,gnoc_error\n')
result = module.kernel_projection(raw, 'America/Toronto', datetime(2026, 10, 11, 21, tzinfo=timezone.utc))
self.assertEqual(result['entry_date_utc'], '2026-10-11T21:35:01+00:00')
self.assertEqual(result['hardware_fields']['gnoc_error_types'], ['0x2000'])
self.assertNotIn('0x1111', json.dumps(result))
self.assertNotIn('a_secret', json.dumps(result))
self.assertNotIn('arbitrary description', json.dumps(result))
self.assertFalse(result['root_cause_proven'])
def test_missing_entry_and_timezone(self):
self.assertEqual(module.kernel_projection('Permission denied', 'America/Toronto')['status'], 'no_accessible_entry')
self.assertEqual(module.kernel_projection('2026-10-11 17:00:00 SYSTEM_LAST_KMSG\nGNOC', 'Unknown/Region')['status'], 'unknown_device_timezone')
def test_reason_rejects_paths_and_urls(self):
self.assertEqual(module.safe_reason('reset,warm,gnoc_error\n'), 'reset,warm,gnoc_error')
self.assertEqual(module.safe_reason('https://private.invalid/login?secret=abc'), 'unknown')
self.assertEqual(module.safe_reason('/data/private'), 'unknown')
def test_locked_skips_kernel_body(self):
commands = []
def run(argv, **kwargs):
commands.append(argv)
if argv[-2:] == ['dumpsys', 'user']:
text = 'Started users state: [0=RUNNING_LOCKED]'
elif argv[-1] == 'persist.sys.timezone':
text = 'America/Toronto'
elif argv[-3:] == ['get', 'global', 'boot_count']:
text = '12'
else:
text = ''
return subprocess.CompletedProcess(argv, 0, text, 'PRIVATE_STDERR')
with patch.object(module.subprocess, 'run', side_effect=run):
result = module.collect('adb', 'test-device', True)
self.assertEqual(result['kernel']['status'], 'requires_user_unlock')
self.assertFalse(any('--print' in command for command in commands))
self.assertNotIn('PRIVATE_STDERR', json.dumps(result))
def test_command_failure_does_not_export_stderr(self):
with patch.object(module.subprocess, 'run', return_value=subprocess.CompletedProcess([], 1, 'UNRELATED_DATA', 'PRIVATE_STDERR')):
result = module.collect('adb', 'test-device', True)
self.assertIsNone(result['boot_count'])
self.assertNotIn('UNRELATED_DATA', json.dumps(result))
self.assertNotIn('PRIVATE_STDERR', json.dumps(result))
if __name__ == '__main__':
unittest.main()