From 0f1709f25cf41a6729dce5c15ec198581448d304 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Mon, 12 Oct 2026 23:05:30 +0200 Subject: [PATCH] wip(spatialgui): isolate screen render state via second GuiRenderer Also repairs origin/main: commit 39b270b1 accidentally registered two SpatialGui mixins in lovisual.mixins.json without committing their sources; this commit ships them. Refactors the SpatialGui capture path to follow the upstream Spatial-GUI architecture. Instead of cancelling and re-targeting the main GuiRenderer.draw() pass (which required shadowing private fields and rebuilding the projection matrices), the open screen is now extracted into an isolated GuiRenderState + GuiRenderer pair owned by SpatialGuiCapture: - GuiSpatialExtractMixin @Redirects Screen.extractRenderStateWithTooltipAndSubtitles inside Gui.extractRenderState: while SpatialGui.isCapturing(), the screen is extracted into SpatialGuiCapture.extractScreen() using a private GuiRenderState instead of the vanilla main state, so the HUD pass no longer sees the screen content. - GameRendererSpatialGuiMixin injects twice around GameRenderer.render: just before GuiRenderer.render() it resizes and clears the capture texture to transparent from the frame's WindowRenderState, and at TAIL it runs the isolated renderer's render()+endFrame() so the screen's draw list executes into the capture texture once the main HUD pass is done. - GuiRendererCaptureMixin is now a @ModifyArg on the target argument of executeDrawRange inside GuiRenderer.draw(): if the instance is the isolated one, the target is swapped for SpatialGuiCapture.target(); the vanilla main renderer keeps its normal framebuffer untouched. SpatialGuiCapture gains screenRenderState, screenGuiRenderer, and the extractScreen/renderIsolatedFrame/clearTransparent/target helpers. SpatialGui.onRenderWorldEngine gets DebugLog.renderThreadOnChange traces at each early-return branch (noLevel, closed, captureNotReady, noTextureView, alpha0, published, closing, mesh) so plane publishing can be diagnosed from a log without a debugger. Docstring in SpatialGui updated to describe the new pipeline. Message kept wip: this pass compiles clean and is behaviour-plausible, but I have not run it against a live Minecraft session this turn. --- .../visuals/spatialgui/SpatialGui.java | 24 ++++- .../GameRendererSpatialGuiMixin.java | 74 +++++++++++++++ .../spatialgui/GuiRendererCaptureMixin.java | 94 ++++--------------- .../spatialgui/GuiSpatialExtractMixin.java | 43 +++++++++ .../render/spatialgui/SpatialGuiCapture.java | 76 ++++++++++++++- 5 files changed, 227 insertions(+), 84 deletions(-) create mode 100644 mod/src/main/java/dev/loki/lovisual/mixins/render/spatialgui/GameRendererSpatialGuiMixin.java create mode 100644 mod/src/main/java/dev/loki/lovisual/mixins/render/spatialgui/GuiSpatialExtractMixin.java diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/spatialgui/SpatialGui.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/spatialgui/SpatialGui.java index 65a0e41b..dff44bdb 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/spatialgui/SpatialGui.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/spatialgui/SpatialGui.java @@ -24,6 +24,7 @@ import dev.loki.lovisual.render.engine.renderer.Renderer3D; import dev.loki.lovisual.render.spatialgui.SpatialGuiCapture; import dev.loki.lovisual.render.spatialgui.SpatialGuiPlane; import dev.loki.lovisual.render.engine.uniform.MeshBuilder; +import dev.loki.lovisual.util.logging.DebugLog; import net.minecraft.client.Camera; import net.minecraft.client.CameraType; import net.minecraft.client.Minecraft; @@ -38,10 +39,12 @@ import org.joml.Vector3fc; * porting the core idea of Spatial GUI (https://github.com/tastytrash/Spatial-GUI, MIT) to LoVisual's own * module/config system and its deferred GUI-render pipeline. *

