Stage pinned Qualcomm bridge and constant-index shader compatibility

(cherry picked from commit 03ca7778eff0a1216a57c7e7933297c49ea14532)
(cherry picked from commit ca9f416118)
This commit is contained in:
K11MCH1 authored and The412Banner committed 2026-10-25 23:08:48 +00:00
1 parent 5f949856b0
commit 6339d00a7a
9 files changed
+763 -1

No files matched your search

+23 -1
View File
@@ -72,7 +72,7 @@ if (!(treeState in ['clean', 'dirty'])) {
def status = gitValue(['status', '--porcelain'])
treeState = status == null ? 'unknown' : (status.isEmpty() ? 'clean' : 'dirty')
}
buildLabel = "${buildOrigin} ${treeState} · ${buildLabel}"
buildLabel = "${buildOrigin} ${treeState} · ${buildLabel}"
def escapedBuildLabel = buildLabel.replace('\\', '\\\\').replace('"', '\\"')
def sourceCommit = providers.environmentVariable('DROIDDECK_SOURCE_COMMIT').getOrNull()
@@ -80,6 +80,11 @@ def sourceCommit = providers.environmentVariable('DROIDDECK_SOURCE_COMMIT').getO
def prNumber = (providers.environmentVariable('DROIDDECK_PR_NUMBER').getOrNull() ?: '0').with { it.isInteger() ? it : '0' }
def ciBuild = providers.environmentVariable('GITHUB_ACTIONS').getOrNull() == 'true'
if (project.hasProperty('reuseInstalledRuntime')) throw new GradleException('All APKs require prepared Linux runtime packages; runtime reuse is not allowed')
def qualcommRuntime = new groovy.json.JsonSlurper().parse(rootProject.file('tools/qualcomm/runtime.json'))
def qualcommSources = fileTree(rootProject.file('tools/qualcomm')) { include '*.py', '*.c', '*.json', '*.sh', '**/*.patch' }
def qualcommDigest = java.security.MessageDigest.getInstance('SHA-256')
qualcommSources.files.sort { it.name }.each { qualcommDigest.update(it.bytes) }
def qualcommRevision = qualcommDigest.digest().encodeHex().toString()
def releaseSigner = rootProject.file('keystore/release-signer.sha256').text.trim()
def commitTime = (gitValue(['show', '-s', '--format=%ct', '--end-of-options', sourceCommit]) ?: '0').with { it.isLong() ? it : '0' }
def desktopKde = rootProject.file('tools/desktop-kde/release.env').readLines()
@@ -129,6 +134,9 @@ android {
buildConfigField "String", "SOURCE_COMMIT", "\"${sourceCommit}\""
buildConfigField "int", "PR_NUMBER", "${prNumber}"
buildConfigField "boolean", "CI_BUILD", "${ciBuild}"
buildConfigField "String", "QUALCOMM_BRIDGE_SHA256", "\"${qualcommRuntime.sha256}\""
buildConfigField "String", "QUALCOMM_GAMESCOPE_SHA256", "\"${qualcommRuntime.gamescope.member_sha256}\""
buildConfigField "String", "QUALCOMM_FIXES_REVISION", "\"${qualcommRevision}\""
buildConfigField "String", "RELEASE_SIGNER", "\"${releaseSigner}\""
buildConfigField "long", "COMMIT_TIME", "${commitTime}L * 1000L"
buildConfigField "String", "VARIANT_SUFFIXES", "\"${variantSuffixes}\""
@@ -278,6 +286,20 @@ def stageRuntimeAssets = tasks.register('stageRuntimeAssets', Sync) {
into(runtimeAssetsDir)
}
android.sourceSets.main.assets.setSrcDirs([runtimeAssetsDir])
def stageQualcommRuntime = tasks.register('stageQualcommRuntime', Exec) {
workingDir rootProject.projectDir
inputs.files(qualcommSources)
inputs.property('ndkVersion', android.ndkVersion)
outputs.dir(layout.buildDirectory.dir('qualcommAssets'))
commandLine pythonExecutable, 'tools/qualcomm/fetch_runtime.py',
'--output', layout.buildDirectory.dir('qualcommAssets/qualcomm').get().asFile.absolutePath,
'--cache', rootProject.file('build/qualcomm-cache').absolutePath,
'--ndk', android.ndkDirectory.absolutePath
}
android.sourceSets.main.assets.srcDir(layout.buildDirectory.dir('qualcommAssets'))
tasks.configureEach { task ->
if (task.name ==~ /merge.*Assets/) task.dependsOn(stageQualcommRuntime)
}
if (vitureSdkDir) android.sourceSets.main.assets.srcDir(layout.buildDirectory.dir('vitureAssets'))
if (uvcDir) {
def stageUvcNotices = tasks.register('stageUvcNotices', Copy) {
+47
View File
@@ -0,0 +1,47 @@
#include <android/hardware_buffer.h>
#include <dlfcn.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv) {
if (argc != 2) return 2;
void *library = dlopen(argv[1], RTLD_NOW | RTLD_LOCAL);
if (!library) { fprintf(stderr, "%s\n", dlerror()); return 3; }
int (*supported)(void) = dlsym(library, "vkp_android_buffer_supported");
void *(*import_buffer)(AHardwareBuffer *) = dlsym(library, "vkp_image_from_android_buffer");
int (*readback)(void *, uint32_t *, int) = dlsym(library, "vkp_image_readback");
void (*destroy)(void *) = dlsym(library, "vkp_image_destroy");
if (!supported || !import_buffer || !readback || !destroy || !supported()) return 4;
AHardwareBuffer_Desc desc = {
.width = 64, .height = 64, .layers = 1, .format = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM,
.usage = AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY | AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE |
AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT,
};
AHardwareBuffer *buffer = NULL;
if (AHardwareBuffer_allocate(&desc, &buffer)) return 5;
AHardwareBuffer_describe(buffer, &desc);
void *pixels = NULL;
if (AHardwareBuffer_lock(buffer, AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY, -1, NULL, &pixels)) return 6;
for (uint32_t y = 0; y < desc.height; y++)
for (uint32_t x = 0; x < desc.width; x++)
((uint32_t *)pixels)[y * desc.stride + x] = 0xff224466u;
if (AHardwareBuffer_unlock(buffer, NULL)) return 7;
void *image = import_buffer(buffer);
if (!image) return 8;
uint32_t result[64 * 64];
int status = readback(image, result, 64 * 64);
if (status < 0) { fprintf(stderr, "Readback failed: %d\n", status); return 9; }
/* vkp_image_readback converts its input to B8G8R8A8_UNORM. */
const uint32_t expected = 0xff664422u;
for (int i = 0; i < 64 * 64; i++) {
if (result[i] != expected) {
fprintf(stderr, "Pixel %d: %#x expected %#x\n", i, result[i], expected);
return 10;
}
}
destroy(image);
AHardwareBuffer_release(buffer);
puts("PASS: Android hardware buffer imported and 4096 pixels read back through the compositor");
return 0;
}
+52
View File
@@ -0,0 +1,52 @@
"""Build the Gamescope shader workaround and its ARM64 glibc Vulkan layer."""
import json
import os
from pathlib import Path, PurePosixPath
import shutil
import subprocess
import tarfile
import tempfile
def build(output, archive, lock, ndk):
host = 'windows-x86_64' if os.name == 'nt' else 'darwin-x86_64' if os.uname().sysname == 'Darwin' else 'linux-x86_64'
suffix = '.exe' if os.name == 'nt' else ''
toolchain = ndk / 'toolchains/llvm/prebuilt' / host
compiler = toolchain / ('bin/clang' + suffix)
shader_compiler = ndk / 'shader-tools' / host / ('glslc' + suffix)
here = Path(__file__).resolve().parent
output.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix='qualcomm-shaders-') as temporary:
source = Path(temporary)
with tarfile.open(archive, 'r:gz') as package:
for member in package:
path = PurePosixPath(member.name)
relative = PurePosixPath(*path.parts[1:])
if not member.isfile() or '..' in relative.parts:
continue
if not str(relative).startswith('src/shaders/') and str(relative) != 'LICENSE':
continue
target = source / str(relative)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(package.extractfile(member).read())
patch = here / 'constant-yuv-samplers.patch'
subprocess.run(['git', 'apply', '--check', str(patch)], cwd=source, check=True)
subprocess.run(['git', 'apply', str(patch)], cwd=source, check=True)
for fingerprint, name in lock['replacements'].items():
subprocess.run([str(shader_compiler), str(source / 'src/shaders' / (name + '.comp')),
'-o', str(output / (fingerprint + '.spv'))], check=True)
licenses = output / 'licenses'
licenses.mkdir(exist_ok=True)
shutil.copyfile(source / 'LICENSE', licenses / 'Gamescope-LICENSE')
shutil.copyfile(archive, licenses / 'gamescope-source.tar.gz')
shutil.copyfile(patch, licenses / patch.name)
# Only Vulkan types and opaque libc pointers cross this glibc ABI boundary.
subprocess.run([str(compiler), '--target=aarch64-linux-gnu', '--sysroot=' + str(toolchain / 'sysroot'),
'-I' + str(toolchain / 'sysroot/usr/include/aarch64-linux-android'),
'-fuse-ld=lld', '-nostdlib', '-shared', '-fPIC', '-O2', '-fno-stack-protector',
'-Wl,-z,max-page-size=16384', '-Wall', '-Wextra', '-Werror',
str(here / 'shader_layer.c'), '-o', str(output / 'qualcomm-shaders.so')], check=True)
manifest = dict(file_format_version='1.2.0', layer=dict(
name='VK_LAYER_DROIDDECK_qualcomm_shaders', type='GLOBAL', library_path='./qualcomm-shaders.so',
api_version='1.4.0', implementation_version='1', description='Gamescope constant YUV sampler indices for Qualcomm'))
(output / 'VkLayer_DROIDDECK_qualcomm_shaders.json').write_text(json.dumps(manifest) + '\n', encoding='utf-8')
+150
View File
@@ -0,0 +1,150 @@
diff --git a/src/shaders/cs_composite_blit.comp b/src/shaders/cs_composite_blit.comp
index 4d974dc..1bf726d 100644
--- a/src/shaders/cs_composite_blit.comp
+++ b/src/shaders/cs_composite_blit.comp
@@ -15,7 +15,19 @@ layout(
vec4 sampleLayer(uint layerIdx, vec2 uv) {
if ((c_ycbcrMask & (1 << layerIdx)) != 0)
- return sampleLayer(s_ycbcr_samplers[layerIdx], layerIdx, uv, false);
+ {
+ switch (layerIdx) {
+ case 0u: return sampleLayer(s_ycbcr_samplers[0], layerIdx, uv, false);
+ case 1u: return sampleLayer(s_ycbcr_samplers[1], layerIdx, uv, false);
+ case 2u: return sampleLayer(s_ycbcr_samplers[2], layerIdx, uv, false);
+ case 3u: return sampleLayer(s_ycbcr_samplers[3], layerIdx, uv, false);
+ case 4u: return sampleLayer(s_ycbcr_samplers[4], layerIdx, uv, false);
+ case 5u: return sampleLayer(s_ycbcr_samplers[5], layerIdx, uv, false);
+ case 6u: return sampleLayer(s_ycbcr_samplers[6], layerIdx, uv, false);
+ case 7u: return sampleLayer(s_ycbcr_samplers[7], layerIdx, uv, false);
+ }
+ return vec4(0.0);
+ }
return sampleLayer(s_samplers[layerIdx], layerIdx, uv, true);
}
diff --git a/src/shaders/cs_composite_blur.comp b/src/shaders/cs_composite_blur.comp
index 318b0e9..d99193b 100644
--- a/src/shaders/cs_composite_blur.comp
+++ b/src/shaders/cs_composite_blur.comp
@@ -18,7 +18,19 @@ layout(
vec4 sampleLayer(uint layerIdx, vec2 uv) {
if ((c_ycbcrMask & (1 << layerIdx)) != 0)
- return sampleLayer(s_ycbcr_samplers[layerIdx], layerIdx, uv, false);
+ {
+ switch (layerIdx) {
+ case 0u: return sampleLayer(s_ycbcr_samplers[0], layerIdx, uv, false);
+ case 1u: return sampleLayer(s_ycbcr_samplers[1], layerIdx, uv, false);
+ case 2u: return sampleLayer(s_ycbcr_samplers[2], layerIdx, uv, false);
+ case 3u: return sampleLayer(s_ycbcr_samplers[3], layerIdx, uv, false);
+ case 4u: return sampleLayer(s_ycbcr_samplers[4], layerIdx, uv, false);
+ case 5u: return sampleLayer(s_ycbcr_samplers[5], layerIdx, uv, false);
+ case 6u: return sampleLayer(s_ycbcr_samplers[6], layerIdx, uv, false);
+ case 7u: return sampleLayer(s_ycbcr_samplers[7], layerIdx, uv, false);
+ }
+ return vec4(0.0);
+ }
return sampleLayer(s_samplers[layerIdx], layerIdx, uv, true);
}
diff --git a/src/shaders/cs_composite_blur_cond.comp b/src/shaders/cs_composite_blur_cond.comp
index 39b0405..28f4e1e 100644
--- a/src/shaders/cs_composite_blur_cond.comp
+++ b/src/shaders/cs_composite_blur_cond.comp
@@ -18,7 +18,19 @@ layout(
vec4 sampleLayer(uint layerIdx, vec2 uv) {
if ((c_ycbcrMask & (1 << layerIdx)) != 0)
- return sampleLayer(s_ycbcr_samplers[layerIdx], layerIdx, uv, false);
+ {
+ switch (layerIdx) {
+ case 0u: return sampleLayer(s_ycbcr_samplers[0], layerIdx, uv, false);
+ case 1u: return sampleLayer(s_ycbcr_samplers[1], layerIdx, uv, false);
+ case 2u: return sampleLayer(s_ycbcr_samplers[2], layerIdx, uv, false);
+ case 3u: return sampleLayer(s_ycbcr_samplers[3], layerIdx, uv, false);
+ case 4u: return sampleLayer(s_ycbcr_samplers[4], layerIdx, uv, false);
+ case 5u: return sampleLayer(s_ycbcr_samplers[5], layerIdx, uv, false);
+ case 6u: return sampleLayer(s_ycbcr_samplers[6], layerIdx, uv, false);
+ case 7u: return sampleLayer(s_ycbcr_samplers[7], layerIdx, uv, false);
+ }
+ return vec4(0.0);
+ }
return sampleLayer(s_samplers[layerIdx], layerIdx, uv, true);
}
diff --git a/src/shaders/cs_composite_rcas.comp b/src/shaders/cs_composite_rcas.comp
index 3caefb1..269d42c 100644
--- a/src/shaders/cs_composite_rcas.comp
+++ b/src/shaders/cs_composite_rcas.comp
@@ -46,7 +46,19 @@ void FsrRcasInputF(inout float r, inout float g, inout float b) {}
vec4 sampleLayer(uint layerIdx, vec2 uv) {
if ((c_ycbcrMask & (1 << layerIdx)) != 0)
- return sampleLayerEx(s_ycbcr_samplers[layerIdx], layerIdx - 1, layerIdx, uv, false);
+ {
+ switch (layerIdx) {
+ case 0u: return sampleLayerEx(s_ycbcr_samplers[0], layerIdx - 1, layerIdx, uv, false);
+ case 1u: return sampleLayerEx(s_ycbcr_samplers[1], layerIdx - 1, layerIdx, uv, false);
+ case 2u: return sampleLayerEx(s_ycbcr_samplers[2], layerIdx - 1, layerIdx, uv, false);
+ case 3u: return sampleLayerEx(s_ycbcr_samplers[3], layerIdx - 1, layerIdx, uv, false);
+ case 4u: return sampleLayerEx(s_ycbcr_samplers[4], layerIdx - 1, layerIdx, uv, false);
+ case 5u: return sampleLayerEx(s_ycbcr_samplers[5], layerIdx - 1, layerIdx, uv, false);
+ case 6u: return sampleLayerEx(s_ycbcr_samplers[6], layerIdx - 1, layerIdx, uv, false);
+ case 7u: return sampleLayerEx(s_ycbcr_samplers[7], layerIdx - 1, layerIdx, uv, false);
+ }
+ return vec4(0.0);
+ }
return sampleLayerEx(s_samplers[layerIdx], layerIdx - 1, layerIdx, uv, true);
}
diff --git a/src/shaders/cs_composite_rcas_1px.comp b/src/shaders/cs_composite_rcas_1px.comp
index 822aa82..8632ddf 100644
--- a/src/shaders/cs_composite_rcas_1px.comp
+++ b/src/shaders/cs_composite_rcas_1px.comp
@@ -46,7 +46,19 @@ void FsrRcasInputF(inout float r, inout float g, inout float b) {}
vec4 sampleLayer(uint layerIdx, vec2 uv) {
if ((c_ycbcrMask & (1 << layerIdx)) != 0)
- return sampleLayerEx(s_ycbcr_samplers[layerIdx], layerIdx - 1, layerIdx, uv, false);
+ {
+ switch (layerIdx) {
+ case 0u: return sampleLayerEx(s_ycbcr_samplers[0], layerIdx - 1, layerIdx, uv, false);
+ case 1u: return sampleLayerEx(s_ycbcr_samplers[1], layerIdx - 1, layerIdx, uv, false);
+ case 2u: return sampleLayerEx(s_ycbcr_samplers[2], layerIdx - 1, layerIdx, uv, false);
+ case 3u: return sampleLayerEx(s_ycbcr_samplers[3], layerIdx - 1, layerIdx, uv, false);
+ case 4u: return sampleLayerEx(s_ycbcr_samplers[4], layerIdx - 1, layerIdx, uv, false);
+ case 5u: return sampleLayerEx(s_ycbcr_samplers[5], layerIdx - 1, layerIdx, uv, false);
+ case 6u: return sampleLayerEx(s_ycbcr_samplers[6], layerIdx - 1, layerIdx, uv, false);
+ case 7u: return sampleLayerEx(s_ycbcr_samplers[7], layerIdx - 1, layerIdx, uv, false);
+ }
+ return vec4(0.0);
+ }
return sampleLayerEx(s_samplers[layerIdx], layerIdx - 1, layerIdx, uv, true);
}
diff --git a/src/shaders/cs_gaussian_blur_horizontal.comp b/src/shaders/cs_gaussian_blur_horizontal.comp
index 2037f1c..3c202bb 100644
--- a/src/shaders/cs_gaussian_blur_horizontal.comp
+++ b/src/shaders/cs_gaussian_blur_horizontal.comp
@@ -51,7 +51,19 @@ void main()
vec4 layerColor;
// YCBCR technically has incorrect blending here but... meh.
if ((c_ycbcrMask & (1 << i)) != 0)
- layerColor = gaussian_blur(s_ycbcr_samplers[i], i, pos, u_blur_radius, false, false);
+ {
+ switch (i) {
+ case 0: layerColor = gaussian_blur(s_ycbcr_samplers[0], i, pos, u_blur_radius, false, false); break;
+ case 1: layerColor = gaussian_blur(s_ycbcr_samplers[1], i, pos, u_blur_radius, false, false); break;
+ case 2: layerColor = gaussian_blur(s_ycbcr_samplers[2], i, pos, u_blur_radius, false, false); break;
+ case 3: layerColor = gaussian_blur(s_ycbcr_samplers[3], i, pos, u_blur_radius, false, false); break;
+ case 4: layerColor = gaussian_blur(s_ycbcr_samplers[4], i, pos, u_blur_radius, false, false); break;
+ case 5: layerColor = gaussian_blur(s_ycbcr_samplers[5], i, pos, u_blur_radius, false, false); break;
+ case 6: layerColor = gaussian_blur(s_ycbcr_samplers[6], i, pos, u_blur_radius, false, false); break;
+ case 7: layerColor = gaussian_blur(s_ycbcr_samplers[7], i, pos, u_blur_radius, false, false); break;
+ default: layerColor = vec4(0.0); break;
+ }
+ }
else
layerColor = gaussian_blur(s_samplers[i], i, pos, u_blur_radius, false, true);
+66
View File
@@ -0,0 +1,66 @@
"""Stage the checksum-pinned ARM64 glibc bridge, including its corresponding source."""
import argparse
import hashlib
import json
from pathlib import Path
import shutil
import urllib.request
import zipfile
def fetch(cache, lock):
expected = lock['sha256']
cache.mkdir(parents=True, exist_ok=True)
archive = cache / (expected + '.tzst')
def valid(path):
return path.is_file() and path.stat().st_size == lock['size'] and hashlib.sha256(path.read_bytes()).hexdigest() == expected
if not valid(archive):
temporary = archive.with_suffix('.partial')
try:
with urllib.request.urlopen(lock['url'], timeout=120) as source, temporary.open('wb') as target:
shutil.copyfileobj(source, target)
if not valid(temporary):
raise ValueError('Qualcomm bridge archive failed SHA-256 verification')
temporary.replace(archive)
finally:
temporary.unlink(missing_ok=True)
return archive
def stage(output, cache, lock, ndk=None):
archive = fetch(cache, lock)
output.mkdir(parents=True, exist_ok=True)
shutil.copyfile(archive, output / 'runtime.tzst')
gamescope = lock['gamescope']
with zipfile.ZipFile(fetch(cache, gamescope)) as package:
binary = package.read(gamescope['member'])
if hashlib.sha256(binary).hexdigest() != gamescope['member_sha256']:
raise ValueError('Qualcomm Gamescope binary failed SHA-256 verification')
(output / 'gamescope').write_bytes(binary)
if 'wsi' in lock:
with zipfile.ZipFile(fetch(cache, lock['wsi'])) as package:
for entry in lock['wsi']['files']:
target = Path(entry['path'])
if target.is_absolute() or '..' in target.parts:
raise ValueError('Invalid WSI destination')
data = package.read(entry['name'])
if len(data) != entry['size'] or hashlib.sha256(data).hexdigest() != entry['sha256']:
raise ValueError('Qualcomm WSI member failed SHA-256 verification')
destination = output / target
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_bytes(data)
if 'shaders' in lock:
if ndk is None:
raise ValueError('The Android NDK is required to build Qualcomm shader compatibility')
from build_shaders import build
build(output / 'shaders', fetch(cache, lock['shaders']), lock['shaders'], ndk)
(output / 'runtime.json').write_text(json.dumps(lock, indent=2) + '\n', encoding='utf-8')
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--output', type=Path, required=True)
parser.add_argument('--cache', type=Path, required=True)
parser.add_argument('--ndk', type=Path, required=True)
args = parser.parse_args()
stage(args.output, args.cache, json.loads(Path(__file__).with_name('runtime.json').read_text()), args.ndk)
+122
View File
@@ -0,0 +1,122 @@
"""Run a real Vulkan transfer and readback through the selected Linux ICD."""
import ctypes as c
import json
U = c.c_uint32
H = c.c_void_p
Q = c.c_uint64
def structure(name, fields):
return type(name, (c.Structure,), {'_fields_': fields})
Application = structure('Application', [('type', U), ('next', H), ('name', c.c_char_p), ('version', U), ('engine', c.c_char_p), ('engineVersion', U), ('api', U)])
Instance = structure('Instance', [('type', U), ('next', H), ('flags', U), ('application', c.POINTER(Application)), ('layerCount', U), ('layers', H), ('extensionCount', U), ('extensions', H)])
QueueCreate = structure('QueueCreate', [('type', U), ('next', H), ('flags', U), ('family', U), ('count', U), ('priorities', c.POINTER(c.c_float))])
DeviceCreate = structure('DeviceCreate', [('type', U), ('next', H), ('flags', U), ('queueCount', U), ('queues', c.POINTER(QueueCreate)), ('layerCount', U), ('layers', H), ('extensionCount', U), ('extensions', H), ('features', H)])
BufferCreate = structure('BufferCreate', [('type', U), ('next', H), ('flags', U), ('size', Q), ('usage', U), ('sharing', U), ('familyCount', U), ('families', H)])
Requirements = structure('Requirements', [('size', Q), ('alignment', Q), ('types', U)])
MemoryType = structure('MemoryType', [('flags', U), ('heap', U)])
Heap = structure('Heap', [('size', Q), ('flags', U)])
MemoryProperties = structure('MemoryProperties', [('typeCount', U), ('types', MemoryType * 32), ('heapCount', U), ('heaps', Heap * 16)])
Allocate = structure('Allocate', [('type', U), ('next', H), ('size', Q), ('index', U)])
PoolCreate = structure('PoolCreate', [('type', U), ('next', H), ('flags', U), ('family', U)])
CommandAllocate = structure('CommandAllocate', [('type', U), ('next', H), ('pool', H), ('level', U), ('count', U)])
Begin = structure('Begin', [('type', U), ('next', H), ('flags', U), ('inheritance', H)])
Barrier = structure('Barrier', [('type', U), ('next', H), ('source', U), ('destination', U)])
Copy = structure('Copy', [('source', Q), ('destination', Q), ('size', Q)])
Submit = structure('Submit', [('type', U), ('next', H), ('waitCount', U), ('waits', H), ('stages', H), ('commandCount', U), ('commands', c.POINTER(H)), ('signalCount', U), ('signals', H)])
def run():
vk = c.CDLL('libvulkan.so.1')
def function(name, arguments, result=c.c_int):
f = getattr(vk, name)
f.argtypes, f.restype = arguments, result
return f
def check(result, operation):
if result != 0:
raise RuntimeError(f'{operation}: VkResult {result}')
instance, device, pool = H(), H(), H()
buffers, memories = [], []
try:
app = Application(type=0, name=b'DroidDeck Qualcomm probe', api=(1 << 22) | (1 << 12))
info = Instance(type=1, application=c.pointer(app))
check(function('vkCreateInstance', [c.POINTER(Instance), H, c.POINTER(H)])(c.byref(info), None, c.byref(instance)), 'instance')
count = U()
enumerate_devices = function('vkEnumeratePhysicalDevices', [H, c.POINTER(U), c.POINTER(H)])
check(enumerate_devices(instance, c.byref(count), None), 'device count')
if not count.value: raise RuntimeError('No Vulkan devices')
devices = (H * count.value)()
check(enumerate_devices(instance, c.byref(count), devices), 'devices')
physical = H(devices[0])
properties = c.create_string_buffer(4096)
function('vkGetPhysicalDeviceProperties', [H, H], None)(physical, properties)
name = properties.raw[20:276].split(b'\0')[0].decode()
family_count = U()
families_fn = function('vkGetPhysicalDeviceQueueFamilyProperties', [H, c.POINTER(U), H], None)
families_fn(physical, c.byref(family_count), None)
families = (U * (family_count.value * 6))()
families_fn(physical, c.byref(family_count), families)
family = next(i for i in range(family_count.value) if families[i * 6] & 7)
priority = c.c_float(1)
queue_info = QueueCreate(type=2, family=family, count=1, priorities=c.pointer(priority))
device_info = DeviceCreate(type=3, queueCount=1, queues=c.pointer(queue_info))
check(function('vkCreateDevice', [H, c.POINTER(DeviceCreate), H, c.POINTER(H)])(physical, c.byref(device_info), None, c.byref(device)), 'device')
queue = H()
function('vkGetDeviceQueue', [H, U, U, c.POINTER(H)], None)(device, family, 0, c.byref(queue))
memory_props = MemoryProperties()
function('vkGetPhysicalDeviceMemoryProperties', [H, c.POINTER(MemoryProperties)], None)(physical, c.byref(memory_props))
for _ in range(2):
buffer = H()
buffer_info = BufferCreate(type=12, size=4096, usage=3)
check(function('vkCreateBuffer', [H, c.POINTER(BufferCreate), H, c.POINTER(H)])(device, c.byref(buffer_info), None, c.byref(buffer)), 'buffer')
buffers.append(buffer)
requirements = Requirements()
function('vkGetBufferMemoryRequirements', [H, H, c.POINTER(Requirements)], None)(device, buffer, c.byref(requirements))
index = next(i for i in range(memory_props.typeCount) if requirements.types & (1 << i) and memory_props.types[i].flags & 6 == 6)
allocation = Allocate(type=5, size=requirements.size, index=index)
memory = H()
check(function('vkAllocateMemory', [H, c.POINTER(Allocate), H, c.POINTER(H)])(device, c.byref(allocation), None, c.byref(memory)), 'memory')
memories.append(memory)
check(function('vkBindBufferMemory', [H, H, H, Q])(device, buffer, memory, 0), 'bind')
pool_info = PoolCreate(type=39, family=family)
check(function('vkCreateCommandPool', [H, c.POINTER(PoolCreate), H, c.POINTER(H)])(device, c.byref(pool_info), None, c.byref(pool)), 'pool')
command = H()
command_info = CommandAllocate(type=40, pool=pool, count=1)
check(function('vkAllocateCommandBuffers', [H, c.POINTER(CommandAllocate), c.POINTER(H)])(device, c.byref(command_info), c.byref(command)), 'command')
begin = Begin(type=42, flags=1)
check(function('vkBeginCommandBuffer', [H, c.POINTER(Begin)])(command, c.byref(begin)), 'begin')
pattern = 0x5A17C0DE
function('vkCmdFillBuffer', [H, H, Q, Q, U], None)(command, buffers[0], 0, 4096, pattern)
barrier_fn = function('vkCmdPipelineBarrier', [H, U, U, U, U, c.POINTER(Barrier), U, H, U, H], None)
barrier = Barrier(type=46, source=0x1000, destination=0x800)
barrier_fn(command, 0x1000, 0x1000, 0, 1, c.byref(barrier), 0, None, 0, None)
region = Copy(size=4096)
function('vkCmdCopyBuffer', [H, H, H, U, c.POINTER(Copy)], None)(command, buffers[0], buffers[1], 1, c.byref(region))
barrier = Barrier(type=46, source=0x1000, destination=0x2000)
barrier_fn(command, 0x1000, 0x4000, 0, 1, c.byref(barrier), 0, None, 0, None)
check(function('vkEndCommandBuffer', [H])(command), 'end')
submit = Submit(type=4, commandCount=1, commands=c.pointer(command))
check(function('vkQueueSubmit', [H, U, c.POINTER(Submit), H])(queue, 1, c.byref(submit), None), 'submit')
check(function('vkQueueWaitIdle', [H])(queue), 'wait')
mapped = H()
check(function('vkMapMemory', [H, H, Q, Q, U, c.POINTER(H)])(device, memories[1], 0, 4096, 0, c.byref(mapped)), 'map')
words = list((U * 1024).from_address(mapped.value))
function('vkUnmapMemory', [H, H], None)(device, memories[1])
if words != [pattern] * 1024: raise RuntimeError('GPU transfer readback mismatch')
print(json.dumps({'status': 'PASS', 'device': name, 'gpu_fill_copy_readback_bytes': 4096}))
finally:
if device:
function('vkDeviceWaitIdle', [H])(device)
if pool: function('vkDestroyCommandPool', [H, H, H], None)(device, pool, None)
for buffer in buffers: function('vkDestroyBuffer', [H, H, H], None)(device, buffer, None)
for memory in memories: function('vkFreeMemory', [H, H, H], None)(device, memory, None)
function('vkDestroyDevice', [H, H], None)(device, None)
if instance: function('vkDestroyInstance', [H, H], None)(instance, None)
if __name__ == '__main__':
run()
+117
View File
@@ -0,0 +1,117 @@
{
"url": "https://github.com/wallentx/DroidDeck/releases/download/hybris-4ed93fb9ee6e2a0b24ff087196f2e93d247fd985/runtime.tzst",
"sha256": "a4e1ab9581c83ee6feff95e0e527930977a6c14c0e72bd2c4abf777e8472d987",
"size": 5799930,
"provenance": {
"component_ci_run": "https://github.com/wallentx/DroidDeck/actions/runs/39360327540",
"note": "Candidate pin: publish this exact archive and verify its URL before updating release.json.",
"producer_commit": "4ed93fb9ee6e2a0b24ff087196f2e93d247fd985",
"upstream_source_commit": "bc8cb3586b79d208fd21b925fa0209b3e432e008"
},
"gamescope": {
"url": "https://github.com/wallentx/DroidDeck/releases/download/main-af320dd86c7de5738ff16cfeeefbcf42581c199e/DroidDeck-0.5.0-wallentx.4.apk",
"sha256": "f73b229bf5d29c53c4ac618815e77c6e5a3eddd5c99e7c93eab276c831fcef6e",
"size": 124331295,
"source_commit": "af320dd86c7de5738ff16cfeeefbcf42581c199e",
"member": "assets/linuxfs/usr/local/bin/gamescope",
"member_sha256": "2250f26cdf0b6c73af80324c2a99b60465bae46ae6719083b9f9f8d1cf680165"
},
"wsi": {
"files": [
{
"mode": 420,
"name": "libVkLayer_window_system_integration.so",
"path": "wsi/libVkLayer_window_system_integration.so",
"sha256": "d07b961181b9547fd48ad9f05cc6d3b808b824676eb5a186fe3feb8caa013f19",
"size": 436888
},
{
"mode": 420,
"name": "VkLayer_window_system_integration.json",
"path": "wsi/VkLayer_window_system_integration.json",
"sha256": "a1df8b1cee7315015d8fe82534451e373ae4d9cb2adfc232c68346ecbf2818ee",
"size": 3683
},
{
"mode": 420,
"name": "LICENSE",
"path": "licenses/wsi-LICENSE",
"sha256": "5eb8d2da52230d2d66ce078a89ae0173aac63e2e73bed5f907a34736d114ea30",
"size": 1086
},
{
"mode": 420,
"name": "source-commit",
"path": "provenance/wsi-source-commit",
"sha256": "8367150b20502af36c472651850694a15a3e2a1fd6b6beb1f6e12a85a97824f5",
"size": 41
},
{
"mode": 420,
"name": "SHA256SUMS",
"path": "provenance/wsi-SHA256SUMS",
"sha256": "fda6f61028659648801de0e3a6fe1676efd99e1551881a5db8630970eae86158",
"size": 108
},
{
"mode": 420,
"name": "downstream.patch",
"path": "licenses/wsi-downstream.patch",
"sha256": "e2ed113d22f05563248ef606c774c22658b184fbc05912229007cd5281027e78",
"size": 769
},
{
"mode": 420,
"name": "source.tar.zst",
"path": "licenses/wsi-source.tar.zst",
"sha256": "22ae7c2d8aa3dfffdda601c3442d1966ab53ad225acdfadee69ba38974d0c4c5",
"size": 209468
},
{
"mode": 420,
"name": "vulkan-headers-commit",
"path": "provenance/wsi-vulkan-headers-commit",
"sha256": "16706d6cdbeb788426a90d66574dfd71068c4d993d39e76ec5c281b1f30dc7a6",
"size": 41
}
],
"id": "wsi",
"ignored": [],
"provenance": {
"ci_run": "https://github.com/wallentx/DroidDeck/actions/runs/38047627963",
"repository_commit": "abe58618a5194f1f15499cbd5f9ec75503782556",
"vulkan_headers_licenses": {
"note": "Added from tracked assembly inputs; the original WSI archive and compiled library are unchanged.",
"revision": "8d6039a455a7ecc7d2a592ff97f62db4e59b70bf",
"source_urls": [
"https://raw.githubusercontent.com/KhronosGroup/Vulkan-Headers/8d6039a455a7ecc7d2a592ff97f62db4e59b70bf/LICENSE.md",
"https://raw.githubusercontent.com/KhronosGroup/Vulkan-Headers/8d6039a455a7ecc7d2a592ff97f62db4e59b70bf/LICENSES/MIT.txt"
]
}
},
"recipe_inputs": {
"tools/vulkan-wsi/build.sh": "9e403151c58e6b2ee15241f525c4bacb2495f072191da6a63065e7fcc25b3b3c",
"tools/vulkan-wsi/licenses/Vulkan-Headers/LICENSE.md": "95ad366d23fadf701d355bc45fb8b82ae2d700239471d35d41286ac3b08ff903",
"tools/vulkan-wsi/licenses/Vulkan-Headers/MIT.txt": "1ca3502222d967f3be5751c55f6b7ee735b5383909c3b501495f54b216dbf227",
"tools/vulkan-wsi/patches/0001-link-x11-external-memory.patch": "e2ed113d22f05563248ef606c774c22658b184fbc05912229007cd5281027e78",
"tools/vulkan-wsi/source.env": "71a88087265a09b4ce06e7c9b98ef6799c6a7657cc6ef9084de663e96f5cfe63"
},
"sha256": "348f9ec42159c1ee6e2baba4312a5e845d26687238f29e3f4a1b0c3c97e13b3c",
"size": 362717,
"url": "https://github.com/wallentx/DroidDeck/releases/download/v0.3.2-progress.20261010.4/wsi-abe5861.zip"
},
"shaders": {
"url": "https://codeload.github.com/ValveSoftware/gamescope/tar.gz/8f212644c46460549035971ab914421180f61a7c",
"sha256": "40b75e449168caed811824b20716e2e7d236cf1bea6fb7dcfd742d50c8c6d6cc",
"size": 970867,
"source_commit": "8f212644c46460549035971ab914421180f61a7c",
"replacements": {
"05a8bfdf4ff5d71b": "cs_gaussian_blur_horizontal",
"ee0a3fcfa1cf1281": "cs_composite_rcas_1px",
"519ce32f2e53f5db": "cs_composite_rcas",
"ee86eeb49ae9951c": "cs_composite_blur_cond",
"73368f4579fe0d27": "cs_composite_blur",
"80346d82c5a6d850": "cs_composite_blit"
}
}
}
+94
View File
@@ -0,0 +1,94 @@
#include <vulkan/vulkan.h>
#include <vulkan/vk_layer.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
static PFN_vkGetInstanceProcAddr next_instance;
static PFN_vkGetDeviceProcAddr next_device;
static int gamescope;
static PFN_vkVoidFunction VKAPI_CALL instance_proc(VkInstance, const char *);
static PFN_vkVoidFunction VKAPI_CALL device_proc(VkDevice, const char *);
static VkResult VKAPI_CALL create_instance(const VkInstanceCreateInfo *info, const VkAllocationCallbacks *allocator, VkInstance *instance) {
VkLayerInstanceCreateInfo *chain = (void *)info->pNext;
while (chain && (chain->sType != VK_STRUCTURE_TYPE_LOADER_INSTANCE_CREATE_INFO || chain->function != VK_LAYER_LINK_INFO)) chain = (void *)chain->pNext;
if (!chain) return VK_ERROR_INITIALIZATION_FAILED;
next_instance = chain->u.pLayerInfo->pfnNextGetInstanceProcAddr;
gamescope = info->pApplicationInfo && info->pApplicationInfo->pApplicationName &&
!strcmp(info->pApplicationInfo->pApplicationName, "gamescope");
PFN_vkCreateInstance create = (void *)next_instance(0, "vkCreateInstance");
chain->u.pLayerInfo = chain->u.pLayerInfo->pNext;
return create(info, allocator, instance);
}
static VkResult VKAPI_CALL create_device(VkPhysicalDevice physical, const VkDeviceCreateInfo *info, const VkAllocationCallbacks *allocator, VkDevice *device) {
VkLayerDeviceCreateInfo *chain = (void *)info->pNext;
while (chain && (chain->sType != VK_STRUCTURE_TYPE_LOADER_DEVICE_CREATE_INFO || chain->function != VK_LAYER_LINK_INFO)) chain = (void *)chain->pNext;
if (!chain) return VK_ERROR_INITIALIZATION_FAILED;
PFN_vkGetInstanceProcAddr get = chain->u.pLayerInfo->pfnNextGetInstanceProcAddr;
next_device = chain->u.pLayerInfo->pfnNextGetDeviceProcAddr;
PFN_vkCreateDevice create = (void *)get(0, "vkCreateDevice");
chain->u.pLayerInfo = chain->u.pLayerInfo->pNext;
return create(physical, info, allocator, device);
}
static uint64_t fingerprint(const void *data, size_t size) {
const unsigned char *bytes = data;
uint64_t hash = 14695981039346656037ULL;
while (size--) hash = (hash ^ *bytes++) * 1099511628211ULL;
return hash;
}
static VkResult VKAPI_CALL create_shader(VkDevice device, const VkShaderModuleCreateInfo *info, const VkAllocationCallbacks *allocator, VkShaderModule *module) {
PFN_vkCreateShaderModule create = (void *)next_device(device, "vkCreateShaderModule");
const char *directory = getenv("DROIDDECK_QUALCOMM_SHADERS");
void *data = 0;
VkShaderModuleCreateInfo replacement = *info;
if (directory && gamescope) {
char path[4096];
snprintf(path, sizeof(path), "%s/%016llx.spv", directory, (unsigned long long)fingerprint(info->pCode, info->codeSize));
FILE *file = fopen(path, "rb");
if (file) {
fseek(file, 0, SEEK_END);
long size = ftell(file);
rewind(file);
if (size > 20 && size < 4194304 && !(size & 3)) {
data = malloc(size);
if (data && fread(data, 1, size, file) == (size_t)size && *(const uint32_t *)data == 0x07230203) {
replacement.codeSize = size;
replacement.pCode = data;
fprintf(stderr, "Qualcomm: substituting Gamescope shader %016llx\n", (unsigned long long)fingerprint(info->pCode, info->codeSize));
}
}
fclose(file);
}
}
VkResult result = create(device, &replacement, allocator, module);
free(data);
return result;
}
static PFN_vkVoidFunction VKAPI_CALL device_proc(VkDevice device, const char *name) {
if (!strcmp(name, "vkGetDeviceProcAddr")) return (void *)device_proc;
if (!strcmp(name, "vkCreateShaderModule")) return (void *)create_shader;
return next_device ? next_device(device, name) : 0;
}
static PFN_vkVoidFunction VKAPI_CALL instance_proc(VkInstance instance, const char *name) {
if (!strcmp(name, "vkGetInstanceProcAddr")) return (void *)instance_proc;
if (!strcmp(name, "vkGetDeviceProcAddr")) return (void *)device_proc;
if (!strcmp(name, "vkCreateInstance")) return (void *)create_instance;
if (!strcmp(name, "vkCreateDevice")) return (void *)create_device;
if (!strcmp(name, "vkCreateShaderModule")) return (void *)create_shader;
return next_instance ? next_instance(instance, name) : 0;
}
VKAPI_ATTR VkResult VKAPI_CALL vkNegotiateLoaderLayerInterfaceVersion(VkNegotiateLayerInterface *interface) {
if (interface->loaderLayerInterfaceVersion > 2) interface->loaderLayerInterfaceVersion = 2;
interface->pfnGetInstanceProcAddr = instance_proc;
interface->pfnGetDeviceProcAddr = device_proc;
interface->pfnGetPhysicalDeviceProcAddr = 0;
return VK_SUCCESS;
}
+92
View File
@@ -0,0 +1,92 @@
import hashlib
import importlib.util
import io
import json
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
import zipfile
SOURCE = Path(__file__).resolve().parents[1] / 'qualcomm/fetch_runtime.py'
spec = importlib.util.spec_from_file_location('qualcomm_fetch', SOURCE)
runtime = importlib.util.module_from_spec(spec)
spec.loader.exec_module(runtime)
class QualcommRuntimeTest(unittest.TestCase):
def lock(self, data):
return dict(sha256=hashlib.sha256(data).hexdigest(), size=len(data), url='https://example.invalid/runtime')
def test_cached_payload_is_rechecked_before_it_is_used(self):
data = b'approved bridge'
with tempfile.TemporaryDirectory() as directory:
cache = Path(directory)
lock = self.lock(data)
target = cache / (lock['sha256'] + '.tzst')
target.write_bytes(b'x' * len(data))
with patch.object(runtime.urllib.request, 'urlopen', return_value=io.BytesIO(data)):
self.assertEqual(runtime.fetch(cache, lock).read_bytes(), data)
def test_bad_download_never_becomes_the_cached_payload(self):
with tempfile.TemporaryDirectory() as directory:
cache = Path(directory)
with patch.object(runtime.urllib.request, 'urlopen', return_value=io.BytesIO(b'corrupt')):
with self.assertRaisesRegex(ValueError, 'SHA-256'):
runtime.fetch(cache, self.lock(b'approved'))
self.assertEqual(list(cache.iterdir()), [])
def test_only_the_pinned_gamescope_member_is_staged(self):
bridge, binary = b'bridge', b'ARM64 gamescope'
archive = io.BytesIO()
with zipfile.ZipFile(archive, 'w') as package:
package.writestr('assets/gamescope', binary)
package.writestr('other-file', b'not staged')
lock = self.lock(bridge)
lock['gamescope'] = self.lock(archive.getvalue()) | dict(member='assets/gamescope', member_sha256=hashlib.sha256(binary).hexdigest())
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
cache, output = root / 'cache', root / 'output'
cache.mkdir()
for data, entry in [(bridge, lock), (archive.getvalue(), lock['gamescope'])]:
(cache / (entry['sha256'] + '.tzst')).write_bytes(data)
runtime.stage(output, cache, lock)
self.assertEqual((output / 'gamescope').read_bytes(), binary)
self.assertEqual(set(p.name for p in output.iterdir()), {'runtime.tzst', 'gamescope', 'runtime.json'})
self.assertEqual(json.loads((output / 'runtime.json').read_text()), lock)
lock['gamescope']['member_sha256'] = '0' * 64
with self.assertRaisesRegex(ValueError, 'Gamescope'):
runtime.stage(root / 'rejected', cache, lock)
def test_wsi_files_must_match_their_individual_pins(self):
bridge, binary, wsi = b'bridge', b'gamescope', b'WSI library'
def package(name, data):
output = io.BytesIO()
with zipfile.ZipFile(output, 'w') as archive:
archive.writestr(name, data)
return output.getvalue()
gamescope_archive = package('gamescope', binary)
wsi_archive = package('layer.so', wsi)
lock = self.lock(bridge)
lock['gamescope'] = self.lock(gamescope_archive) | dict(member='gamescope', member_sha256=hashlib.sha256(binary).hexdigest())
lock['wsi'] = self.lock(wsi_archive) | dict(files=[dict(
name='layer.so', path='wsi/layer.so', size=len(wsi), sha256='0' * 64)])
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
cache = root / 'cache'
cache.mkdir()
for data, entry in [(bridge, lock), (gamescope_archive, lock['gamescope']), (wsi_archive, lock['wsi'])]:
(cache / (entry['sha256'] + '.tzst')).write_bytes(data)
with self.assertRaisesRegex(ValueError, 'WSI member'):
runtime.stage(root / 'bad-member', cache, lock)
lock['wsi']['files'][0]['sha256'] = hashlib.sha256(wsi).hexdigest()
lock['wsi']['files'][0]['path'] = '../outside.so'
with self.assertRaisesRegex(ValueError, 'destination'):
runtime.stage(root / 'bad-path', cache, lock)
self.assertFalse((root / 'outside.so').exists())
if __name__ == '__main__':
unittest.main()