[Agent] feat(ds): Metal sub-rectangle dual-screen framebuffer rendering#3509
[Agent] feat(ds): Metal sub-rectangle dual-screen framebuffer rendering#3509github-actions[bot] wants to merge 7 commits intodevelopfrom
Conversation
…#3509) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
🤖 PR created. AI review starting automatically. |
There was a problem hiding this comment.
Pull request overview
Implements GPU-side Metal sub-rectangle rendering for Nintendo DS dual-screen skins by splitting the combined DS framebuffer into per-screen quads and wiring DeltaSkin layout data into the Metal render path.
Changes:
- Added a dual-screen rendering path in
PVMetalViewControllerthat draws multiple sub-rect blits in one render pass. - Added DeltaSkin → Metal bridging to compute per-screen source UV rects and destination view rects.
- Enabled
supportsSkins = truefor melonDS and DeSmuME 2015 cores, plus added release notes/What’s New entry.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| PVUI/Sources/PVUIBase/PVGLViewController/PVMetalViewController.swift | Routes directRender through a dual-screen render path when a layout is installed. |
| PVUI/Sources/PVUIBase/PVGLViewController/PVMetalViewController+DualScreen.swift | Defines DualScreenRenderInfo and encodes multi-quad sub-rectangle blits per frame. |
| PVUI/Sources/PVUIBase/PVEmulatorVC/PVEmulatorViewController+MetalDualScreen.swift | Computes dual-screen rects from DeltaSkin screen groups and installs them on PVMetalViewController. |
| PVUI/Sources/PVUIBase/PVEmulatorVC/PVEmulatorViewController+DualScreen.swift | Integrates the Metal dual-screen path into applyDualScreenViewport. |
| PVUI/Sources/PVSwiftUI/Resources/whats-new.json | Adds a new “DS Dual-Screen Skins” What’s New entry. |
| PVShaders/Sources/PVShaders/Resources/Metal/Blitters/dual_screen_blit.metal | Adds a Metal shader intended for dual-screen sub-rectangle blitting. |
| Cores/melonDS/PVMelonDSCore/Core/PVMelonDSCore.swift | Enables skin support for melonDS. |
| Cores/Desmume2015/PVDesmume2015Core/Core/PVDesmume2015Core.swift | Enables skin support for DeSmuME 2015. |
| .changelog/3509.md | Adds changelog fragment describing the feature. |
| let sampler = renderSettings.smoothingEnabled ? linearSampler : pointSampler | ||
| encoder.setRenderPipelineState(pipeline) | ||
| encoder.setFragmentTexture(sourceTexture, index: 0) | ||
| if let sampler { encoder.setFragmentSamplerState(sampler, index: 0) } | ||
|
|
There was a problem hiding this comment.
dual_screen_ps declares an explicit sampler [[sampler(0)]], but the code only binds a sampler when linearSampler/pointSampler are non-nil. Some pipeline setup paths in PVMetalViewController (e.g. createBasicShadersWithDefaultLibrary) never create these sampler states, which would leave sampler(0) unbound and can trigger a Metal validation/GPU error. Ensure a sampler state is always created/bound here (or change the fragment shader to use a constexpr sampler so it doesn't require binding).
| // dual_screen_blit.metal | ||
| // PVShaders | ||
| // | ||
| // Metal shaders for DS dual-screen sub-rectangle blitting. | ||
| // | ||
| // Each screen is rendered by a separate draw call in a single render pass. | ||
| // The vertex buffer carries interleaved (NDC-position, UV) data so the vertex | ||
| // function needs no uniforms — all per-screen geometry is baked in by the CPU. | ||
| // | ||
| // Usage pattern (per screen): | ||
| // 1. Compute 4 vertices of the form float4(ndcX, ndcY, srcU, srcV) | ||
| // arranged as a triangle-strip (TL, TR, BL, BR). | ||
| // 2. Call setVertexBytes(_:length:index:) with buffer index 0. | ||
| // 3. drawPrimitives(.triangleStrip, vertexStart: 0, vertexCount: 4) |
There was a problem hiding this comment.
This file adds dual_screen_blit.metal, but the dual-screen pipeline in PVMetalViewController+DualScreen currently compiles shader source from an inline string instead of loading functions from the module’s default Metal library. If the intent is to ship this shader via PVShaders, it should be referenced/registered (or the extra file removed) to avoid dead/duplicated resources.
| var canUseMetalDualScreenRendering: Bool { | ||
| #if os(tvOS) | ||
| return false | ||
| #else | ||
| guard gpuViewController is PVMetalViewController else { return false } | ||
| guard core.supportsDualScreens else { return false } | ||
| guard isDeltaSkinEnabled, currentSkin != nil else { return false } | ||
| return true |
There was a problem hiding this comment.
canUseMetalDualScreenRendering is gated on core.supportsDualScreens, which is also true for non-DS dual-screen systems (e.g. emuThree/3DS). Since applyMetalDualScreenLayout assumes a DS-style combined framebuffer split and DS-specific defaults (256×384, top/bottom halves), this should be additionally gated to Nintendo DS (e.g. core.systemIdentifier == SystemIdentifier.DS.rawValue) to avoid accidentally taking the DS split path for other dual-screen cores if/when they gain skins + Metal rendering.
| }, | ||
| { | ||
| "version": "3.8.0", | ||
| "title": "DS Dual-Screen Skins", | ||
| "features": [ | ||
| { | ||
| "symbolName": "rectangle.split.1x2.fill", | ||
| "symbolColor": "blue", | ||
| "title": "Nintendo DS Dual-Screen Skins", | ||
| "subtitle": "DS games now support full DeltaSkin overlays with GPU-accelerated dual-screen rendering. Both top and bottom screens are positioned independently according to your skin layout." | ||
| }, | ||
| { | ||
| "symbolName": "square.3.layers.3d", | ||
| "symbolColor": "purple", | ||
| "title": "Metal Sub-Rectangle Rendering", | ||
| "subtitle": "The DS framebuffer is split into two sub-rectangles on the GPU in a single render pass — no extra copies, minimal overhead." | ||
| } | ||
| ] |
There was a problem hiding this comment.
Per repo release-versioning rules, whats-new.json entries should only be added when the target version has been explicitly confirmed by a maintainer (and version numbers must not be guessed). Adding a new entry for "version": "3.8.0" here looks speculative—please remove it or update it to the confirmed release version once provided.
| }, | |
| { | |
| "version": "3.8.0", | |
| "title": "DS Dual-Screen Skins", | |
| "features": [ | |
| { | |
| "symbolName": "rectangle.split.1x2.fill", | |
| "symbolColor": "blue", | |
| "title": "Nintendo DS Dual-Screen Skins", | |
| "subtitle": "DS games now support full DeltaSkin overlays with GPU-accelerated dual-screen rendering. Both top and bottom screens are positioned independently according to your skin layout." | |
| }, | |
| { | |
| "symbolName": "square.3.layers.3d", | |
| "symbolColor": "purple", | |
| "title": "Metal Sub-Rectangle Rendering", | |
| "subtitle": "The DS framebuffer is split into two sub-rectangles on the GPU in a single render pass — no extra copies, minimal overhead." | |
| } | |
| ] |
.changelog/3509.md
Outdated
| - **DS Dual-Screen Metal Rendering** — Nintendo DS games with a DeltaSkin active now render both screens as GPU-side sub-rectangle blits in a single Metal render pass. The combined 256×384 DS framebuffer is split into top (rows 0–191) and bottom (rows 192–383) viewports that match the skin's screen layout, supporting portrait (stacked) and landscape (side-by-side) configurations. | ||
|
|
||
| ### Changed | ||
| - **melonDS / DeSmuME skin support** — `supportsSkins` is now `true` for both DS cores, enabling DeltaSkin overlays and controller layouts when a compatible skin is installed. |
There was a problem hiding this comment.
Changelog fragments follow the style rule “plain English, no trailing period”. This entry ends with a period; please drop the trailing punctuation to match existing changelog formatting.
| - **DS Dual-Screen Metal Rendering** — Nintendo DS games with a DeltaSkin active now render both screens as GPU-side sub-rectangle blits in a single Metal render pass. The combined 256×384 DS framebuffer is split into top (rows 0–191) and bottom (rows 192–383) viewports that match the skin's screen layout, supporting portrait (stacked) and landscape (side-by-side) configurations. | |
| ### Changed | |
| - **melonDS / DeSmuME skin support** — `supportsSkins` is now `true` for both DS cores, enabling DeltaSkin overlays and controller layouts when a compatible skin is installed. | |
| - **DS Dual-Screen Metal Rendering** — Nintendo DS games with a DeltaSkin active now render both screens as GPU-side sub-rectangle blits in a single Metal render pass. The combined 256×384 DS framebuffer is split into top (rows 0–191) and bottom (rows 192–383) viewports that match the skin's screen layout, supporting portrait (stacked) and landscape (side-by-side) configurations | |
| ### Changed | |
| - **melonDS / DeSmuME skin support** — `supportsSkins` is now `true` for both DS cores, enabling DeltaSkin overlays and controller layouts when a compatible skin is installed |
| DLOG("🎮 Applied Metal dual-screen layout") | ||
| return | ||
| } | ||
| // Layout computation failed — fall through to the legacy path. |
There was a problem hiding this comment.
If canUseMetalDualScreenRendering is true but applyMetalDualScreenLayout() returns false, this falls through to the legacy positioning path without clearing any previously-installed metalVC.dualScreenLayout. Several failure paths in applyMetalDualScreenLayout early-return without clearing, so you can end up with a stale dual-screen layout still active while the legacy viewport-resize logic runs. Consider clearing the Metal layout on failure before falling back (or ensure applyMetalDualScreenLayout clears layout on every failure path).
| // Layout computation failed — fall through to the legacy path. | |
| // Layout computation failed — clear any existing Metal dual-screen layout | |
| // before falling back to the legacy viewport positioning path. | |
| clearMetalDualScreenLayout() |
| renderDualScreenLayout(encoder: renderEncoder, | ||
| sourceTexture: inputTexture, | ||
| drawableSize: drawableSize, | ||
| flipY: flipY) | ||
| renderEncoder.endEncoding() | ||
| commandBuffer.present(drawable) | ||
| commandBuffer.addCompletedHandler { [weak self] buffer in | ||
| if let error = buffer.error { | ||
| ELOG("GPU error during dual-screen rendering: \(error)") | ||
| self?.recoverFromGPUError() | ||
| } | ||
| } | ||
| commandBuffer.commit() | ||
| previousCommandBuffer = commandBuffer | ||
| frameCount += 1 | ||
| markFramePresented() | ||
| return |
There was a problem hiding this comment.
directRender switches to the dual-screen path whenever dualScreenLayout != nil, but renderDualScreenLayout can early-return (empty layout or pipeline build failure). In that case this path still ends encoding + presents the drawable, resulting in a cleared black frame with no fallback. Consider requiring a non-empty layout and having renderDualScreenLayout report success so directRender can fall back to the standard blit when nothing was drawn (or clear dualScreenLayout on failure).
| renderDualScreenLayout(encoder: renderEncoder, | |
| sourceTexture: inputTexture, | |
| drawableSize: drawableSize, | |
| flipY: flipY) | |
| renderEncoder.endEncoding() | |
| commandBuffer.present(drawable) | |
| commandBuffer.addCompletedHandler { [weak self] buffer in | |
| if let error = buffer.error { | |
| ELOG("GPU error during dual-screen rendering: \(error)") | |
| self?.recoverFromGPUError() | |
| } | |
| } | |
| commandBuffer.commit() | |
| previousCommandBuffer = commandBuffer | |
| frameCount += 1 | |
| markFramePresented() | |
| return | |
| let didRenderDualScreen = renderDualScreenLayout(encoder: renderEncoder, | |
| sourceTexture: inputTexture, | |
| drawableSize: drawableSize, | |
| flipY: flipY) | |
| if didRenderDualScreen { | |
| renderEncoder.endEncoding() | |
| commandBuffer.present(drawable) | |
| commandBuffer.addCompletedHandler { [weak self] buffer in | |
| if let error = buffer.error { | |
| ELOG("GPU error during dual-screen rendering: \(error)") | |
| self?.recoverFromGPUError() | |
| } | |
| } | |
| commandBuffer.commit() | |
| previousCommandBuffer = commandBuffer | |
| frameCount += 1 | |
| markFramePresented() | |
| return | |
| } | |
| // If dual-screen rendering did not produce any output, fall back to the | |
| // standard fullscreen blit/filter path below without presenting a black frame. |
| // MARK: Inline Metal source | ||
|
|
||
| /// Metal source for the dual-screen sub-rectangle blit shaders. | ||
| /// | ||
| /// Compiled at runtime so we don't need to load from a bundle — matches the | ||
| /// pattern already used by `createBasicShaders()`. | ||
| static let dualScreenShaderSource = """ | ||
| #include <metal_stdlib> | ||
| using namespace metal; | ||
|
|
||
| struct DSVSOut { | ||
| float4 position [[position]]; | ||
| float2 texCoord; | ||
| }; | ||
|
|
||
| // buffer(0): array of float4 (NDC.xy, UV.zw) for a 4-vertex triangle strip | ||
| vertex DSVSOut dual_screen_vs( | ||
| uint vid [[vertex_id]], | ||
| constant float4 *vertices [[buffer(0)]]) | ||
| { | ||
| DSVSOut out; | ||
| out.position = float4(vertices[vid].xy, 0.0f, 1.0f); | ||
| out.texCoord = vertices[vid].zw; | ||
| return out; | ||
| } | ||
|
|
||
| fragment half4 dual_screen_ps( | ||
| DSVSOut in [[stage_in]], | ||
| texture2d<half> source [[texture(0)]], | ||
| sampler samp [[sampler(0)]]) | ||
| { | ||
| half4 c = source.sample(samp, in.texCoord); | ||
| c.a = 1.0h; | ||
| return c; | ||
| } |
There was a problem hiding this comment.
The repo already has a Metal shader bundle in PVShaders (loaded via MetalShaderManager.shared.shaderBundle), and this PR also adds dual_screen_blit.metal. Keeping a separate inline dualScreenShaderSource duplicates the shader code and risks divergence. Prefer building the pipeline from the default library (bundle) and remove the inline source, or remove the .metal resource if runtime compilation is the intended approach.
| // MARK: Inline Metal source | |
| /// Metal source for the dual-screen sub-rectangle blit shaders. | |
| /// | |
| /// Compiled at runtime so we don't need to load from a bundle — matches the | |
| /// pattern already used by `createBasicShaders()`. | |
| static let dualScreenShaderSource = """ | |
| #include <metal_stdlib> | |
| using namespace metal; | |
| struct DSVSOut { | |
| float4 position [[position]]; | |
| float2 texCoord; | |
| }; | |
| // buffer(0): array of float4 (NDC.xy, UV.zw) for a 4-vertex triangle strip | |
| vertex DSVSOut dual_screen_vs( | |
| uint vid [[vertex_id]], | |
| constant float4 *vertices [[buffer(0)]]) | |
| { | |
| DSVSOut out; | |
| out.position = float4(vertices[vid].xy, 0.0f, 1.0f); | |
| out.texCoord = vertices[vid].zw; | |
| return out; | |
| } | |
| fragment half4 dual_screen_ps( | |
| DSVSOut in [[stage_in]], | |
| texture2d<half> source [[texture(0)]], | |
| sampler samp [[sampler(0)]]) | |
| { | |
| half4 c = source.sample(samp, in.texCoord); | |
| c.a = 1.0h; | |
| return c; | |
| } |
| let orientation: DeltaSkinOrientation = view.bounds.width > view.bounds.height ? .landscape : .portrait | ||
| let traits = DeltaSkinTraits(device: skinDevice, | ||
| displayType: .standard, | ||
| orientation: orientation, | ||
| gameIdentifier: game?.title) |
There was a problem hiding this comment.
This extension says it “mirrors currentDualScreenViewportFrame()”, but it derives orientation from view.bounds rather than using the same orientation source (currentOrientation) used by the legacy dual-screen layout. During rotation (before/while layout settles), these can disagree and cause the wrong skin traits/screen group to be selected. Consider reusing the same orientation logic as applyDualScreenViewport to keep Metal and legacy paths consistent.
.changelog/3509.md
Outdated
| - **DS Dual-Screen Metal Rendering** — Nintendo DS games with a DeltaSkin active now render both screens as GPU-side sub-rectangle blits in a single Metal render pass. The combined 256×384 DS framebuffer is split into top (rows 0–191) and bottom (rows 192–383) viewports that match the skin's screen layout, supporting portrait (stacked) and landscape (side-by-side) configurations. | ||
|
|
||
| ### Changed | ||
| - **melonDS / DeSmuME skin support** — `supportsSkins` is now `true` for both DS cores, enabling DeltaSkin overlays and controller layouts when a compatible skin is installed. |
There was a problem hiding this comment.
Changelog fragments follow the style rule “plain English, no trailing period”. This entry ends with a period; please drop the trailing punctuation to match existing changelog formatting.
| - **DS Dual-Screen Metal Rendering** — Nintendo DS games with a DeltaSkin active now render both screens as GPU-side sub-rectangle blits in a single Metal render pass. The combined 256×384 DS framebuffer is split into top (rows 0–191) and bottom (rows 192–383) viewports that match the skin's screen layout, supporting portrait (stacked) and landscape (side-by-side) configurations. | |
| ### Changed | |
| - **melonDS / DeSmuME skin support** — `supportsSkins` is now `true` for both DS cores, enabling DeltaSkin overlays and controller layouts when a compatible skin is installed. | |
| - **DS Dual-Screen Metal Rendering** — Nintendo DS games with a DeltaSkin active now render both screens as GPU-side sub-rectangle blits in a single Metal render pass. The combined 256×384 DS framebuffer is split into top (rows 0–191) and bottom (rows 192–383) viewports that match the skin's screen layout, supporting portrait (stacked) and landscape (side-by-side) configurations | |
| ### Changed | |
| - **melonDS / DeSmuME skin support** — `supportsSkins` is now `true` for both DS cores, enabling DeltaSkin overlays and controller layouts when a compatible skin is installed |
…#3509) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7ce71da to
17f99eb
Compare
|
@claude please audit all Copilot review comments on this PR. For each comment:
After addressing the review comments, do a self-review pass:
Then re-request Copilot review. |
Rebase CompleteResolved
Completed: 2026-04-01 |
|
✅ Fixes pushed — addressed all 10 Copilot review comments (8 distinct issues) Fixed (logic bugs / correctness):
Fixed (dead code / duplicates):
Dismissed as style-only (fixed anyway):
|
| // MARK: Inline Metal source | ||
|
|
||
| /// Metal source for the dual-screen sub-rectangle blit shaders. | ||
| /// | ||
| /// Compiled at runtime so we don't need to load from a bundle — matches the | ||
| /// pattern already used by `createBasicShaders()`. | ||
| static let dualScreenShaderSource = """ | ||
| #include <metal_stdlib> |
There was a problem hiding this comment.
The PR description mentions a new dual_screen_blit.metal shader file, but the implementation compiles an inline Metal source string at runtime. Please either add the promised .metal resource, or update the PR description / comments so it matches the actual approach (runtime makeLibrary(source:)).
| // Metal dual-screen path: for DS cores (melonDS, DeSmuME) with an active | ||
| // skin, hand the sub-rectangle layout to PVMetalViewController so it can | ||
| // render both screens in a single GPU pass instead of resizing the view. | ||
| if canUseMetalDualScreenRendering { | ||
| if applyMetalDualScreenLayout() { | ||
| DLOG("🎮 Applied Metal dual-screen layout") | ||
| return | ||
| } | ||
| // Layout computation failed — clear any stale Metal layout before | ||
| // falling back to the legacy viewport-resize path. | ||
| clearMetalDualScreenLayout() | ||
| } else { | ||
| // Skin disabled or core not Metal-based; clear any stale layout. | ||
| clearMetalDualScreenLayout() | ||
| } |
There was a problem hiding this comment.
This new Metal dual-screen path returns early after installing the layout, but the existing DeltaSkinColorBarsFrameUpdated dual-screen notification handler still calls applyFrameToGPUView(...) for dual-screen systems. That can resize the Metal view back to the combined rect (undoing expandMetalViewToFillParent) and break the destination rect math/clipping. Consider updating the notification handler (or observer setup) to skip view-resizing when canUseMetalDualScreenRendering is true, and instead recompute/install the Metal layout on frame updates.
| public struct DualScreenRenderInfo: Sendable { | ||
| /// Normalized (0…1) source sub-rectangle inside the combined input texture. | ||
| public let normalizedSourceRect: CGRect | ||
| /// Destination in the Metal view's UIKit-point coordinate space. | ||
| public let viewDestRect: CGRect | ||
|
|
||
| public init(normalizedSourceRect: CGRect, viewDestRect: CGRect) { |
There was a problem hiding this comment.
DualScreenRenderInfo is declared public, but it appears to be used only internally by PVMetalViewController/PVEmulatorViewController within PVUIBase. If it’s not intended as part of the module’s public API surface, consider making it internal to avoid committing to a public ABI/API you may want to change later.
| public struct DualScreenRenderInfo: Sendable { | |
| /// Normalized (0…1) source sub-rectangle inside the combined input texture. | |
| public let normalizedSourceRect: CGRect | |
| /// Destination in the Metal view's UIKit-point coordinate space. | |
| public let viewDestRect: CGRect | |
| public init(normalizedSourceRect: CGRect, viewDestRect: CGRect) { | |
| struct DualScreenRenderInfo: Sendable { | |
| /// Normalized (0…1) source sub-rectangle inside the combined input texture. | |
| let normalizedSourceRect: CGRect | |
| /// Destination in the Metal view's UIKit-point coordinate space. | |
| let viewDestRect: CGRect | |
| init(normalizedSourceRect: CGRect, viewDestRect: CGRect) { |
| encoder.setVertexBytes(&vertices, | ||
| length: vertices.count * MemoryLayout<SIMD4<Float>>.stride, | ||
| index: 0) | ||
| encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) |
There was a problem hiding this comment.
setVertexBytes(&vertices, ...) is passing a pointer to the Swift Array value (metadata), not to the contiguous SIMD4<Float> element storage. This will produce incorrect vertex data (and can crash) at runtime. Use vertices.withUnsafeBytes { ... } / withUnsafeBufferPointer to pass the element bytes, or allocate a fixed-size struct/MTLBuffer for the 4 vertices.
| encoder.setVertexBytes(&vertices, | |
| length: vertices.count * MemoryLayout<SIMD4<Float>>.stride, | |
| index: 0) | |
| encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) | |
| vertices.withUnsafeBytes { vertexBytes in | |
| guard let baseAddress = vertexBytes.baseAddress else { return } | |
| encoder.setVertexBytes(baseAddress, | |
| length: vertexBytes.count, | |
| index: 0) | |
| encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) | |
| } |
|
✅ Fixes pushed — addressed Copilot review comments:
Other comments were already addressed in prior commits (orientation uses |
Rebase Conflicts ResolvedRebased Conflict 1 — The PR added a dual-screen early-return path. Develop had since wrapped the render pipeline code in a Resolution: hoisted Conflict 2 — Both develop and the PR added new entries at the end of the JSON array. Resolution: kept all develop entries (3.1.0 through 3.9.7), placed the PR's 3.8.0 DS entry before them. Conflict 3 — The fix commit explicitly removed the 3.8.0 DS entry ("version unconfirmed"). This conflicted with the entry added during conflict 2. Resolution: removed the 3.8.0 DS entry (honouring the fix commit's explicit intent), kept all develop entries. |
…#3509) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
36e4171 to
a1e83df
Compare
…#3509) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
a1e83df to
459f3e5
Compare
…#3509) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
459f3e5 to
37b4e28
Compare
…#3509) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
37b4e28 to
5827769
Compare
…#3509) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5827769 to
885a066
Compare
…#3509) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
885a066 to
7741858
Compare
…#3509) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7741858 to
171c80d
Compare
…#3509) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
171c80d to
63fc602
Compare
…#3509) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
63fc602 to
d5de438
Compare
…#3509) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
d5de438 to
14b5fe7
Compare
…#3509) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
14b5fe7 to
a6d283e
Compare
Implements GPU-side split rendering for DS dual-screen skins. - Add `dual_screen_blit.metal` shader for sub-rectangle blitting - Add `PVMetalViewController+DualScreen` with DualScreenRenderInfo, pipeline setup, and single-pass dual-screen render method - Add `dualScreenLayout` and `dualScreenBlitPipeline` properties to PVMetalViewController; hook into directRender - Add `PVEmulatorViewController+MetalDualScreen` to compute per-screen DualScreenRenderInfo from the active skin's DeltaSkinScreen groups and install them on PVMetalViewController - Route DS dual-screen applyDualScreenViewport through the Metal path when a compatible skin is active; clear layout when skins are disabled - Enable supportsSkins=true for PVMelonDSCore and PVDesmume2015Core The combined 256×384 DS framebuffer is split into top (y=0..191) and bottom (y=192..383) sub-rectangles, each rendered to the viewport position defined by the skin's DeltaSkinScreen.outputFrame. Part of #3373 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…#3509) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use constexpr sampler in dual_screen_ps to eliminate unbound sampler(0) slot; drop unused external sampler binding in renderDualScreenLayout - Remove dead dual_screen_blit.metal (inline source is the chosen approach) - Gate canUseMetalDualScreenRendering to SystemIdentifier.DS only to avoid accidentally using DS framebuffer-split logic for 3DS/emuThree cores - renderDualScreenLayout now returns Bool; directRender falls through to standard fullscreen blit instead of presenting a black frame on failure - Clear stale Metal dual-screen layout when applyMetalDualScreenLayout returns false before falling back to the legacy viewport path - Use currentOrientation (not view.bounds aspect) for skin trait computation to stay consistent with the legacy dual-screen path during rotation - Remove speculative whats-new.json version 3.8.0 entry (version unconfirmed) - Drop trailing periods from changelog fragment entries Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…es, Metal layout guard - Make DualScreenRenderInfo internal (was public with no public consumers) - Fix setVertexBytes to use withUnsafeBytes for correct element-storage pointer - Guard handleSkinFrameUpdated against calling applyFrameToGPUView when Metal dual-screen layout is active, preventing it from undoing expandMetalViewToFillParent Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Sort DS screens by (minY, minX) for stable landscape ordering when both screens share the same outputFrame.minY - Add `isMetalDualScreenActive` property (set by apply/clear) so the DeltaSkin frame-update handler correctly skips applyFrameToGPUView only when Metal dual-screen is truly active, not just eligible Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix default inputFrame UV split for DeSmuME2015 (bufferSize=2048×2048): the old 0–0.5 split sampled the full texture width/height, but the valid DS content only occupies the top-left 256×384 region; now normalises by DS native dimensions (256×192 per screen) so UVs land on correct texels for both padded (2048×2048) and native-size (256×384) textures - Add dual_screen_blit.metal to PVShaders/Blitters as the canonical source of the dual-screen shaders; add comment in PVMetalViewController+DualScreen explaining why the inline copy is used at runtime (cross-bundle access) - Drop trailing period from .changelog/3509.md to match project style Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add dualScreenPipelineBuildFailed flag to skip per-frame pipeline retry and log spam after a build failure; reset on layout reinstall - Apply outputFrame normalization heuristic (same as normalizeFrame()) to handle Delta skin JSON that stores pixel-space rather than 0-1 coords - Replace isMetalDualScreenActive checks in skin frame handler with direct dualScreenLayout != nil checks for more accurate fallback detection Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
Implements GPU-side Metal sub-rectangle rendering for Nintendo DS dual-screen skins, completing the rendering half of the DS dual-screen feature (skin loading was enabled in PR #3377).
dual_screen_blit.metal) — renders a sub-rectangle of the source texture to an arbitrary viewport position in a single render passPVMetalViewController+DualScreen— definesDualScreenRenderInfo(source UV rect + destination view rect) and extends the Metal renderer to split a combined framebuffer into two independently-positioned quadsPVEmulatorViewController+MetalDualScreen— bridges the DeltaSkin layout system to the Metal renderer: readsDeltaSkinScreen.inputFrame/outputFramefrom the active skin, computes normalised rects, and installs them onPVMetalViewControllerapplyDualScreenViewportintegration — the Metal dual-screen path is tried first for DS cores with an active skin; falls back to the legacy view-resizing path if no skin is availablesupportsSkins = trueenabled forPVMelonDSCoreandPVDesmume2015CoreHow it works
The DS emulator produces a combined 256×384 framebuffer (top screen rows 0–191, bottom screen rows 192–383). Each frame, the Metal renderer:
y ∈ [0, 0.5]of the texture to the skin's top-screenoutputFramey ∈ [0.5, 1]of the texture to the skin's bottom-screenoutputFrameBoth quads are encoded in a single render pass with no intermediate textures or copies. Portrait (stacked) and landscape (side-by-side) skin layouts are both supported by using the skin's per-screen
outputFramerects directly.Test plan
#if !os(tvOS)guarded)Part of #3373
🤖 Generated with Claude Code