Repository navigation
gfx: engine fixes for the shading tiers, scene lights and blending, glTF material parameters - #2326
Merged
Merged
Conversation
jcelerier
force-pushed
the
gfx/shading-tiers
branch
2 times, most recently
from
October 9, 2026 20:05
4549ebb to
2e9ace7
Compare
… the scene without a camera The cascades followed the first directional light found, whether or not it had Cast shadow on; they now follow the first one that casts, and fall back to the first directional light only when none does, since casting is off by default. Without a camera in its Scene In the setup fitted the cascades to an identity camera, a two-unit box at the origin, so shadows were cut off and with more than one cascade disappeared; it now covers the Shadow distance around the origin and warns once that the camera should be wired in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
The env block already carried a fog mode (linear, exponential, exponential squared) and a density, but nothing set them, so only linear fog could be reached. Two controls are added after the existing ones so saved inlets keep their index. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
Primitives point at their material and the preprocessor resolves a draw's material by looking that pointer up in the scene's material table. The override only replaced the table with clones, so no draw found its material and every overridden mesh rendered with the default one. It now rewrites the meshes and nodes that use a targeted material to point at the clone, passes untouched subtrees through as they are, and keeps the rewritten meshes across rebuilds while upstream is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
jcelerier
force-pushed
the
gfx/shading-tiers
branch
from
October 11, 2026 00:16
e4ce327 to
54da0b2
Compare
This was referenced Oct 11, 2026
Joints had no parent and an identity rest transform, so without an animation every joint matrix was the inverse bind matrix and the mesh collapsed, and with one the joints did not follow their parents and the mesh tore. Each joint now takes its node's rest TRS and its parent joint from the node tree; nodes between two joints become joints of their own so their animation tracks still apply, and the root joint gets a fixed parent bringing it into the skinned mesh's space. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
textureFormat is an optional texture_format property, which QML could not assign from a number or a string, so scripts building scenes could not ask for a float texture. Converters from int, double and string are registered next to the render-size ones; a negative number, an empty string or Auto clears the format. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
…ntrols The base colour was four sliders and the emissive colour three sliders from 0 to 10; they become an RGBA colour (alpha is the material's) and an emissive colour, and Emissive strength goes up to 100 so the brightest emission the sliders allowed can still be reached. PBR Mesh is in no release yet, so scores are updated rather than migrated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
The texture-reference comment gave the bucket six bits and the layer twenty-four while the encoder packs seven and twenty-three, the bucket cap was given as 16 against kMaxBuckets = 8, the material slot as 64 bytes against an 80-byte MaterialGPU, and the camera buffers as binding only the active camera although the camera auxiliaries carry every packed one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
A sampled AUXILIARY texture got a sampler without mipmapping unless the shader declared MIPMAP_MODE, so textureLod on the scene's prefiltered_map read mip 0 at every roughness and classic_pbr_openpbr reflected like a mirror everywhere. When the declaration leaves MIPMAP_MODE out, the sampler now follows the texture it is bound to: linear for a mipmapped texture, none otherwise, since GL treats a mip-filtered sampler on a single-level texture as incomplete. Raw rasters and CSF geometry auxiliaries both do this. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
The camera block a raw raster binds before a scene camera reaches it held identity matrices and a zero renderSize, so a shader dividing gl_FragCoord by camera.renderSize divided by zero. It is now seeded with the render size, as the Scene Preprocessor's camera is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
Images and the video renderers only ever drew into their consumers' render targets and published no texture of their own, so an input that samples the producer's texture directly (STATIC) read black. Images now publishes the current image to such an input, and a video renderer converts the frame into a texture of its own for it. Inputs with a render target keep the drawn, scaled image: publishing it unconditionally would let texture inputs that read upstream textures directly bypass the Images controls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
The expression evaluator read one port per input from port 0, but a raw raster's port 0 is its Geometry input and an input may own no port or several, so "$face_size" read another port and the output fell back to the render size. It now walks the ports the way the node creates them. The declared size is also re-evaluated every frame and the targets are rebuilt when it changes, and a single plain colour output with an explicit size now renders at that size instead of at the consumer's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
PIPELINE_STATE.VERTEX_COUNT and INSTANCE_COUNT took integer literals only, so a procedural draw could not follow an input or a buffer's size. A string is now kept as an expression and evaluated every frame against the raw raster's expression variables ($<input>, $COUNT_<buffer>, ...). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
…ated Any SIZE that was not an integer literal allocated 1024 elements for an auxiliary buffer nothing upstream publishes, and $COUNT_<name> then reported 1024. SIZE is now evaluated like the other raw raster expressions; 1024 remains the fallback when it does not resolve. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
COLOR_WRITE was only read inside BLEND, so a top-level one was dropped and every channel was written. It is now applied to every attachment whatever blend applies, and "none" disables colour writes instead of meaning rgba. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
A compute shader reads one geometry per input, and with several cables on one the others were dropped silently. That is now reported once per input. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
The vertex epilogue mirrors Y on D3D and Metal, and for cube faces on every backend, which reverses the winding; the pipeline's front face was swapped to compensate only when culling was on. With CULL_MODE none gl_FrontFacing was therefore inverted there, and two-sided materials flipped their normals the wrong way. The swap now applies whatever the cull mode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
A GPU float attribute whose stride is not the std430 one (a tightly packed float3 from Pointcloud to mesh or Buffers to geometry, an interleaved XYZ_RGB buffer) was bound as is when declared vec3, so every element after the first was misread, and left as a one-element buffer when declared vec4. It is now repacked into an owned std430 buffer by the GPU scatter every frame, read straight from the upstream buffer. The scatter wrote its destination elements dst_components apart, so CPU float3 data declared vec3 was misread the same way; it now takes the destination stride. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
A CSF read only the first mesh of its input geometry, so the other parts of a multi-part Geometry Loader or of Merge Geometries were dropped from its output. Each further mesh now runs the passes in its own dispatch, over its own buffers and SRBs, so length() and $VERTEX_COUNT are the mesh's own and its index, auxiliaries and textures stay with it; the output carries one mesh per input mesh. A mesh lacking a required attribute is forwarded unchanged. Generators, feedback loops, indirect commands and writable storage are state the meshes would overwrite in turn: those CSFs keep reading the first mesh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
… GPU A draw whose indices were GPU-resident was skipped, so PBR Mesh drew nothing for any indexed geometry that reached it on the GPU: an uploaded CPU index (Academy USD Mesh Loader), an index a CSF forwards, or a Scene Preprocessor output wrapped back into a scene. The index buffer is now copied into the arena slab every frame like the GPU vertex streams: uint32 in one copy, uint16 widened index by index into zeroed uint32 slots (not on Metal, whose macOS blits need 4-byte alignment). The mesh fingerprint follows the index buffer handle, and PBR Mesh's index byte offset reaches the geometry the preprocessor reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
Only position, normal, texcoord0 and tangent were copied from GPU sources; a GPU color0 or texcoord1 (a CSF such as AddColor upstream of PBR Mesh) came out as the white / zero fallback. Both are now queued with the other GPU stream copies, at the source stride and offset, and mixed into the mesh fingerprint. A float3 colour keeps the fallback's alpha; non-float colour formats still take the fallback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
glslang accepts `float char` for SPIR-V and SPIRV-Cross passes it through
to the GL target, where NVIDIA refuses it ("unexpected reserved word").
Probing its compiler against the GLSL, C++ and Cg keyword sets finds
char, register, row_major and the NV mesh-shader qualifiers outside the
spec's reserved list, which glslang and SPIRV-Cross already handle. They
are now prefixed with isf_glsl_ for OpenGL bakes, like the HLSL and MSL
renames. The reserved-names fixture also uses register and row_major.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
ShaderCache renames `frac` before the HLSL bake, so the older SPIRV-Cross on CI cannot emit `float frac = frac(...)` either; the file now has to bake clean like the rest, and the known-gap list it was the only entry of is gone. The rename test expected `new` to pass through on D3D, which the HLSL list renames, and now covers the OpenGL list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
Since 3784b82 gave yuv410p, rgb555le, pal8 and bgr8 GPU decoders, only bgr565le of the clips the swscale case relied on still reaches Rescale, and Media_LibavDecode failed `exercised >= 4`. The runner now also makes rgb444le, bgr444le and yvyu422 clips, with an odd-sized yvyu422 for the 4:2:2 rounding; the GPU-decoded clips stay for the other cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
After a render-list rebuild, Model Display re-acquires the geometry it was last sent, whose buffer handles belong to the released list: a GPU node's geometry input released its upload with releaseBuffer() and allocated a new one, but the new handle only reaches the display a frame later. That frame bound the freed buffer: NVIDIA crashed in vkCmdBindVertexBuffers or in the GL deferred release, Metal in setVertexInput, and test_gfx_model_display_gpu_geometry segfaulted in 9 of 15 runs (0 of 10 now). CustomMesh::drawSingleMesh now skips the draw when a vertex or index buffer is retiring, like a null one; the display draws again once the new handle arrives. isRetiringBuffer rather than isRetiredBuffer: the frame count only advances in RenderList::render, not across every QRhi frame that frees a buffer. A geometry input marks its fresh uploads live, so a recycled address does not read as retired, and the retirement lookup skips the mutex when nothing is retired. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
…tart threads Setting a new environment variable reallocates environ, so a getenv on another thread (LeapC's poll loop) could read the freed array and crash. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
Without BOOST_ASIO_ENABLE_VERSION_NAMESPACE, Boost.Asio names its extern "C" thread entry asio_detail_posix_thread_function, like standalone Asio. In a static Linux/macOS link the LSL addon's strong definition replaced our weak one, so Boost.Asio threads freed their own func object on exit and ~io_context's join of the resolver thread read it after free: the mapper_http / mapper_datagram segfaults. Enabled on all platforms (was Windows-only), also for external addons, and libossia bumped to ossia/libossia#960. test_app_hides_lsl_symbols checks that the app exports none of liblsl's symbols. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
Both drifted at glyph edges on llvmpipe (SSIM 0.99992 / 0.99974, max_abs 75 / 170); regenerated with text-render.sh --update-refs --rebless (two agreeing runs, analyze.py 36/36). size-large still matched and is kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
The branch commit 94e7df045 was rebased on merge; d606b8801 on master has the same tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
liblsl now keeps its bundled Asio, pugixml and loguru apart by compiling them as lsl_asio, lsl_pugi and lsl_loguru (no prelink), so look for those names in the app's dynamic exports as well. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
QImage::flipped is new in Qt 6.9; the Coverage job builds with Qt 6.4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
A window output takes its title from the device name, once, in window_device's constructor. That is all an embedder gets, and a name it has to keep stable for other reasons is a poor title: Scenic names its devices after the node id because that name is also the address prefix its cabling resolves against, so every Video Monitor window was called "dst_window_1_dev". Add a /title parameter beside /fullscreen. It routes through ScreenNode::setTitle, so the value is cached in m_title and survives the window being recreated by a swapchain or graphics-API rebuild, which writing to the QWindow directly does not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019QYyvhRXvSMSRQPaq12Teq
spoutSenderNames::CreateSender renames a taken sender name to "name_N" in place, with strcpy_s(name, 256, ...) (SpoutSenderNames.cpp:213-223). The D3D11, D3D12 and Vulkan outputs passed it the buffer of a temporary std::string, so a collision wrote past it (two bytes past the 32-byte block for a 31-character name) and the real name was lost. Release then used the configured name, which removed the other application's sender from the registry and left ours dangling. registerDXSender copies the name into a SpoutMaxSenderNameLen buffer, keeps what Spout registered, and the release and GL frame-sync calls use that name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
The SDK asks receivers to check IsUpdated() after every receive (Spout.cpp:995-1006): a receive that sees a new sender texture only re-links it and returns without copying (Spout.cpp:909-923), and the next receive clears the flag. The GL input never called it and polled the registry by name instead. It now reconfigures from the receiver's own size, format and handle when IsUpdated() is set, after each of its two receive calls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
A playback-only app opens a duplex device for no reason, and on macOS the capture half is gated by a microphone consent prompt that arrives while restart_engine() runs synchronously from initDocuments() -- so the process freezes with no window and nothing on screen to explain it. SCORE_AUDIO_NO_INPUT=1 defaults such an app to no input card while keeping output, which SCORE_AUDIO_BACKEND=dummy cannot. It only sets the default: a stored preference still wins, so the setting stays usable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019QYyvhRXvSMSRQPaq12Teq
… directory The platform scripts read them from another directory, so a relative --app-environment only worked when a per-OS overlay happened to exist and the merge replaced it with a mktemp path. Icons, qrc and appdata were passed through the same way. A missing environment file is now an error at parse time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
Closing the Syphon sender crashed score on Metal one second later, in -[AGXG14XFamilyRenderContext setFragmentTextures:withRange:] under QRhiMetal::enqueueShaderResourceBindings (the Domeport Pro report). The sampler's QRhiTexture is createFrom()'d the client's MTLTexture, and QMetalTexture::createFrom (qrhimetal.mm:5376) stores the pointer with owns = false: QRhi never retains it. m_currentMtlTexture was its only owner. Once the server is gone for kReopenAfterEmpty frames, update() clears enabled, and the next openServer() stopped the client and set m_currentMtlTexture = nil (ARC: -fobjc-arc for this file), freeing it. With no server to find, nothing re-created the QRhiTexture, so the SRBs bound the freed texture on the next draw. The OpenGL path had the same shape: stopping the client deallocated the SyphonOpenGLImage, which deletes its GL texture, and the sampler then read an unloadable texture (black). openServer() now leaves the current texture alone; it is replaced only when a new frame rebuilds the QRhiTexture, so the last frame stays on screen. The GL path keeps the SyphonOpenGLImage that owns its texture the same way. Replaced Metal textures are kept FramesInFlight + 1 frames, since Qt's command buffers hold unretained references, and releaseState() drops them behind an empty command buffer once the sampler is gone. A restarted sender comes back under a new UUID, so the receiver also matches the name and app it was last connected to; serverStillPresent() checks the exact UUID it holds, so a quick restart still reconnects. macmini-m1, external Metal sender quit after 8 s: before, 3/3 runs SIGSEGV in setFragmentTextures; after, 10/10 Metal and 3/3 OpenGL runs survive with the last frame held, and restarted senders at 640x360 then 480x270 reconnect with exact pixels on both backends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
A packaged app is two processes: <App>.exe, the launcher, starts app-bin.exe and waits. Neither set an AppUserModelID, so Windows derived one from each executable's path. The windows belong to app-bin.exe, so a pinned launcher and the running app never shared a taskbar button, and pinning the running app pinned app-bin.exe, which starts without the launcher's arguments and environment. - create-app-windows.sh bakes --app-identifier (or ossia.score.custom.<name> without it) into the launcher, reduced to the allowed characters and 128 of them. - The launcher passes it to app-bin.exe as SCORE_CUSTOM_APP_USER_MODEL_ID, with its own path as SCORE_CUSTOM_APP_LAUNCHER. - score reads both with GetEnvironmentVariableW at the top of main(), before any window or plug-in thread, and calls SetCurrentProcessExplicitAppUserModelID. - An application event filter labels every top-level Qt::Window and Qt::Dialog when its HWND is created (QPlatformSurfaceEvent), widgets and Qt Quick windows alike. It sets RelaunchCommand, RelaunchDisplayNameResource and RelaunchIconResource (the launcher), then the ID, through SHGetPropertyStoreForWindow. It clears them before the HWND is destroyed. - Without the variable nothing changes: plain score has no explicit ID. The packaging makes a zip and creates no shortcuts, so there is no shortcut to label. https://learn.microsoft.com/en-us/windows/win32/shell/appids Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
…ensions A TextureSource renders on its Qt Quick window's own VkDevice, which Qt creates with only the scene graph's fixed extension list, so importing a Spout texture there fails. An application event filter merges VK_KHR_external_memory(_win32), VK_KHR_dedicated_allocation and VK_KHR_get_memory_requirements2 into each QQuickWindow's graphics configuration on SurfaceCreated, before the first expose creates the QRhi, and warns if a window already has one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
vkGetInstanceProcAddr returns a loader trampoline for any device command, so DMABufPlaneImporter and HWCudaVulkanDecoder reported themselves usable on a VkDevice that never enabled VK_KHR_external_memory_fd, such as a Qt Quick window's, and then called through a null dispatch entry. vulkanDeviceCommand() resolves through the QRhi's own device, which returns null for commands of extensions it did not enable. DMA-BUF now requires vkGetMemoryFdPropertiesKHR and vkGetImageDrmFormatModifierPropertiesEXT on the device; the physical-device scan remains only for VK_EXT_external_memory_dma_buf, which has no command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
On a device without VK_KHR_external_semaphore_fd the instance trampoline for vkGetSemaphoreFdKHR jumps to null; vkGetDeviceProcAddr returns null instead, create() fails and the interop fence falls back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
requiredAlignment() asked the instance for vkGetMemoryHostPointerPropertiesEXT, which resolves whether or not the device enabled VK_EXT_external_memory_host, so an import on such a device called a null entry. Resolve it through the device, and use the QRhi's own QVulkanInstance where Qt provides it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
A Qt Quick window's VkDevice belongs to QVulkanDefaultInstance, not to score's static instance. Resolve vkGetMemoryWin32HandlePropertiesKHR through that instance's vkGetDeviceProcAddr directly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
DMA-BUF, host import, the CUDA decoder and the CUDA semaphore must report unavailable on a device made with Qt's default extensions and available on one made with sharedVulkanDeviceExtensions(), and an import gated the way the callers gate it must not reach the Qt-default device. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
RenderList::renderImpl carried m_initialBatch, holding every renderer's init()-time uploads, into the frame's first pass. When an inlet spec changed before that first frame, the surgical render-target update released and re-initialised the renderer first, and releaseState() deletes its material UBO outright, so the batch then named a freed QRhiBuffer. On OpenGL, enqueueResourceUpdates copies a uniform-buffer update into the buffer's CPU-side store: a write into freed memory, or, after destroy() cleared it, into QByteArray's static empty storage. test_gfx_input_sampler_filter (raw raster, OpenGL) crashed there, or read back black, depending on what reused the allocation. The batch is now committed to the command buffer as soon as the frame takes it, as onEdgeRemoved() and Graph::relinkGraph() already flush it before releasing renderer state. The new case in test_gfx_outside_frame_release_buffer re-inits a probe renderer on the first frame and checks, from its releaseState(), whether the batch its upload went into still holds it: deterministic, on every backend. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex
jcelerier
force-pushed
the
gfx/shading-tiers
branch
from
October 13, 2026 21:57
a12afe4 to
fe6d2e7
Compare
github-merge-queue
Bot
removed this pull request from the merge queue due to a conflict with the base branch
Oct 13, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
These are the engine fixes found while building the shading tiers (unlit / blinn_phong / pbr_fast / classic_pbr) in csf-examples, which use the Scene Preprocessor's aux buffers. Each fix comes with a test that fails without it and passes with it.
Raw raster and CSF
PIPELINE_STATE.COLOR_WRITEis honoured. The front face stays correct underCULL_MODE none.Scene
light_slot, see the libossia PR.Nodes
**glob spans path segments.inl.textureFormat = "RGBA16F").Added after review of the first CI run
<name>_prev.setDepthClamporQRhiResource::rhi(), and GPU JS needs Qt 6.7, which hasQQuickRhiItem. Checked locally with mold:Depends on
d606b8801.custom-pbr.scoreto the PBR Mesh colour controls.test_app_hides_lsl_symbolsexpects it.Gfx/Graph/RowOrder.hpp.Validation
7f2252111d, then re-runs of every failure. Still failing: GL multiview (needs the SDK Qt above), two explorer tests that depend on the desktop window manager (they pass on Xvfb), and the PipeWire and shader-sweep tests that already failed before this PR.132c8b3330. Rebuilds at the current head are in progress.Tests on macOS, Windows and static builds (folded in from #2332):
ForkProbe.hpp: resolve__llvm_profile_write_filewithdlsymon Apple, asObjectGallery.cppdoes. Mach-O has no weak undefined externals, so seven tests failed to link on macOS outside coverage builds.bitfocus_module_driveris aGUItest: it bootsscore_test/App.hpp, which with static plug-ins references every plug-in.CanvasPaintingTest:nearrenamed tocolorsClose(<windef.h>definesnear).Regex_UI_Test: its Switch uuid no longer clashes withSwitch_UI_Test's in unity builds.DYLD_FALLBACK_LIBRARY_PATH, since score refuses to start withDYLD_LIBRARY_PATHset (18 tests).score::interactiveSession()moves toscore/widgets/MessageBoxand the local device uses it: with--script, offscreen or minimal platforms, the port warning is only logged.Verified together with #2330 on Linux (Vulkan, GL), macOS (Metal) and Windows (D3D11/12, Vulkan, GL) at the same commits.
D3D12 robustness:
Backend portability, from the contact-sheet triage of the example scores:
Gfx/Graph/RowOrder.hpp.char,register,row_major, …).IMG_NORM_PIXEL_LODreads an explicit level.Merged in: #2331 (camera release, Syphon decoder on Metal, an inert replaced execution context) and #2330 (static clang64 test build, add-ons through response files on Windows).
Crash and build fixes from the full Linux test run:
model_display_gpu_geometryin 9 of 15 runs, and the same crash was seen on macOS.BOOST_ASIO_ENABLE_VERSION_NAMESPACE). liblsl's standalone Asio had replaced Boost's thread entry point, somapper_httpandmapper_datagramdouble-freed.input_sampler_filter(raw raster, OpenGL) crashed or read back black in about half of its runs. The batch is now committed as soon as the frame takes it, with a deterministic regression case inoutside_frame_release_buffer.SCORE_GFX_NO_SHARED_VKDEVICE, enables DMA-BUF and DRM-modifier import again.vulkan_importer_device_checksimports a LINEAR udmabuf and a vendor-tiled GBM buffer there with no validation errors.libLLVM.so.20was binding to score's LLVM 23 and crashed at exit or in llvmpipe shader compiles.QT3D_RENDERERand the Quick Controls style are set before plug-ins start threads. Adding a variable reallocates the environment on glibc < 2.41, and LeapC's poll thread was reading it at the time.text_rendergoldens are regenerated;test_app_hides_lsl_symbols.Spout (Windows):
name_1inside a temporarystd::string, two bytes past the heap block for a 31-character name. Score then released the configured name, which removed the other application's sender from the registry. Score now keeps the registered name, uses it for release and GL frame sync, and logs the rename.IsUpdated()after each receive, as the SDK requires, and follows sender size and format changes from the receiver.Vulkan interop on Qt Quick's own VkDevice:
VK_KHR_external_memory(plus_win32on Windows),VK_KHR_dedicated_allocationandVK_KHR_get_memory_requirements2. An application event filter merges them into the window's graphics configuration onSurfaceCreated, before the first expose creates the QRhi, and keeps whatever the app already configured. If a window already has a QRhi at that point, the filter logs a warning. A Spout input shown by a QMLTextureSourceon Vulkan rendered black (vkGetMemoryWin32HandlePropertiesKHR not available) and now shows the sender's pixels.vulkanDeviceCommand(), i.e.vkGetDeviceProcAddr) instead of the instance.vkGetInstanceProcAddrhands out a loader trampoline for any device command. On a Qt-default device, all four reported available, and the host import and semaphore export jumped to a null dispatch entry (SIGSEGV). They now report unavailable, and the existing fallbacks run. On score's own device, all four are still available.QVulkanInstance: a Qt Quick device belongs toQVulkanDefaultInstance.test_gfx_vulkan_importer_device_checks.Packaging:
tools/create-app.shresolves relative--app-environment, icon, qrc and appdata paths against the caller's directory, and stops at parse time when the environment file is missing. A relative environment path used to work only when a per-OS overlay existed.--app-identifier, orossia.score.custom.<name>without it. The launcher passes it to app-bin.exe, which sets it on the process before any window exists. Every top-level window also carries it, with relaunch properties that point at the launcher. The running app and the launcher now share one taskbar identity, and pinning the running app pins the launcher. Plain score is unchanged.Release review (fixups, to autosquash with
git rebase -i --autosquash origin/master; each comes with a test that fails without it)_blend_draw_cmdsas an injection (test_threedim_pbr_mesh_material).row_majorinsidelayout(...)is a qualifier and stays; glslang rejected the renamed form on OpenGL (test_gfx_shader_corpus_targets).SCORE_AUDIO_NO_INPUTapplies: setupDefaultSettings writes the CardIn default first, so the unconfigured state is the empty card, not a missing key (test_unit_audio_settings_no_input).ossia::hash_set/ossia::hash_mapwhere the branch introduced std containers.Review notes, unconfirmed doubts and design items (per-frame per-index copies for 16-bit GPU indices, the blend sort versus the MDI fast path): /mnt/build/diag/release-review-2326.md.
🤖 Generated with Claude Code
https://claude.ai/code/session_01JeL19ofVgPe9u9q7J1V9ex