- * The actual off-screen capture happens in {@code GuiRendererCaptureMixin} (which redirects the vanilla - * {@code GuiRenderer.draw()} pass into {@link SpatialGuiCapture} instead of the main render target while this - * module is active), and mouse routing happens in {@code MouseHandlerSpatialGuiMixin} (which reprojects the - * real cursor position onto the plane published every frame via {@link SpatialGuiPlane}). + * The off-screen capture follows the reference architecture: {@code GuiSpatialExtractMixin} diverts the open + * screen's extraction from the vanilla main GUI state into {@link SpatialGuiCapture}'s isolated state, + * {@code GameRendererSpatialGuiMixin} clears the capture texture and runs the isolated renderer at the end of + * the frame, and {@code GuiRendererCaptureMixin} swaps that renderer's draw target for the capture texture — + * mouse routing happens in {@code MouseHandlerSpatialGuiMixin} (which reprojects the real cursor position onto + * the plane published every frame via {@link SpatialGuiPlane}). */ @ModuleInfo( id = "spatialgui", @@ -168,23 +171,29 @@ public class SpatialGui extends Module { @Override public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) { Minecraft mc = Minecraft.getInstance(); - if (mc.player == null || mc.level == null) return; + if (mc.player == null || mc.level == null) { + DebugLog.renderThreadOnChange("sg.plane", "noLevel", "[SpatialGui] plane skipped: no level"); + return; + } boolean liveScreen = trackedScreen != null; float progress = animationProgress(); if (progress <= 0.0F && !liveScreen) { SpatialGuiPlane.clear(); + DebugLog.renderThreadOnChange("sg.plane", "closed", "[SpatialGui] plane skipped: screen closed"); return; } SpatialGuiCapture capture = SpatialGuiCapture.get(); if (!capture.isReady()) { SpatialGuiPlane.clear(); + DebugLog.renderThreadOnChange("sg.plane", "captureNotReady", "[SpatialGui] plane skipped: capture not ready"); return; } GpuTextureView textureView = capture.colorTextureView(); if (textureView == null) { SpatialGuiPlane.clear(); + DebugLog.renderThreadOnChange("sg.plane", "noTextureView", "[SpatialGui] plane skipped: no texture view"); return; } @@ -245,6 +254,7 @@ public class SpatialGui extends Module { float drawAlpha = "Fade".equals(style) ? baseAlpha * ease(progress) : baseAlpha; if (drawAlpha <= 0.001F) { SpatialGuiPlane.clear(); + DebugLog.renderThreadOnChange("sg.plane", "alpha0", "[SpatialGui] plane skipped: drawAlpha<=0"); return; } @@ -261,6 +271,7 @@ public class SpatialGui extends Module { } MeshBuilder mesh = renderer.batchTextured(LoVisualRenderPipelines.WORLD_TEXTURED_PREMULTIPLIED, textureView, cachedSampler, Renderer3D.DepthMode.NONE); + DebugLog.renderThreadOnChange("sg.plane.mesh", mesh != null, "[SpatialGui] plane mesh=%b alpha=%.2f", mesh != null, drawAlpha); if (mesh != null) { // The capture holds premultiplied colour (GUI draws blended onto a transparent target), so the fade // factor must scale RGB together with alpha. @@ -275,8 +286,11 @@ public class SpatialGui extends Module { if (liveScreen) { SpatialGuiPlane.publish(new SpatialGuiPlane(center, planeRight, planeUp, halfWidth, halfHeight, guiWidth, guiHeight)); + DebugLog.renderThreadOnChange("sg.plane", "published", + "[SpatialGui] plane published center=(%.2f, %.2f, %.2f) size=%.2fx%.2f", center.x, center.y, center.z, halfWidth * 2.0F, halfHeight * 2.0F); } else { SpatialGuiPlane.clear(); + DebugLog.renderThreadOnChange("sg.plane", "closing", "[SpatialGui] plane cleared: closing animation"); } } diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/spatialgui/GameRendererSpatialGuiMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/spatialgui/GameRendererSpatialGuiMixin.java new file mode 100644 index 00000000..617c4bd8 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/spatialgui/GameRendererSpatialGuiMixin.java @@ -0,0 +1,74 @@ +/* + * Ported/adapted from Spatial GUI (https://github.com/tastytrash/Spatial-GUI), MIT License. + * Copyright (c) tastytrash and Spatial GUI contributors. + * + * This file is part of LoVisual and is distributed under LoVisual's own GPLv3 license terms + * for the modifications made here (see mod/LICENSE and mod/THIRD_PARTY_NOTICES.md); the MIT-licensed + * portions ported from Spatial GUI remain available under the MIT terms above. + */ +package dev.loki.lovisual.mixins.render.spatialgui; + +import dev.loki.lovisual.features.module.modules.visuals.spatialgui.SpatialGui; +import dev.loki.lovisual.render.spatialgui.SpatialGuiCapture; +import dev.loki.lovisual.util.logging.DebugLog; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.gui.render.GuiRenderer; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.state.WindowRenderState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * Adapted from Spatial-GUI's {@code GameRendererMixin}: right before the main GUI pass the capture texture is + * (re)sized to the frame's window state and cleared to transparent, and at the end of the frame the isolated + * screen renderer built by {@link SpatialGuiCapture} runs its own prepare/draw cycle so the open screen lands + * in the capture texture while the flat overlay stays absent. + */ +@Mixin(GameRenderer.class) +public abstract class GameRendererSpatialGuiMixin { + + @Inject(method = "render", at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/gui/render/GuiRenderer;render()V")) + private void lovisual$prepareSpatialCapture(DeltaTracker deltaTracker, boolean renderLevel, CallbackInfo ci) { + SpatialGui module = SpatialGui.getInstance(); + if (module == null || !module.isCapturing()) { + DebugLog.renderThreadOnChange("sg.prepare", "notCapturing", + "[SpatialGui] prepare skipped: not capturing"); + return; + } + + WindowRenderState windowState = ((GameRenderer) (Object) this).gameRenderState().windowRenderState; + if (windowState == null || windowState.width <= 0 || windowState.height <= 0) { + DebugLog.renderThreadOnChange("sg.prepare", "badWindowState", + "[SpatialGui] prepare skipped: windowState=%s", String.valueOf(windowState)); + return; + } + + SpatialGuiCapture capture = SpatialGuiCapture.get(); + capture.ensure(windowState.width, windowState.height); + capture.clearTransparent(); + DebugLog.renderThreadOnChange("sg.prepare", windowState.width + "x" + windowState.height, + "[SpatialGui] capture prepared and cleared %dx%d", windowState.width, windowState.height); + } + + @Inject(method = "render", at = @At("TAIL")) + private void lovisual$renderIsolatedSpatialScreen(DeltaTracker deltaTracker, boolean renderLevel, CallbackInfo ci) { + SpatialGui module = SpatialGui.getInstance(); + if (module == null || !module.isCapturing()) { + DebugLog.renderThreadOnChange("sg.tail", "notCapturing", + "[SpatialGui] isolated TAIL skipped: not capturing"); + return; + } + if (!SpatialGuiCapture.get().isReady()) { + DebugLog.renderThreadOnChange("sg.tail", "notReady", + "[SpatialGui] isolated TAIL skipped: capture not ready"); + return; + } + + DebugLog.renderThreadOnChange("sg.tail", "run", + "[SpatialGui] isolated render frame"); + SpatialGuiCapture.get().renderIsolatedFrame(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/spatialgui/GuiRendererCaptureMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/spatialgui/GuiRendererCaptureMixin.java index 10a74b29..f6249cb8 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/spatialgui/GuiRendererCaptureMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/spatialgui/GuiRendererCaptureMixin.java @@ -8,92 +8,38 @@ */ package dev.loki.lovisual.mixins.render.spatialgui; -import com.mojang.blaze3d.ProjectionType; -import com.mojang.blaze3d.buffers.GpuBufferSlice; import com.mojang.blaze3d.pipeline.RenderTarget; -import com.mojang.blaze3d.systems.RenderSystem; import dev.loki.lovisual.features.module.modules.visuals.spatialgui.SpatialGui; import dev.loki.lovisual.render.spatialgui.SpatialGuiCapture; -import net.minecraft.client.Minecraft; +import dev.loki.lovisual.util.logging.DebugLog; import net.minecraft.client.gui.render.GuiRenderer; -import net.minecraft.client.renderer.Projection; -import net.minecraft.client.renderer.ProjectionMatrixBuffer; -import net.minecraft.client.renderer.state.WindowRenderState; -import org.joml.Matrix4f; -import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -import java.util.List; -import java.util.function.Supplier; +import org.spongepowered.asm.mixin.injection.ModifyArg; /** - * Adapted from Spatial-GUI's {@code ScreenExtractor}/{@code TextureTargetManager}: on this Minecraft version the - * whole 2D GUI pass is a deferred draw list executed once per frame by {@link GuiRenderer#draw()} against an - * explicit {@link RenderTarget}. Redirecting the currently open screen into a 3D plane therefore only requires - * pointing that one draw call at {@link SpatialGuiCapture}'s off-screen target instead of the main render - * target, and cancelling the vanilla draw so nothing shows up as a flat overlay. - *

- * The vertex data itself is prepared and uploaded earlier in {@code GuiRenderer.render()} (which this mixin does - * not touch), so this only ever affects where the already-built draw list is executed to. + * Adapted from Spatial-GUI's {@code GuiRendererMixin}: every {@code draw()} executes its prepared draw list + * through {@code executeDrawRange(label, target, ...)} — when the instance is the isolated screen renderer + * built by {@link SpatialGuiCapture} while SpatialGui is capturing, the target argument is swapped for the + * off-screen capture texture. The vanilla main GuiRenderer (HUD) keeps its normal framebuffer, and the flat + * screen overlay is naturally absent because the screen was extracted into the isolated state instead. */ -@Mixin(value = GuiRenderer.class, priority = 900) +@Mixin(GuiRenderer.class) public abstract class GuiRendererCaptureMixin { - @Shadow - @Final - private List draws; - - @Shadow - private int firstDrawIndexAfterBlur; - - @Shadow - @Final - private Projection guiProjection; - - @Shadow - @Final - private ProjectionMatrixBuffer guiProjectionMatrixBuffer; - - @Shadow - private void executeDrawRange(Supplier label, RenderTarget target, GpuBufferSlice transform, int startIndex, int endIndex) { - throw new AssertionError(); - } - - @Inject(method = "draw", at = @At("HEAD"), cancellable = true) - private void lovisual$captureToSpatialPlane(CallbackInfo ci) { - if (draws == null || draws.isEmpty()) return; - + @ModifyArg( + method = "draw", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/gui/render/GuiRenderer;executeDrawRange(Ljava/util/function/Supplier;Lcom/mojang/blaze3d/pipeline/RenderTarget;Lcom/mojang/blaze3d/buffers/GpuBufferSlice;II)V"), + index = 1) + private RenderTarget lovisual$redirectSpatialCapture(RenderTarget original) { SpatialGui module = SpatialGui.getInstance(); - if (module == null || !module.isCapturing()) return; - - Minecraft mc = Minecraft.getInstance(); - if (mc == null || mc.gameRenderer == null) return; - WindowRenderState windowState = mc.gameRenderer.gameRenderState().windowRenderState; - if (windowState == null || windowState.guiScale <= 0) return; - + if (module == null || !module.isCapturing()) return original; SpatialGuiCapture capture = SpatialGuiCapture.get(); - RenderTarget target = capture.ensure(windowState.width, windowState.height); - capture.clearTransparent(); - - guiProjection.setupOrtho(1000.0F, 11000.0F, - (float) windowState.width / windowState.guiScale, (float) windowState.height / windowState.guiScale, true); - RenderSystem.setProjectionMatrix(guiProjectionMatrixBuffer.getBuffer(guiProjection), ProjectionType.ORTHOGRAPHIC); - GpuBufferSlice transform = RenderSystem.getDynamicUniforms().writeTransform(new Matrix4f().setTranslation(0.0F, 0.0F, -11000.0F)); - - int splitIndex = Math.min(firstDrawIndexAfterBlur, draws.size()); - if (splitIndex > 0) { - executeDrawRange(() -> "spatial gui before blur", target, transform, 0, splitIndex); - } - if (draws.size() > firstDrawIndexAfterBlur) { - RenderSystem.getDevice().createCommandEncoder().clearDepthTexture(target.getDepthTexture(), 0.0D); - mc.gameRenderer.processBlurEffect(); - executeDrawRange(() -> "spatial gui after blur", target, transform, firstDrawIndexAfterBlur, draws.size()); - } - - ci.cancel(); + if ((Object) this != capture.screenGuiRenderer()) return original; + RenderTarget redirect = capture.target(); + if (redirect == null) return original; + DebugLog.renderThreadOnce("sg.swap", "[SpatialGui] draw target swapped to capture texture"); + return redirect; } } diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/spatialgui/GuiSpatialExtractMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/spatialgui/GuiSpatialExtractMixin.java new file mode 100644 index 00000000..ee20653a --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/spatialgui/GuiSpatialExtractMixin.java @@ -0,0 +1,43 @@ +/* + * Ported/adapted from Spatial GUI (https://github.com/tastytrash/Spatial-GUI), MIT License. + * Copyright (c) tastytrash and Spatial GUI contributors. + * + * This file is part of LoVisual and is distributed under LoVisual's own GPLv3 license terms + * for the modifications made here (see mod/LICENSE and mod/THIRD_PARTY_NOTICES.md); the MIT-licensed + * portions ported from Spatial GUI remain available under the MIT terms above. + */ +package dev.loki.lovisual.mixins.render.spatialgui; + +import dev.loki.lovisual.features.module.modules.visuals.spatialgui.SpatialGui; +import dev.loki.lovisual.render.spatialgui.SpatialGuiCapture; +import dev.loki.lovisual.util.logging.DebugLog; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.screens.Screen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +/** + * Adapted from Spatial-GUI's {@code GuiMixin}: while SpatialGui is capturing, the open screen's extraction + * is diverted from the vanilla main GUI render state into {@link SpatialGuiCapture}'s isolated state, so the + * main pass draws only the HUD and the screen content lands exclusively in the capture texture for the 3D + * plane. Mouse coordinates arrive already reprojected onto the plane via the MouseHandler mixin. + */ +@Mixin(Gui.class) +public abstract class GuiSpatialExtractMixin { + + @Redirect(method = "extractRenderState", at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/gui/screens/Screen;extractRenderStateWithTooltipAndSubtitles(Lnet/minecraft/client/gui/GuiGraphicsExtractor;IIF)V")) + private void lovisual$isolateScreenExtraction(Screen screen, GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) { + SpatialGui module = SpatialGui.getInstance(); + boolean capturing = module != null && module.isCapturing(); + DebugLog.renderThreadOnChange("sg.extract", capturing, + "[SpatialGui] screen extraction divert=%b screen=%s", capturing, screen.getClass().getSimpleName()); + if (capturing) { + SpatialGuiCapture.get().extractScreen(screen, mouseX, mouseY, partialTick); + } else { + screen.extractRenderStateWithTooltipAndSubtitles(graphics, mouseX, mouseY, partialTick); + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/render/spatialgui/SpatialGuiCapture.java b/mod/src/main/java/dev/loki/lovisual/render/spatialgui/SpatialGuiCapture.java index 226c349e..95bc5184 100644 --- a/mod/src/main/java/dev/loki/lovisual/render/spatialgui/SpatialGuiCapture.java +++ b/mod/src/main/java/dev/loki/lovisual/render/spatialgui/SpatialGuiCapture.java @@ -14,16 +14,30 @@ import com.mojang.blaze3d.pipeline.TextureTarget; import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.textures.GpuTexture; import com.mojang.blaze3d.textures.GpuTextureView; +import dev.loki.lovisual.util.logging.DebugLog; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.render.GuiRenderer; +import net.minecraft.client.gui.render.pip.GuiBannerResultRenderer; +import net.minecraft.client.gui.render.pip.GuiBookModelRenderer; +import net.minecraft.client.gui.render.pip.GuiEntityRenderer; +import net.minecraft.client.gui.render.pip.GuiProfilerChartRenderer; +import net.minecraft.client.gui.render.pip.GuiSkinRenderer; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.renderer.state.gui.GuiRenderState; import org.joml.Vector4f; +import java.util.List; + /** * Owns the off-screen {@link RenderTarget} that the currently open {@link net.minecraft.client.gui.screens.Screen} - * is redirected into when the SpatialGui module is displaying it as a 3D plane instead of a flat overlay. + * is rendered into while the SpatialGui module displays it as a 3D plane instead of a flat overlay. *

- * This replaces Spatial-GUI's {@code TextureTargetManager}/{@code ScreenExtractor} pair: on this Minecraft - * version the GUI is drawn through a deferred {@code GuiRenderer} that executes its draw list against an - * explicit {@link RenderTarget}, so capturing the screen only requires swapping that target for the duration - * of one draw call (see {@code GuiRendererCaptureMixin}) instead of hooking a legacy immediate-mode GL pass. + * Follows the reference Spatial-GUI architecture: the screen is extracted into a private + * {@link GuiRenderState} served by a second {@link GuiRenderer} instance (see the spatialgui mixins), and that + * instance's draw pass is redirected into {@link #target()} while the vanilla main GUI pass keeps rendering + * the HUD untouched. Texture size matches the window framebuffer; the target is cleared to transparent once + * per frame before the main GUI pass. */ public final class SpatialGuiCapture { private static final SpatialGuiCapture INSTANCE = new SpatialGuiCapture(); @@ -32,6 +46,9 @@ public final class SpatialGuiCapture { private int width; private int height; + private GuiRenderState screenRenderState; + private GuiRenderer screenGuiRenderer; + private SpatialGuiCapture() { } @@ -57,6 +74,17 @@ public final class SpatialGuiCapture { return target; } + /** Current capture target, created lazily against the real window size if nothing prepared it yet. */ + public TextureTarget target() { + if (target == null) { + Minecraft mc = Minecraft.getInstance(); + if (mc != null && mc.getWindow() != null) { + ensure(mc.getWindow().getWidth(), mc.getWindow().getHeight()); + } + } + return target; + } + public void clearTransparent() { if (target == null) return; GpuTexture colorTexture = target.getColorTexture(); @@ -69,6 +97,44 @@ public final class SpatialGuiCapture { } } + /** Second GuiRenderer whose draw pass is redirected into the capture target; built on first use. */ + public GuiRenderer screenGuiRenderer() { + if (screenGuiRenderer == null) { + Minecraft mc = Minecraft.getInstance(); + screenRenderState = new GuiRenderState(); + screenGuiRenderer = new GuiRenderer( + screenRenderState, + mc.gameRenderer.featureRenderDispatcher(), + List.of( + new GuiEntityRenderer(mc.getEntityRenderDispatcher()), + new GuiSkinRenderer(), + new GuiBookModelRenderer(), + new GuiBannerResultRenderer(mc.getAtlasManager()), + new GuiProfilerChartRenderer())); + } + return screenGuiRenderer; + } + + /** Extracts the screen into the isolated render state instead of the vanilla main GUI state. */ + public void extractScreen(Screen screen, int mouseX, int mouseY, float partialTick) { + Minecraft mc = Minecraft.getInstance(); + screenGuiRenderer(); + GuiGraphicsExtractor graphics = new GuiGraphicsExtractor(mc, screenRenderState, mouseX, mouseY); + screen.extractRenderStateWithTooltipAndSubtitles(graphics, mouseX, mouseY, partialTick); + int[] count = {0}; + screenRenderState.forEachElement(e -> count[0]++, GuiRenderState.TraverseRange.ALL); + DebugLog.renderThreadOnChange("sg.extract.elements", count[0], + "[SpatialGui] isolated render state elements=%d screen=%s", count[0], screen.getClass().getSimpleName()); + } + + /** Builds and draws the isolated frame: prepare, execute into the capture target, reset the state. */ + public void renderIsolatedFrame() { + GuiRenderer renderer = screenGuiRenderer(); + renderer.render(); + renderer.endFrame(); + DebugLog.renderThreadOnce("sg.isolated.render", "[SpatialGui] isolated render pass executed"); + } + public GpuTextureView colorTextureView() { return target == null ? null : target.getColorTextureView(); }