diff --git a/app/build.gradle b/app/build.gradle index 9ca31085b..201a73ff2 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() @@ -289,6 +289,13 @@ android.sourceSets.main.assets.setSrcDirs([runtimeAssetsDir]) def stageQualcommRuntime = tasks.register('stageQualcommRuntime', Exec) { workingDir rootProject.projectDir inputs.files(qualcommSources) + if (qualcommRuntime.gamescope.build) { + inputs.file(rootProject.file(qualcommRuntime.gamescope.build.binary)) + inputs.file(rootProject.file(qualcommRuntime.gamescope.build.source)) + } + if (qualcommRuntime.mesa) { + inputs.dir(rootProject.file(qualcommRuntime.mesa.directory)) + } inputs.property('ndkVersion', android.ndkVersion) outputs.dir(layout.buildDirectory.dir('qualcommAssets')) commandLine pythonExecutable, 'tools/qualcomm/fetch_runtime.py', diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/QualcommSystemDriver.kt b/app/src/main/java/com/droiddeck/launcher/gpu/QualcommSystemDriver.kt index c2c629cc4..4025466e7 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/QualcommSystemDriver.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/QualcommSystemDriver.kt @@ -13,7 +13,10 @@ object QualcommSystemDriver { const val ID = "system-qualcomm" const val HAL = "/vendor/lib64/hw/vulkan.adreno.so" private val required = listOf("lib/libhybris-vulkan-icd.so.0", "lib/libhybris-common.so.1", "lib/libhybris/linker/q.so", - "bin/gamescope", "shaders/qualcomm-shaders.so", "wsi/libVkLayer_window_system_integration.so") + "bin/gamescope", "shaders/qualcomm-shaders.so", "wsi/libVkLayer_window_system_integration.so", + "mesa/libgallium-26.2.3.so", "mesa/dri/zink_dri.so", "mesa/libGLX_droiddeck_qualcomm.so.0", + "mesa/libEGL_droiddeck_qualcomm.so.0", "mesa/libgbm.so.1", "mesa/gbm/dri_gbm.so", + "mesa/egl-vendor.json", "mesa/qualcomm-gl.sh") @JvmStatic fun detected(): Boolean { if (Build.VERSION.SDK_INT < 29 || !File(HAL).canRead()) return false @@ -36,7 +39,7 @@ object QualcommSystemDriver { val vk = VulkanInfo.query() val version = vk["driver_version_raw"]?.toULongOrNull()?.let { "${(it shr 22) and 0x7fu}.${(it shr 12) and 0x3ffu}.${it and 0xfffu}" } ?: vk["driver_version"] ?: "unknown version" - return "System Qualcomm · ${vk["device"] ?: "Adreno"} · $version (experimental)" + return "System Qualcomm · ${vk["device"] ?: "Adreno"} · $version (experimental)" } @JvmStatic @Synchronized fun prepare(context: Context): File { @@ -55,6 +58,7 @@ object QualcommSystemDriver { copyAssets(context, "qualcomm/wsi", File(staging, "wsi")) copyAssets(context, "qualcomm/shaders", File(staging, "shaders")) copyAssets(context, "qualcomm/licenses", File(staging, "licenses")) + copyAssets(context, "qualcomm/mesa", File(staging, "mesa")) check(required.all { File(staging, it).isFile }) { "The Qualcomm Vulkan bridge is incomplete" } File(staging, ".complete").writeText(sha) FileUtils.delete(dir) diff --git a/tools/linuxfs/desktop/droiddeck-gpu b/tools/linuxfs/desktop/droiddeck-gpu index 17b565f62..26b056d15 100644 --- a/tools/linuxfs/desktop/droiddeck-gpu +++ b/tools/linuxfs/desktop/droiddeck-gpu @@ -27,7 +27,12 @@ if [ "${DROIDDECK_SYSTEM_QUALCOMM:-0}" = 1 ]; then export VK_LAYER_PATH="$DROIDDECK_QUALCOMM_ROOT/shaders" export VK_INSTANCE_LAYERS=VK_LAYER_DROIDDECK_qualcomm_shaders child_env+=("VK_LAYER_PATH=$DROIDDECK_QUALCOMM_ROOT/wsi:$DROIDDECK_QUALCOMM_ROOT/lib" - VK_INSTANCE_LAYERS=VK_LAYER_window_system_integration:VK_LAYER_HYBRIS_compat WSI_X11_FORCE_SHM=1 DISABLE_GAMESCOPE_WSI=1) + VK_INSTANCE_LAYERS=VK_LAYER_window_system_integration:VK_LAYER_HYBRIS_compat WSI_X11_FORCE_SHM=1 DISABLE_GAMESCOPE_WSI=1 + "LD_LIBRARY_PATH=$DROIDDECK_QUALCOMM_ROOT/mesa${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + "LIBGL_DRIVERS_PATH=$DROIDDECK_QUALCOMM_ROOT/mesa/dri" + "GBM_BACKENDS_PATH=$DROIDDECK_QUALCOMM_ROOT/mesa/gbm" + __GLX_VENDOR_LIBRARY_NAME=droiddeck_qualcomm + "__EGL_VENDOR_LIBRARY_FILENAMES=$DROIDDECK_QUALCOMM_ROOT/mesa/egl-vendor.json") fi "${BL_GAMESCOPE_BINARY:-gamescope}" "${args[@]}" -- /usr/bin/env "${child_env[@]}" /usr/local/bin/droiddeck-gpu-window "$@" rc=$? diff --git a/tools/mangoapp/mangoapp b/tools/mangoapp/mangoapp index 16b13743b..7fc9d2dee 100644 --- a/tools/mangoapp/mangoapp +++ b/tools/mangoapp/mangoapp @@ -4,6 +4,7 @@ if [ "${DROIDDECK_SYSTEM_QUALCOMM:-0}" = 1 ]; then export VK_LAYER_PATH="$DROIDDECK_QUALCOMM_ROOT/wsi:$DROIDDECK_QUALCOMM_ROOT/lib" export VK_INSTANCE_LAYERS=VK_LAYER_window_system_integration:VK_LAYER_HYBRIS_compat export WSI_X11_FORCE_SHM=1 DISABLE_GAMESCOPE_WSI=1 + . "$DROIDDECK_QUALCOMM_ROOT/mesa/qualcomm-gl.sh" fi starts=/tmp/droiddeck-mangoapp-starts now=$(date +%s) diff --git a/tools/qualcomm/build_gamescope.py b/tools/qualcomm/build_gamescope.py new file mode 100644 index 000000000..5f8959d9a --- /dev/null +++ b/tools/qualcomm/build_gamescope.py @@ -0,0 +1,84 @@ +"""Build the private Qualcomm Gamescope on ARM64 Linux and retain its source.""" +import argparse +import gzip +import hashlib +import json +from pathlib import Path +import shutil +import subprocess +import tarfile + +ROOT = Path(__file__).resolve().parents[2] +HERE = Path(__file__).resolve().parent +REVISION = '8f212644c46460549035971ab914421180f61a7c' +OPTIONS = ['-Dbuildtype=release', '-Denable_openvr_support=false', + '-Denable_gamescope_wsi_layer=false', '-Denable_tests=false', + '-Ddrm_backend=enabled', '-Dsdl2_backend=enabled', '-Dpipewire=disabled', + '-Davif_screenshots=disabled', '-Dinput_emulation=disabled', '-Dbenchmark=disabled'] + +def run(args, cwd=None): + subprocess.run([str(arg) for arg in args], cwd=cwd, check=True) + +def patches(): + base = ROOT / 'tools/gamescope/patches' + extra = HERE / 'gamescope-patches' + return sorted([*base.glob('*.patch'), *extra.glob('*.patch')], key=lambda p: p.name) + [ + HERE / 'constant-yuv-samplers.patch', HERE / 'gamescope-no-dmabuf.patch'] + +def prepare(source): + if source.exists(): + raise ValueError('Source directory already exists; use a new directory or --prepared-source') + source.mkdir(parents=True) + run(['git', 'init', source]) + run(['git', 'remote', 'add', 'origin', 'https://github.com/ValveSoftware/gamescope.git'], source) + run(['git', 'fetch', '--depth', '1', 'origin', REVISION], source) + run(['git', 'checkout', '--detach', 'FETCH_HEAD'], source) + run(['git', 'submodule', 'update', '--init', '--depth', '1', 'src/reshade', + 'subprojects/libliftoff', 'subprojects/vkroots', 'subprojects/wlroots', + 'thirdparty/SPIRV-Headers'], source) + for patch in patches(): + run(['git', 'apply', '--ignore-whitespace', patch], source) + +def archive_source(source, destination): + with destination.open('wb') as raw, gzip.GzipFile(filename='', mode='wb', fileobj=raw, mtime=0) as compressed: + with tarfile.open(fileobj=compressed, mode='w', format=tarfile.PAX_FORMAT) as package: + for path in sorted(source.rglob('*')): + relative = path.relative_to(source) + if any(part in {'.git', 'build', '__pycache__'} for part in relative.parts) or not path.is_file(): + continue + info = package.gettarinfo(str(path), arcname='gamescope/' + relative.as_posix()) + info.uid = info.gid = info.mtime = 0 + info.uname = info.gname = '' + info.mode = 0o755 if path.suffix == '.sh' else 0o644 + info.pax_headers = {} + with path.open('rb') as data: + package.addfile(info, data) + +def package(source, output, strip_tool='strip'): + output.mkdir(parents=True, exist_ok=True) + shutil.copyfile(source / 'build/src/gamescope', output / 'gamescope') + run([strip_tool, '--strip-unneeded', output / 'gamescope']) + archive_source(source, output / 'source.tar.gz') + manifest = {'source_commit': REVISION, 'options': OPTIONS, + 'patches': {str(p.relative_to(ROOT)).replace('\\', '/'): hashlib.sha256(p.read_bytes()).hexdigest() for p in patches()}, + 'files': {name: {'sha256': hashlib.sha256((output / name).read_bytes()).hexdigest(), + 'size': (output / name).stat().st_size} for name in ['gamescope', 'source.tar.gz']}} + (output / 'build.json').write_text(json.dumps(manifest, indent=2) + '\n') + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--source', type=Path, default=ROOT / 'build/qualcomm-gamescope/source') + parser.add_argument('--output', type=Path, default=ROOT / 'build/qualcomm-gamescope') + parser.add_argument('--prepared-source', action='store_true') + parser.add_argument('--prepare-only', action='store_true') + parser.add_argument('--package-only', action='store_true') + parser.add_argument('--strip', default='strip') + parser.add_argument('--jobs', type=int, default=4) + args = parser.parse_args() + if not args.prepared_source and not args.package_only: + prepare(args.source) + if not args.prepare_only: + if not args.package_only: + run(['meson', 'setup', 'build', *OPTIONS], args.source) + run(['ninja', '-C', 'build', '-j' + str(args.jobs), 'src/gamescope'], args.source) + package(args.source, args.output, args.strip) diff --git a/tools/qualcomm/build_zink.py b/tools/qualcomm/build_zink.py new file mode 100644 index 000000000..8481aa41c --- /dev/null +++ b/tools/qualcomm/build_zink.py @@ -0,0 +1,76 @@ +"""Build private ARM64 Zink libraries with the pre-maintenance5 fallback.""" +import argparse +import hashlib +import json +from pathlib import Path +import shutil +import subprocess + +ROOT = Path(__file__).resolve().parents[2] +HERE = Path(__file__).resolve().parent +REVISION = '31e9a6b2e95e30d84bf3177d1f497d063e59b6b2' +OPTIONS = ['-Dbuildtype=release', '-Dgallium-drivers=zink', '-Dvulkan-drivers=[]', + '-Dllvm=disabled', '-Dplatforms=x11,wayland', '-Dglvnd=enabled', + '-Dglvnd-vendor-name=droiddeck_qualcomm', '-Dgallium-va=disabled', + '-Dvideo-codecs=[]', '-Dbuild-tests=false', '-Dtools=[]', + '-Dglx=dri', '-Degl=enabled', '-Dgbm=enabled'] +LIBRARIES = { + 'libgallium-26.2.3.so': 'src/gallium/targets/dri/libgallium-26.2.3.so', + 'dri/zink_dri.so': 'src/gallium/targets/dril/libdril_dri.so', + 'libGLX_droiddeck_qualcomm.so.0': 'src/glx/libGLX_droiddeck_qualcomm.so.0.0.0', + 'libEGL_droiddeck_qualcomm.so.0': 'src/egl/libEGL_droiddeck_qualcomm.so.0.0.0', + 'libgbm.so.1': 'src/gbm/libgbm.so.1.0.0', + 'gbm/dri_gbm.so': 'src/gbm/backends/dri/dri_gbm.so', +} + +def run(args, cwd=None): + subprocess.run([str(arg) for arg in args], cwd=cwd, check=True) + +def prepare(source): + if source.exists(): + raise ValueError('Source directory already exists; use a new directory or --prepared-source') + source.mkdir(parents=True) + run(['git', 'init', source]) + run(['git', 'remote', 'add', 'origin', 'https://gitlab.freedesktop.org/mesa/mesa.git'], source) + run(['git', 'fetch', '--depth', '1', 'origin', REVISION], source) + run(['git', 'checkout', '--detach', 'FETCH_HEAD'], source) + run(['git', 'apply', HERE / 'zink-optional-maintenance5.patch'], source) + +def package(source, output, strip_tool): + output.mkdir(parents=True, exist_ok=True) + for name, relative in LIBRARIES.items(): + destination = output / name + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(source / 'build' / relative, destination) + run([strip_tool, '--strip-unneeded', destination]) + (output / 'egl-vendor.json').write_text(json.dumps({ + 'file_format_version': '1.0.0', 'ICD': {'library_path': 'libEGL_droiddeck_qualcomm.so.0'}}) + '\n') + shutil.copyfile(HERE / 'qualcomm-gl.sh', output / 'qualcomm-gl.sh') + shutil.copytree(source / 'licenses', output / 'licenses', dirs_exist_ok=True) + shutil.copyfile(source / 'docs/license.rst', output / 'licenses/Mesa-license.rst') + shutil.copyfile(HERE / 'zink-optional-maintenance5.patch', output / 'licenses/zink-optional-maintenance5.patch') + (output / 'licenses/source.json').write_text(json.dumps({ + 'url': 'https://gitlab.freedesktop.org/mesa/mesa/-/tree/' + REVISION, + 'source_commit': REVISION, 'options': OPTIONS, + 'patch_sha256': hashlib.sha256((HERE / 'zink-optional-maintenance5.patch').read_bytes()).hexdigest()}, indent=2) + '\n') + files = [{'path': p.relative_to(output).as_posix(), 'sha256': hashlib.sha256(p.read_bytes()).hexdigest(), + 'size': p.stat().st_size} for p in sorted(output.rglob('*')) if p.is_file() and p.name != 'build.json'] + (output / 'build.json').write_text(json.dumps({'source_commit': REVISION, 'files': files}, indent=2) + '\n') + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--source', type=Path, default=ROOT / 'build/qualcomm-zink/source') + parser.add_argument('--output', type=Path, default=ROOT / 'build/qualcomm-zink/runtime') + parser.add_argument('--prepared-source', action='store_true') + parser.add_argument('--prepare-only', action='store_true') + parser.add_argument('--package-only', action='store_true') + parser.add_argument('--strip', default='strip') + parser.add_argument('--jobs', type=int, default=4) + args = parser.parse_args() + if not args.prepared_source and not args.package_only: + prepare(args.source) + if not args.prepare_only: + if not args.package_only: + run(['meson', 'setup', 'build', *OPTIONS], args.source) + run(['ninja', '-C', 'build', '-j' + str(args.jobs)], args.source) + package(args.source, args.output, args.strip) diff --git a/tools/qualcomm/egl_probe.c b/tools/qualcomm/egl_probe.c new file mode 100644 index 000000000..80cbef341 --- /dev/null +++ b/tools/qualcomm/egl_probe.c @@ -0,0 +1,56 @@ +#include +#include +#include +#include + +int main(void) +{ + EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + EGLint major, minor, count; + const EGLint attributes[] = { EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, + EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, EGL_NONE }; + const EGLint size[] = { EGL_WIDTH, 16, EGL_HEIGHT, 16, EGL_NONE }; + EGLConfig config; + if (!eglInitialize(display, &major, &minor) || !eglBindAPI(EGL_OPENGL_API) || + !eglChooseConfig(display, attributes, &config, 1, &count) || count != 1) { + fprintf(stderr, "EGL setup failed: 0x%x\n", eglGetError()); + return 1; + } + EGLSurface surface = eglCreatePbufferSurface(display, config, size); + EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, NULL); + if (surface == EGL_NO_SURFACE || context == EGL_NO_CONTEXT || + !eglMakeCurrent(display, surface, surface, context)) { + fprintf(stderr, "EGL context failed: 0x%x\n", eglGetError()); + return 1; + } + const char *renderer = (const char *)glGetString(GL_RENDERER); + printf("Renderer: %s\nOpenGL: %s\n", renderer, glGetString(GL_VERSION)); + if (!renderer || !strstr(renderer, "zink") || !strstr(renderer, "Adreno") || + strstr(renderer, "llvmpipe")) return 1; + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + glViewport(0, 0, 16, 16); + glColor4f(1.0f, 0.0f, 0.0f, 1.0f); + glBegin(GL_TRIANGLES); + glVertex2f(-1.0f, -1.0f); + glVertex2f(3.0f, -1.0f); + glVertex2f(-1.0f, 3.0f); + glEnd(); + unsigned char pixels[16 * 16 * 4]; + memset(pixels, 0, sizeof(pixels)); + glReadPixels(0, 0, 16, 16, GL_RGBA, GL_UNSIGNED_BYTE, pixels); + if (glGetError() != GL_NO_ERROR) return 1; + for (unsigned i = 0; i < sizeof(pixels); i += 4) { + if (pixels[i] != 255 || pixels[i + 1] || pixels[i + 2] || pixels[i + 3] != 255) { + fprintf(stderr, "Readback mismatch at pixel %u\n", i / 4); + return 1; + } + } + puts("GPU triangle/readback: all 256 pixels passed"); + eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + eglDestroyContext(display, context); + eglDestroySurface(display, surface); + eglTerminate(display); + return 0; +} diff --git a/tools/qualcomm/fetch_runtime.py b/tools/qualcomm/fetch_runtime.py index 2ac404239..b8820338a 100644 --- a/tools/qualcomm/fetch_runtime.py +++ b/tools/qualcomm/fetch_runtime.py @@ -32,11 +32,48 @@ def stage(output, cache, lock, ndk=None): 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']) + built = gamescope.get('build') + if built: + root = Path(__file__).resolve().parents[2] + def build_input(name): + target = (root / built[name]).resolve() + if not target.is_relative_to(root / 'build'): + raise ValueError('Invalid Qualcomm Gamescope build path') + if not target.is_file(): + raise FileNotFoundError('Build private Gamescope first with tools/qualcomm/build_gamescope.py: ' + str(target)) + return target.read_bytes() + binary = build_input('binary') + source = build_input('source') + if len(source) != built['source_size'] or hashlib.sha256(source).hexdigest() != built['source_sha256']: + raise ValueError('Qualcomm Gamescope source failed SHA-256 verification') + licenses = output / 'licenses' + licenses.mkdir(exist_ok=True) + (licenses / 'gamescope-patched-source.tar.gz').unlink(missing_ok=True) + (licenses / 'gamescope-patched-source.tar.gzip').write_bytes(source) + else: + 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 'mesa' in lock: + root = Path(__file__).resolve().parents[2] + directory = (root / lock['mesa']['directory']).resolve() + if not directory.is_relative_to(root / 'build'): + raise ValueError('Invalid Qualcomm Mesa build path') + for entry in lock['mesa']['files']: + relative = Path(entry['path']) + if relative.is_absolute() or '..' in relative.parts: + raise ValueError('Invalid Qualcomm Mesa destination') + origin = directory / relative + if not origin.is_file(): + raise FileNotFoundError('Build private Zink first with tools/qualcomm/build_zink.py: ' + str(origin)) + data = origin.read_bytes() + if len(data) != entry['size'] or hashlib.sha256(data).hexdigest() != entry['sha256']: + raise ValueError('Qualcomm Mesa member failed SHA-256 verification: ' + entry['path']) + target = output / 'mesa' / relative + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(data) if 'wsi' in lock: with zipfile.ZipFile(fetch(cache, lock['wsi'])) as package: for entry in lock['wsi']['files']: diff --git a/tools/qualcomm/gamescope-no-dmabuf.patch b/tools/qualcomm/gamescope-no-dmabuf.patch new file mode 100644 index 000000000..3df881606 --- /dev/null +++ b/tools/qualcomm/gamescope-no-dmabuf.patch @@ -0,0 +1,124 @@ +--- a/src/rendervulkan.hpp ++++ b/src/rendervulkan.hpp +@@ -866,6 +866,7 @@ + inline VkPipelineLayout pipelineLayout() {return m_pipelineLayout;} + inline int drmRenderFd() {return m_drmRendererFd;} + inline bool supportsModifiers() {return m_bSupportsModifiers;} ++ inline bool supportsDmaBuf() {return m_bSupportsDmaBuf;} + inline uint32_t vendorID() const {return m_uVendorID;} + inline const VkPhysicalDeviceMemoryProperties &memoryProperties() const {return m_memoryProperties;} + inline bool supportsMutableSwapchain() {return m_bSupportsMutableSwapchain;} +@@ -938,6 +939,7 @@ + uint32_t m_uVendorID = 0; + bool m_bHasDrmPrimaryDevId = false; + bool m_bSupportsModifiers = false; ++ bool m_bSupportsDmaBuf = false; + bool m_bSupportsMutableSwapchain = false; + bool m_bSupportsPresentWait = false; + bool m_bInitialized = false; +--- a/src/rendervulkan.cpp ++++ b/src/rendervulkan.cpp +@@ -452,10 +452,16 @@ + + bool hasDrmProps = vulkan_has_drm_props(); + bool supportsForeignQueue = false; ++ bool supportsMemoryFd = false; ++ bool supportsDmaBuf = false; + bool supportsHDRMetadata = false; + bool presentIdExtension = false; + bool presentWaitExtension = false; + for (const auto& ext : m_supportedExts) { ++ if ( strcmp(ext.extensionName, VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME) == 0 ) ++ supportsMemoryFd = true; ++ if ( strcmp(ext.extensionName, VK_EXT_EXTERNAL_MEMORY_DMA_BUF_EXTENSION_NAME) == 0 ) ++ supportsDmaBuf = true; + if ( strcmp(ext.extensionName, VK_EXT_IMAGE_DRM_FORMAT_MODIFIER_EXTENSION_NAME) == 0 ) + m_bSupportsModifiers = true; + +@@ -471,6 +477,18 @@ + + if ( strcmp(ext.extensionName, VK_EXT_HDR_METADATA_EXTENSION_NAME) == 0 ) + supportsHDRMetadata = true; ++ } ++ ++ m_bSupportsDmaBuf = supportsMemoryFd && supportsDmaBuf; ++ if ( !m_bSupportsDmaBuf ) ++ { ++ if ( !GetBackend()->UsesVulkanSwapchain() ) ++ { ++ vk_log.errorf( "This backend requires DMA-BUF import/export support" ); ++ return false; ++ } ++ m_bSupportsModifiers = false; ++ vk_log.infof( "DMA-BUF unavailable; using SHM inputs with Vulkan swapchain rendering" ); + } + + vk_log.infof( "physical device %s DRM format modifiers", m_bSupportsModifiers ? "supports" : "does not support" ); +@@ -617,8 +635,11 @@ + enabledExtensions.push_back( VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME ); + } + +- enabledExtensions.push_back( VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME ); +- enabledExtensions.push_back( VK_EXT_EXTERNAL_MEMORY_DMA_BUF_EXTENSION_NAME ); ++ if ( m_bSupportsDmaBuf ) ++ { ++ enabledExtensions.push_back( VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME ); ++ enabledExtensions.push_back( VK_EXT_EXTERNAL_MEMORY_DMA_BUF_EXTENSION_NAME ); ++ } + + enabledExtensions.push_back( VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME ); + +@@ -2144,6 +2165,11 @@ + { + flags.bExportable = true; + } ++ if ( ( pDMA || flags.bExportable ) && !g_device.supportsDmaBuf() ) ++ { ++ vk_log.errorf( "DMA-BUF texture requested without import/export support" ); ++ return false; ++ } + + if ( flags.bTransferSrc == true ) + { +@@ -2983,6 +3009,8 @@ + } + + wlr_drm_format_set_add( &sampledShmFormats, drmFormat, DRM_FORMAT_MOD_LINEAR ); ++ if ( !g_device.supportsDmaBuf() ) ++ return true; + + if ( g_device.supportsModifiers() ) + { +@@ -4836,7 +4864,7 @@ + { + if (buffer_caps & WLR_BUFFER_CAP_DMABUF) + { +- return &sampledDRMFormats; ++ return g_device.supportsDmaBuf() ? &sampledDRMFormats : nullptr; + } + else if (buffer_caps & WLR_BUFFER_CAP_DATA_PTR) + { +@@ -4878,7 +4906,10 @@ + struct wlr_renderer *vulkan_renderer_create( void ) + { + VulkanRenderer_t *renderer = new VulkanRenderer_t(); +- wlr_renderer_init(&renderer->base, &renderer_impl, WLR_BUFFER_CAP_DMABUF | WLR_BUFFER_CAP_DATA_PTR); ++ uint32_t caps = WLR_BUFFER_CAP_DATA_PTR; ++ if ( g_device.supportsDmaBuf() ) ++ caps |= WLR_BUFFER_CAP_DMABUF; ++ wlr_renderer_init(&renderer->base, &renderer_impl, caps); + return &renderer->base; + } + +--- a/src/wlserver.cpp ++++ b/src/wlserver.cpp +@@ -2243,7 +2243,8 @@ + + // Have to make this old ancient thing for compat with older XWayland. + // Someday, he will be purged. +- wlr_drm_create(wlserver.display, wlserver.wlr.renderer); ++ if ( wlr_renderer_get_texture_formats( wlserver.wlr.renderer, WLR_BUFFER_CAP_DMABUF ) != nullptr ) ++ wlr_drm_create(wlserver.display, wlserver.wlr.renderer); + + if ( GetBackend()->SupportsExplicitSync() ) + { diff --git a/tools/qualcomm/gamescope-patches/0116-release-cursor-shm-buffers.patch b/tools/qualcomm/gamescope-patches/0116-release-cursor-shm-buffers.patch new file mode 100644 index 000000000..03b4d7fd0 --- /dev/null +++ b/tools/qualcomm/gamescope-patches/0116-release-cursor-shm-buffers.patch @@ -0,0 +1,15 @@ +--- a/src/wlserver.cpp ++++ b/src/wlserver.cpp +@@ -203,6 +203,12 @@ + } + + void xwayland_surface_commit(struct wlr_surface *wlr_surface) { ++ // Cursor surfaces never acquire an X11/xdg window association. Keeping their ++ // commits in g_PendingCommits pins every old SHM buffer until the cursor ++ // surface is destroyed. X11 cursor images are handled by MouseCursor. ++ if ( wlr_surface->role && strcmp( wlr_surface->role->name, "wl_pointer-cursor" ) == 0 ) ++ return; ++ + wlr_surface->current.committed = 0; + + wlserver_x11_surface_info *wlserver_x11_surface_info = get_wl_surface_info(wlr_surface)->x11_surface; diff --git a/tools/qualcomm/gamescope-patches/0121-powervr-single-pixel-rcas.patch b/tools/qualcomm/gamescope-patches/0121-powervr-single-pixel-rcas.patch new file mode 100644 index 000000000..d2b3aafea --- /dev/null +++ b/tools/qualcomm/gamescope-patches/0121-powervr-single-pixel-rcas.patch @@ -0,0 +1,15 @@ +diff --git a/src/rendervulkan.cpp b/src/rendervulkan.cpp +--- a/src/rendervulkan.cpp ++++ b/src/rendervulkan.cpp +@@ -996,8 +996,9 @@ + SHADER(BLUR, cs_composite_blur); + SHADER(BLUR_COND, cs_composite_blur_cond); + SHADER(BLUR_FIRST_PASS, cs_gaussian_blur_horizontal); +- // The one pixel layout is only measured on Adreno, every other vendor keeps the quad swizzle. +- if (m_uVendorID == 0x5143) /* Qualcomm */ ++ // PowerVR rejects the quad-swizzled RCAS for 3+ layers with YCbCr mask 2. ++ // The existing one-pixel shader compiles those variants and covers the same 16x16 tile. ++ if (m_uVendorID == 0x5143 || m_uVendorID == 0x1010) /* Qualcomm or Imagination */ + { + SHADER(RCAS, cs_composite_rcas_1px); + } diff --git a/tools/qualcomm/gamescope-patches/0122-powervr-linear-dmabuf-import.patch b/tools/qualcomm/gamescope-patches/0122-powervr-linear-dmabuf-import.patch new file mode 100644 index 000000000..40a913560 --- /dev/null +++ b/tools/qualcomm/gamescope-patches/0122-powervr-linear-dmabuf-import.patch @@ -0,0 +1,156 @@ +diff --git a/src/rendervulkan.cpp b/src/rendervulkan.cpp +--- a/src/rendervulkan.cpp ++++ b/src/rendervulkan.cpp +@@ -13,6 +13,7 @@ + #include + #include "vulkan_include.h" + #include "nested_swapchain.hpp" ++#include "dmabuf_import.hpp" + #include "Utils/Algorithm.h" + + #if defined(__linux__) +@@ -2229,8 +2230,27 @@ + return false; + } + +- if ( res == VK_SUCCESS && +- ( externalImageProperties.externalMemoryProperties.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_IMPORTABLE_BIT ) ) ++ bool importable = res == VK_SUCCESS && ++ ( externalImageProperties.externalMemoryProperties.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_IMPORTABLE_BIT ); ++ if ( gamescope::RetryPowerVRLinearQuery( g_device.vendorID(), imageInfo.format, ++ pDMA->modifier, pDMA->n_planes, imageInfo.flags, res ) ) ++ { ++ // PowerVR's mutable-format query rejects this linear import even though ++ // explicit-layout imports and UNORM/sRGB GPU sampling work. Query the base ++ // format without changing the image we actually create or its supplied stride. ++ VkImageCreateInfo baseFormat = imageInfo; ++ baseFormat.flags &= ~VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; ++ baseFormat.pNext = nullptr; ++ res = getModifierProps( &baseFormat, pDMA->modifier, &externalImageProperties ); ++ importable = res == VK_SUCCESS && ++ ( externalImageProperties.externalMemoryProperties.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_IMPORTABLE_BIT ); ++ } ++ if ( !importable && g_device.vendorID() == 0x1010 ) ++ { ++ vk_log.errorf( "PowerVR DMA-BUF modifier is not importable; refusing a different tiling" ); ++ return false; ++ } ++ if ( importable ) + { + modifierInfo = { + .sType = VK_STRUCTURE_TYPE_IMAGE_DRM_FORMAT_MODIFIER_EXPLICIT_CREATE_INFO_EXT, +@@ -2348,6 +2368,22 @@ + + m_format = imageInfo.format; + ++ VkMemoryFdPropertiesKHR fdProperties = { ++ .sType = VK_STRUCTURE_TYPE_MEMORY_FD_PROPERTIES_KHR, ++ }; ++ if ( pDMA ) ++ { ++ if ( !g_device.vk.GetMemoryFdPropertiesKHR ) ++ return false; ++ res = g_device.vk.GetMemoryFdPropertiesKHR( g_device.device(), ++ VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, pDMA->fd[0], &fdProperties ); ++ if ( res != VK_SUCCESS ) ++ { ++ vk_errorf( res, "vkGetMemoryFdPropertiesKHR failed" ); ++ return false; ++ } ++ } ++ + res = g_device.vk.CreateImage(g_device.device(), &imageInfo, nullptr, &m_vkImage); + if (res != VK_SUCCESS) { + vk_errorf( res, "vkCreateImage failed" ); +@@ -2357,10 +2393,22 @@ + VkMemoryRequirements memRequirements; + g_device.vk.GetImageMemoryRequirements(g_device.device(), m_vkImage, &memRequirements); + ++ uint32_t memoryType = pDMA ++ ? gamescope::DmabufMemoryType( g_device.memoryProperties(), memRequirements.memoryTypeBits, ++ fdProperties.memoryTypeBits, properties ) ++ : uint32_t( g_device.findMemoryType( properties, memRequirements.memoryTypeBits ) ); ++ if ( memoryType == UINT32_MAX ) ++ { ++ vk_log.errorf( "No compatible image memory type (image=0x%x, fd=0x%x)", ++ memRequirements.memoryTypeBits, fdProperties.memoryTypeBits ); ++ g_device.vk.DestroyImage( g_device.device(), m_vkImage, nullptr ); ++ m_vkImage = VK_NULL_HANDLE; ++ return false; ++ } + VkMemoryAllocateInfo allocInfo = { + .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + .allocationSize = memRequirements.size, +- .memoryTypeIndex = uint32_t(g_device.findMemoryType(properties, memRequirements.memoryTypeBits)), ++ .memoryTypeIndex = memoryType, + }; + + m_size = allocInfo.allocationSize; +@@ -2453,6 +2501,13 @@ + if ( res != VK_SUCCESS ) + { + vk_errorf( res, "vkBindImageMemory failed" ); ++ if ( pDMA ) ++ vk_log.errorf( "DMA-BUF: %ux%u format=%u modifier=0x%" PRIx64 ++ " planes=%d stride=%u offset=%u tiling=%u allocation=%" PRIu64 ++ " memoryType=%u imageBits=0x%x fdBits=0x%x", width, height, imageInfo.format, ++ pDMA->modifier, pDMA->n_planes, pDMA->stride[0], pDMA->offset[0], imageInfo.tiling, ++ uint64_t( allocInfo.allocationSize ), allocInfo.memoryTypeIndex, ++ memRequirements.memoryTypeBits, fdProperties.memoryTypeBits ); + return false; + } + +diff --git a/src/rendervulkan.hpp b/src/rendervulkan.hpp +--- a/src/rendervulkan.hpp ++++ b/src/rendervulkan.hpp +@@ -791,6 +791,7 @@ + VK_FUNC(GetImageMemoryRequirements) \ + VK_FUNC(GetImageSubresourceLayout) \ + VK_FUNC(GetMemoryFdKHR) \ ++ VK_FUNC(GetMemoryFdPropertiesKHR) \ + VK_FUNC(GetQueryPoolResults) \ + VK_FUNC(GetSemaphoreCounterValue) \ + VK_FUNC(GetSwapchainImagesKHR) \ +@@ -865,6 +866,8 @@ + inline VkPipelineLayout pipelineLayout() {return m_pipelineLayout;} + inline int drmRenderFd() {return m_drmRendererFd;} + inline bool supportsModifiers() {return m_bSupportsModifiers;} ++ inline uint32_t vendorID() const {return m_uVendorID;} ++ inline const VkPhysicalDeviceMemoryProperties &memoryProperties() const {return m_memoryProperties;} + inline bool supportsMutableSwapchain() {return m_bSupportsMutableSwapchain;} + inline bool supportsPresentWait() {return m_bSupportsPresentWait;} + inline bool hasDrmPrimaryDevId() {return m_bHasDrmPrimaryDevId;} +diff --git a/src/dmabuf_import.hpp b/src/dmabuf_import.hpp +new file mode 100644 +--- /dev/null ++++ b/src/dmabuf_import.hpp +@@ -0,0 +1,29 @@ ++#pragma once ++#include ++ ++namespace gamescope ++{ ++inline bool RetryPowerVRLinearQuery( uint32_t vendor, VkFormat format, uint64_t modifier, ++ uint32_t planes, VkImageCreateFlags flags, VkResult result ) ++{ ++ return vendor == 0x1010 && modifier == 0 && planes == 1 && ++ flags == VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT && result == VK_ERROR_FORMAT_NOT_SUPPORTED && ++ ( format == VK_FORMAT_R8G8B8A8_UNORM || format == VK_FORMAT_B8G8R8A8_UNORM ); ++} ++ ++inline uint32_t DmabufMemoryType( const VkPhysicalDeviceMemoryProperties &memory, ++ uint32_t imageBits, uint32_t fdBits, VkMemoryPropertyFlags preferred ) ++{ ++ uint32_t fallback = UINT32_MAX; ++ const uint32_t compatible = imageBits & fdBits; ++ for ( uint32_t i = 0; i < memory.memoryTypeCount; ++i ) ++ { ++ if ( !( compatible & ( 1u << i ) ) ) continue; ++ if ( fallback == UINT32_MAX ) fallback = i; ++ if ( ( memory.memoryTypes[i].propertyFlags & preferred ) == preferred ) return i; ++ } ++ // Device-local is a preference for an existing imported buffer. CPU mapping ++ // properties are requirements and must never be silently relaxed. ++ return preferred == VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT ? fallback : UINT32_MAX; ++} ++} diff --git a/tools/qualcomm/gamescope-patches/0123-powervr-rgba-copy-surface.patch b/tools/qualcomm/gamescope-patches/0123-powervr-rgba-copy-surface.patch new file mode 100644 index 000000000..72344e6bc --- /dev/null +++ b/tools/qualcomm/gamescope-patches/0123-powervr-rgba-copy-surface.patch @@ -0,0 +1,61 @@ +diff --git a/src/nested_surface_format.hpp b/src/nested_surface_format.hpp +new file mode 100644 +--- /dev/null ++++ b/src/nested_surface_format.hpp +@@ -0,0 +1,31 @@ ++#pragma once ++ ++#include ++ ++namespace gamescope ++{ ++// The SDR composition shader declares rgba8, which requires an R8G8B8A8_UNORM ++// storage view. Binding a BGRA view is a format mismatch; a same-format copy ++// cannot correct it. Keep the workaround scoped to the PowerVR SDR copy path ++// where the channel reversal was reproduced, not game-facing texture formats. ++// On success, formatCount means the existing selector should choose the format. ++// On failure, no matching RGBA SDR surface was advertised for this scoped path. ++inline bool ChooseNestedSDRCopySurfaceFormat( uint32_t vendorId, bool copy, bool hdr, ++ const VkSurfaceFormatKHR *formats, uint32_t formatCount, uint32_t &surfaceFormat ) ++{ ++ surfaceFormat = formatCount; ++ if ( vendorId != 0x1010 || !copy || hdr ) ++ return true; ++ ++ for ( uint32_t i = 0; i < formatCount; ++i ) ++ { ++ if ( formats[i].format == VK_FORMAT_R8G8B8A8_UNORM && ++ formats[i].colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR ) ++ { ++ surfaceFormat = i; ++ return true; ++ } ++ } ++ return false; ++} ++} +diff --git a/src/rendervulkan.cpp b/src/rendervulkan.cpp +--- a/src/rendervulkan.cpp ++++ b/src/rendervulkan.cpp +@@ -14,6 +14,7 @@ + #include "vulkan_include.h" + #include "nested_swapchain.hpp" + #include "dmabuf_import.hpp" ++#include "nested_surface_format.hpp" + #include "Utils/Algorithm.h" + + #if defined(__linux__) +@@ -3330,6 +3331,13 @@ + uint32_t surfaceFormat = formatCount; + VkColorSpaceKHR preferredColorSpace = g_bOutputHDREnabled ? VK_COLOR_SPACE_HDR10_ST2084_EXT : VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; + ++ if ( !gamescope::ChooseNestedSDRCopySurfaceFormat( g_device.vendorID(), config.copy, ++ g_bOutputHDREnabled, pOutput->surfaceFormats.data(), formatCount, surfaceFormat ) ) ++ { ++ vk_log.errorf( "PowerVR copy composition requires an RGBA SDR surface" ); ++ return false; ++ } ++ + if ( surfaceFormat == formatCount ) + { + for ( surfaceFormat = 0; surfaceFormat < formatCount; surfaceFormat++ ) diff --git a/tools/qualcomm/gamescope-patches/0125-sdl-wait-before-present.patch b/tools/qualcomm/gamescope-patches/0125-sdl-wait-before-present.patch new file mode 100644 index 000000000..d8037df9c --- /dev/null +++ b/tools/qualcomm/gamescope-patches/0125-sdl-wait-before-present.patch @@ -0,0 +1,23 @@ +diff --git a/src/Backends/SDLBackend.cpp b/src/Backends/SDLBackend.cpp +--- a/src/Backends/SDLBackend.cpp ++++ b/src/Backends/SDLBackend.cpp +@@ -351,14 +351,15 @@ namespace gamescope + std::optional oCompositeResult = vulkan_composite( (FrameInfo_t *)pFrameInfo, nullptr, false ); + if ( !oCompositeResult ) + return -EINVAL; + ++ // Present has no render-finished semaphore, so wait before the native consumer can read ++ // the old contents. This serializes composition and present; a future pipelined path must ++ // signal a binary render-finished semaphore and make vkQueuePresentKHR wait on it. ++ vulkan_wait( *oCompositeResult, true ); ++ + vulkan_present_to_window(); + + // TODO: Hook up PresentationFeedback. +- +- // Wait for the composite result on our side *after* we +- // commit the buffer to the compositor to avoid a bubble. +- vulkan_wait( *oCompositeResult, true ); + + GetVBlankTimer().UpdateWasCompositing( true ); + GetVBlankTimer().UpdateLastDrawTime( get_time_in_nanos() - g_SteamCompMgrVBlankTime.ulWakeupTime ); diff --git a/tools/qualcomm/gamescope-patches/0126-shm-commit-ready.patch b/tools/qualcomm/gamescope-patches/0126-shm-commit-ready.patch new file mode 100644 index 000000000..1c5affc9a --- /dev/null +++ b/tools/qualcomm/gamescope-patches/0126-shm-commit-ready.patch @@ -0,0 +1,176 @@ +diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp +--- a/src/steamcompmgr.cpp ++++ b/src/steamcompmgr.cpp +@@ -95,6 +95,7 @@ + #include "Script/Script.h" + #include "refresh_rate.h" + #include "commit.h" ++#include "buffer_commit_sync.hpp" + #include "reshade_effect_manager.hpp" + #include "BufferMemo.h" + #include "vrclient_detect.h" +@@ -8451,7 +8452,6 @@ void update_wayland_res(CommitDoneList_t *doneCommits, steamcompmgr_win_t *w, Re + return; + } + +- int fence = -1; + global_focus_t *pCurrentFocus = GetCurrentFocus(); + + static bool bMangoappSocketDisable = env_to_bool( getenv( "GAMESCOPE_MANGOAPP_SOCKET_DISABLE" )); +@@ -8492,8 +8492,6 @@ void update_wayland_res(CommitDoneList_t *doneCommits, steamcompmgr_win_t *w, Re + bool bValidPreemptiveScale = reslistentry.pAcquirePoint && pUpscaleFocus && cv_upscale_preemptive; + bool bPreemptiveUpscale = bValidPreemptiveScale && newCommit->ShouldPreemptivelyUpscale( pUpscaleFocus->eUpscaleFilter, pUpscaleFocus->eUpscaleScaler ); + +- bool bKnownReady = false; +- + std::pair eventFd = gamescope::CAcquireTimelinePoint::k_InvalidEvent; + + if ( bPreemptiveUpscale ) +@@ -8589,31 +8587,44 @@ void update_wayland_res(CommitDoneList_t *doneCommits, steamcompmgr_win_t *w, Re + pBackendFb->SetBuffer( buf ); + } + +- if ( eventFd != gamescope::CAcquireTimelinePoint::k_InvalidEvent ) +- { +- fence = eventFd.first; +- bKnownReady = eventFd.second; +- } +- else +- { +- struct wlr_dmabuf_attributes dmabuf = {0}; +- if ( wlr_buffer_get_dmabuf( buf, &dmabuf ) ) +- { +- fence = dup( dmabuf.fd[0] ); +- } +- else ++ gamescope::BufferCommitSync commitSync = gamescope::SelectBufferCommitSync( ++ eventFd != gamescope::CAcquireTimelinePoint::k_InvalidEvent, ++ eventFd.first, ++ eventFd.second, ++ [&]() -> std::optional { ++ struct wlr_dmabuf_attributes dmabuf = {0}; ++ if ( wlr_buffer_get_dmabuf( buf, &dmabuf ) ) ++ { ++ int fd = dup( dmabuf.fd[0] ); ++ if ( fd < 0 ) ++ xwm_log.errorf( "Failed to duplicate DMA-BUF fence: %d", errno ); ++ return fd; ++ } ++ return std::nullopt; ++ } ); ++ if ( commitSync.source == gamescope::BufferCommitSyncSource::SynchronousUpload ) ++ { ++ // vulkan_create_texture_from_wlr_buffer waited for this SHM upload to finish. ++ // Exporting its image memory as a DMA-BUF does not produce a sync fence and can ++ // return an invalid fd on drivers that do not export the allocation. ++ static bool loggedSynchronousUpload = false; ++ if ( !loggedSynchronousUpload ) + { +- fence = newCommit->vulkanTex->memoryFence(); ++ xwm_log.infof( "Treating synchronously uploaded SHM commit %lu as ready without an external fence", ++ newCommit->commitID ); ++ loggedSynchronousUpload = true; + } + } + + gpuvis_trace_printf( "pushing wait for commit %lu win %lx", newCommit->commitID, w->type == steamcompmgr_win_type_t::XWAYLAND ? w->xwayland().id : 0 ); ++ if ( !gamescope::DispatchBufferCommitSync( newCommit.get(), g_ImageWaiter, commitSync, ++ mango_nudge, uMangoMsgType, doneCommits ) ) + { +- newCommit->SetFence( fence, mango_nudge, uMangoMsgType, doneCommits ); +- if ( bKnownReady ) +- newCommit->Signal(); +- else +- g_ImageWaiter.AddWaitable( newCommit.get() ); ++ xwm_log.errorf( "Failed to queue %s commit fence fd %d for commit %lu", ++ gamescope::BufferCommitSyncSourceName( commitSync.source ), commitSync.fence, newCommit->commitID ); ++ // Dropping the commit releases its buffer and discards presentation feedback. ++ w->receivedDoneCommit = true; ++ return; + } + + w->commit_queue.push_back( std::move(newCommit) ); +diff --git a/src/buffer_commit_sync.hpp b/src/buffer_commit_sync.hpp +new file mode 100644 +--- /dev/null ++++ b/src/buffer_commit_sync.hpp +@@ -0,0 +1,79 @@ ++#pragma once ++ ++#include ++ ++namespace gamescope ++{ ++enum class BufferCommitSyncSource ++{ ++ ExplicitAcquire, ++ DmaBuf, ++ SynchronousUpload, ++}; ++ ++struct BufferCommitSync ++{ ++ int fence = -1; ++ bool knownReady = false; ++ BufferCommitSyncSource source = BufferCommitSyncSource::SynchronousUpload; ++ ++ static constexpr BufferCommitSync ExplicitAcquire( int fence, bool knownReady ) ++ { ++ return { fence, knownReady, BufferCommitSyncSource::ExplicitAcquire }; ++ } ++ ++ static constexpr BufferCommitSync DmaBuf( int fence ) ++ { ++ return { fence, false, BufferCommitSyncSource::DmaBuf }; ++ } ++ ++ static constexpr BufferCommitSync SynchronousUpload() ++ { ++ return { -1, true, BufferCommitSyncSource::SynchronousUpload }; ++ } ++}; ++ ++template ++BufferCommitSync SelectBufferCommitSync( bool hasExplicitAcquire, int explicitFence, ++ bool explicitKnownReady, GetDmaBufFence &&getDmaBufFence ) ++{ ++ if ( hasExplicitAcquire ) ++ return BufferCommitSync::ExplicitAcquire( explicitFence, explicitKnownReady ); ++ ++ if ( std::optional dmaBufFence = getDmaBufFence() ) ++ return BufferCommitSync::DmaBuf( *dmaBufFence ); ++ ++ return BufferCommitSync::SynchronousUpload(); ++} ++ ++constexpr const char *BufferCommitSyncSourceName( BufferCommitSyncSource source ) ++{ ++ switch ( source ) ++ { ++ case BufferCommitSyncSource::ExplicitAcquire: ++ return "explicit acquire"; ++ case BufferCommitSyncSource::DmaBuf: ++ return "DMA-BUF"; ++ case BufferCommitSyncSource::SynchronousUpload: ++ return "synchronous upload"; ++ } ++ ++ return "unknown"; ++} ++ ++template ++bool DispatchBufferCommitSync( Commit *commit, Waiter &waiter, const BufferCommitSync &sync, ++ bool mangoNudge, unsigned int mangoMsgType, DoneCommits *doneCommits ) ++{ ++ commit->SetFence( sync.fence, mangoNudge, mangoMsgType, doneCommits ); ++ if ( !sync.knownReady && sync.fence < 0 ) ++ return false; ++ if ( sync.knownReady ) ++ { ++ commit->Signal(); ++ return true; ++ } ++ ++ return waiter.AddWaitable( commit ); ++} ++} diff --git a/tools/qualcomm/gamescope-patches/0127-refresh-shm-buffer-contents.patch b/tools/qualcomm/gamescope-patches/0127-refresh-shm-buffer-contents.patch new file mode 100644 index 000000000..74e1b3811 --- /dev/null +++ b/tools/qualcomm/gamescope-patches/0127-refresh-shm-buffer-contents.patch @@ -0,0 +1,54 @@ +diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp +--- a/src/steamcompmgr.cpp ++++ b/src/steamcompmgr.cpp +@@ -1851,16 +1851,22 @@ + commit->fifo = false; + } + +- if ( gamescope::OwningRc pTexture = s_BufferMemos.LookupVulkanTexture( buf ) ) +- { +- // Going from OwningRc -> Rc now. +- commit->vulkanTex = pTexture; +- return commit; +- } +- + struct wlr_dmabuf_attributes dmabuf = {0}; ++ const bool bIsDmabuf = wlr_buffer_get_dmabuf( buf, &dmabuf ); ++ // DMA-BUF textures reference the producer's live allocation. SHM textures are ++ // snapshots uploaded into separate memory and must be refreshed every commit. ++ if ( bIsDmabuf ) ++ { ++ if ( gamescope::OwningRc pTexture = s_BufferMemos.LookupVulkanTexture( buf ) ) ++ { ++ // Going from OwningRc -> Rc now. ++ commit->vulkanTex = pTexture; ++ return commit; ++ } ++ } ++ + gamescope::OwningRc pBackendFb; +- if ( wlr_buffer_get_dmabuf( buf, &dmabuf ) ) ++ if ( bIsDmabuf ) + { + pBackendFb = GetBackend()->ImportDmabufToBackend( &dmabuf ); + } +@@ -1874,7 +1880,8 @@ + + commit->vulkanTex = pOwnedTexture; + +- s_BufferMemos.MemoizeBuffer( buf, std::move( pOwnedTexture ) ); ++ if ( bIsDmabuf ) ++ s_BufferMemos.MemoizeBuffer( buf, std::move( pOwnedTexture ) ); + + return commit; + } +@@ -8599,7 +8606,8 @@ + } ); + if ( commitSync.source == gamescope::BufferCommitSyncSource::SynchronousUpload ) + { +- // vulkan_create_texture_from_wlr_buffer waited for this SHM upload to finish. ++ // Every SHM commit is uploaded synchronously; only DMA-BUF textures are memoized. ++ // vulkan_create_texture_from_wlr_buffer waited for this upload to finish. + // Exporting its image memory as a DMA-BUF does not produce a sync fence and can + // return an invalid fd on drivers that do not export the allocation. + static bool loggedSynchronousUpload = false; diff --git a/tools/qualcomm/gamescope-patches/0128-keep-swapchain-shm-textures-internal.patch b/tools/qualcomm/gamescope-patches/0128-keep-swapchain-shm-textures-internal.patch new file mode 100644 index 000000000..b6aafc521 --- /dev/null +++ b/tools/qualcomm/gamescope-patches/0128-keep-swapchain-shm-textures-internal.patch @@ -0,0 +1,15 @@ +diff --git a/src/rendervulkan.cpp b/src/rendervulkan.cpp +--- a/src/rendervulkan.cpp ++++ b/src/rendervulkan.cpp +@@ -4966,7 +4966,10 @@ + CVulkanTexture::createFlags texCreateFlags; + texCreateFlags.bSampled = true; + texCreateFlags.bTransferDst = true; +- texCreateFlags.bFlippable = true; ++ // Swapchain backends sample this synchronous SHM upload locally. They never ++ // scan it out or forward its DMA-BUF, so external allocation and ownership ++ // transfers are unnecessary. Keep exportable inputs for the other backends. ++ texCreateFlags.bFlippable = !GetBackend()->UsesVulkanSwapchain(); + if ( pTex->BInit( width, height, 1u, drmFormat, texCreateFlags, nullptr, 0, 0, nullptr, pBackendFb ) == false ) + return nullptr; + diff --git a/tools/qualcomm/gamescope-patches/0129-keep-swapchain-cursors-internal.patch b/tools/qualcomm/gamescope-patches/0129-keep-swapchain-cursors-internal.patch new file mode 100644 index 000000000..a11923a4f --- /dev/null +++ b/tools/qualcomm/gamescope-patches/0129-keep-swapchain-cursors-internal.patch @@ -0,0 +1,14 @@ +diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp +--- a/src/steamcompmgr.cpp ++++ b/src/steamcompmgr.cpp +@@ -2350,6 +2350,9 @@ bool MouseCursor::getTexture() + + CVulkanTexture::createFlags texCreateFlags; +- texCreateFlags.bFlippable = true; ++ // The Vulkan swapchain backend samples cursor pixels locally. It does not ++ // scan out or forward a cursor DMA-BUF, so keep this allocation internal. ++ // Hardware and forwarding backends retain their exportable cursor texture. ++ texCreateFlags.bFlippable = !GetBackend()->UsesVulkanSwapchain(); + if ( GetBackend()->SupportsPlaneHardwareCursor() ) + { + texCreateFlags.bLinear = true; // cursor buffer needs to be linear diff --git a/tools/qualcomm/gamescope-patches/0130-explicit-float-header.patch b/tools/qualcomm/gamescope-patches/0130-explicit-float-header.patch new file mode 100644 index 000000000..9945d351c --- /dev/null +++ b/tools/qualcomm/gamescope-patches/0130-explicit-float-header.patch @@ -0,0 +1,10 @@ +--- a/src/wlserver.cpp ++++ b/src/wlserver.cpp +@@ -2,6 +2,7 @@ + #define _GNU_SOURCE 1 + + #include ++#include + #include + #include + #include diff --git a/tools/qualcomm/qualcomm-gl.sh b/tools/qualcomm/qualcomm-gl.sh new file mode 100644 index 000000000..be84e21b7 --- /dev/null +++ b/tools/qualcomm/qualcomm-gl.sh @@ -0,0 +1,5 @@ +export LD_LIBRARY_PATH="$DROIDDECK_QUALCOMM_ROOT/mesa${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" +export LIBGL_DRIVERS_PATH="$DROIDDECK_QUALCOMM_ROOT/mesa/dri" +export GBM_BACKENDS_PATH="$DROIDDECK_QUALCOMM_ROOT/mesa/gbm" +export __GLX_VENDOR_LIBRARY_NAME=droiddeck_qualcomm +export __EGL_VENDOR_LIBRARY_FILENAMES="$DROIDDECK_QUALCOMM_ROOT/mesa/egl-vendor.json" diff --git a/tools/qualcomm/runtime.json b/tools/qualcomm/runtime.json index 16cea7031..cbc03d47f 100644 --- a/tools/qualcomm/runtime.json +++ b/tools/qualcomm/runtime.json @@ -9,12 +9,14 @@ "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" + "source_commit": "8f212644c46460549035971ab914421180f61a7c", + "member_sha256": "1efe37d2bee38b86a051f84279802054d770aed6d9eff7d9379b4648705d0881", + "build": { + "binary": "build/qualcomm-gamescope/gamescope", + "source": "build/qualcomm-gamescope/source.tar.gz", + "source_sha256": "11e9134ef9fe0616bc1c1ef8bdf61c52b48f473ce07e562108cf7476a017e531", + "source_size": 10104613 + } }, "wsi": { "files": [ @@ -113,5 +115,101 @@ "73368f4579fe0d27": "cs_composite_blur", "80346d82c5a6d850": "cs_composite_blit" } + }, + "mesa": { + "source_commit": "31e9a6b2e95e30d84bf3177d1f497d063e59b6b2", + "directory": "build/qualcomm-zink/runtime", + "files": [ + { + "path": "dri/zink_dri.so", + "sha256": "a65a4b895ebf49ec2841023fa550f50344574e1b68d574468365ea7857f577fa", + "size": 133368 + }, + { + "path": "egl-vendor.json", + "sha256": "aafac15b0a72b9b782d88729f3481c217b27f6e8664ffc9d6493df566ebb466a", + "size": 93 + }, + { + "path": "gbm/dri_gbm.so", + "sha256": "57e5aa8c3903c13db6a38e28cdc9160f2bd09cec04c88c90d6603d15b761b524", + "size": 134024 + }, + { + "path": "libEGL_droiddeck_qualcomm.so.0", + "sha256": "234d2041db7e87717c507ee90697058f646d1d9b61adc94f4a8cc52576fe733d", + "size": 398632 + }, + { + "path": "libgallium-26.2.3.so", + "sha256": "bcda320860b051725ca49fd546937cc6512373bc21ce0d1e7f9c097341f3e9bc", + "size": 21578336 + }, + { + "path": "libgbm.so.1", + "sha256": "e9f131c50d423832eb947292b2a4f6405e51c272ce76906e3e31d5e5988334a9", + "size": 67840 + }, + { + "path": "libGLX_droiddeck_qualcomm.so.0", + "sha256": "0c508865568327c98230c7ae5c241716715e70cc5b9dfc942cdb7bb79fc346a5", + "size": 397704 + }, + { + "path": "licenses/Apache-2.0", + "sha256": "aeae52465380b8b7959c940355e869cbbc277d4559a7ababa49cbc4818c051f5", + "size": 10232 + }, + { + "path": "licenses/BSL-1.0", + "sha256": "cf7105f24feaf70f28b8de10074e4d37bef89b8f3ae0a5f1280237f027d77ec0", + "size": 1666 + }, + { + "path": "licenses/exceptions/Linux-Syscall-Note", + "sha256": "d3a16686cf8797fd8a7b35564b9de8586e6ed8e19c9447e16ab6dda0d411215d", + "size": 1281 + }, + { + "path": "licenses/GPL-1.0-or-later", + "sha256": "13ff2a162a56796d863de9373ea46860a0fc28da81c192e76da9704dc117c27b", + "size": 13307 + }, + { + "path": "licenses/GPL-2.0-only", + "sha256": "535fe49430f3098603b190f07040d5b0c65eb8ed5fe173108e26eb668e758c00", + "size": 18774 + }, + { + "path": "licenses/Mesa-license.rst", + "sha256": "e272a136f6ce1b855afeef2fbe44f94f17d3b4d91cef29f8364af5800c5838a8", + "size": 1866 + }, + { + "path": "licenses/MIT", + "sha256": "a1179eae4e8b0ef06cbad6b6a2e324bb1c1cd9281cf40b42c0b15b0e34df2683", + "size": 1402 + }, + { + "path": "licenses/SGI-B-2.0", + "sha256": "8299bbd83c4d77eb5ae767cf250e9b3b470218e863714cba2e5e7eaed51285f2", + "size": 1844 + }, + { + "path": "licenses/source.json", + "sha256": "6bb1d1549c264228466853656258f1e3c6822c2bf314260e4900cd0eed3a393c", + "size": 658 + }, + { + "path": "licenses/zink-optional-maintenance5.patch", + "sha256": "25f0733bc42199344f205a916f6674d578a9a78250fd6b4cecbfb1cbbc8f8cf5", + "size": 1682 + }, + { + "path": "qualcomm-gl.sh", + "sha256": "2ed84bb2a5fa20cde52f18733fe3e8d1fda22f3f0d0bdad47363ff71a04522f5", + "size": 353 + } + ] } } diff --git a/tools/qualcomm/zink-optional-maintenance5.patch b/tools/qualcomm/zink-optional-maintenance5.patch new file mode 100644 index 000000000..df30f8722 --- /dev/null +++ b/tools/qualcomm/zink-optional-maintenance5.patch @@ -0,0 +1,34 @@ +--- a/src/gallium/drivers/zink/zink_device_info.py ++++ b/src/gallium/drivers/zink/zink_device_info.py +@@ -68,8 +68,7 @@ + features=True), + Extension("VK_KHR_maintenance5", + alias="maint5", +- features=True, properties=True, +- required=True), ++ features=True, properties=True), + Extension("VK_KHR_maintenance6", + alias="maint6", + features=True, properties=True), +--- a/src/gallium/drivers/zink/zink_compiler.c ++++ b/src/gallium/drivers/zink/zink_compiler.c +@@ -3056,7 +3056,7 @@ + bool can_remove = false; + if (!nir_find_variable_with_location(consumer, nir_var_shader_in, VARYING_SLOT_PSIZ)) { + /* maintenance5 guarantees "A default size of 1.0 is used if PointSize is not written" */ +- if (!var->data.explicit_xfb_buffer && delete_psiz_store(producer, true)) ++ if (screen->info.have_KHR_maintenance5 && !var->data.explicit_xfb_buffer && delete_psiz_store(producer, true)) + can_remove = !(producer->info.outputs_written & VARYING_BIT_PSIZ); + else if (consumer->info.stage != MESA_SHADER_FRAGMENT) + can_remove = !var->data.explicit_location; +--- a/src/gallium/drivers/zink/zink_context.c ++++ b/src/gallium/drivers/zink/zink_context.c +@@ -1007,7 +1007,7 @@ + NULL, + VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT + }; +- bvci.pNext = &usage; ++ bvci.pNext = screen->info.have_KHR_maintenance5 ? &usage : NULL; + if (zink_get_format_props(screen, format)->bufferFeatures & VK_FORMAT_FEATURE_STORAGE_TEXEL_BUFFER_BIT) + usage.usage |= VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT; + diff --git a/tools/tests/test_qualcomm_runtime.py b/tools/tests/test_qualcomm_runtime.py index 2215806f4..d6968ef25 100644 --- a/tools/tests/test_qualcomm_runtime.py +++ b/tools/tests/test_qualcomm_runtime.py @@ -86,6 +86,34 @@ class QualcommRuntimeTest(unittest.TestCase): runtime.stage(root / 'bad-path', cache, lock) self.assertFalse((root / 'outside.so').exists()) + def test_local_builds_are_pinned_and_never_fall_back_to_a_download(self): + bridge, binary, source, mesa = b'bridge', b'built gamescope', b'corresponding source', b'private mesa' + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + built, cache = root / 'build/private', root / 'cache' + built.mkdir(parents=True) + cache.mkdir() + lock = self.lock(bridge) + (cache / (lock['sha256'] + '.tzst')).write_bytes(bridge) + for name, data in [('gamescope', binary), ('source.tar.gz', source), ('mesa.so', mesa)]: + (built / name).write_bytes(data) + lock['gamescope'] = dict(member_sha256=hashlib.sha256(binary).hexdigest(), build=dict( + binary='build/private/gamescope', source='build/private/source.tar.gz', + source_sha256=hashlib.sha256(source).hexdigest(), source_size=len(source))) + lock['mesa'] = dict(directory='build/private', files=[dict( + path='mesa.so', sha256=hashlib.sha256(mesa).hexdigest(), size=len(mesa))]) + with patch.object(runtime, '__file__', str(root / 'tools/qualcomm/fetch_runtime.py')): + output = root / 'output' + runtime.stage(output, cache, lock) + self.assertEqual((output / 'gamescope').read_bytes(), binary) + self.assertEqual((output / 'licenses/gamescope-patched-source.tar.gzip').read_bytes(), source) + self.assertEqual((output / 'mesa/mesa.so').read_bytes(), mesa) + (built / 'mesa.so').write_bytes(b'wrong mesa') + with self.assertRaisesRegex(ValueError, 'Mesa member'): + runtime.stage(output, cache, lock) + (built / 'gamescope').unlink() + with self.assertRaisesRegex(FileNotFoundError, 'Build private Gamescope'): + runtime.stage(output, cache, lock) if __name__ == '__main__':