diff --git a/app/build.gradle b/app/build.gradle index c7a3455dd..9ca31085b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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) { diff --git a/tools/qualcomm/ahb_probe.c b/tools/qualcomm/ahb_probe.c new file mode 100644 index 000000000..fa3efa02f --- /dev/null +++ b/tools/qualcomm/ahb_probe.c @@ -0,0 +1,47 @@ +#include +#include +#include +#include +#include + +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; +} diff --git a/tools/qualcomm/build_shaders.py b/tools/qualcomm/build_shaders.py new file mode 100644 index 000000000..941465246 --- /dev/null +++ b/tools/qualcomm/build_shaders.py @@ -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') diff --git a/tools/qualcomm/constant-yuv-samplers.patch b/tools/qualcomm/constant-yuv-samplers.patch new file mode 100644 index 000000000..620c17761 --- /dev/null +++ b/tools/qualcomm/constant-yuv-samplers.patch @@ -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); + diff --git a/tools/qualcomm/fetch_runtime.py b/tools/qualcomm/fetch_runtime.py new file mode 100644 index 000000000..2ac404239 --- /dev/null +++ b/tools/qualcomm/fetch_runtime.py @@ -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) diff --git a/tools/qualcomm/probe.py b/tools/qualcomm/probe.py new file mode 100644 index 000000000..9666ed157 --- /dev/null +++ b/tools/qualcomm/probe.py @@ -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() diff --git a/tools/qualcomm/runtime.json b/tools/qualcomm/runtime.json new file mode 100644 index 000000000..16cea7031 --- /dev/null +++ b/tools/qualcomm/runtime.json @@ -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" + } + } +} diff --git a/tools/qualcomm/shader_layer.c b/tools/qualcomm/shader_layer.c new file mode 100644 index 000000000..0bf70a393 --- /dev/null +++ b/tools/qualcomm/shader_layer.c @@ -0,0 +1,94 @@ +#include +#include +#include +#include +#include +#include + +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; +} diff --git a/tools/tests/test_qualcomm_runtime.py b/tools/tests/test_qualcomm_runtime.py new file mode 100644 index 000000000..2215806f4 --- /dev/null +++ b/tools/tests/test_qualcomm_runtime.py @@ -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()