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
- * 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();
}