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.
This commit is contained in:
parent
4fce425dcd
commit
0f1709f25c
5 changed files with 227 additions and 84 deletions
|
|
@ -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.SpatialGuiCapture;
|
||||||
import dev.loki.lovisual.render.spatialgui.SpatialGuiPlane;
|
import dev.loki.lovisual.render.spatialgui.SpatialGuiPlane;
|
||||||
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
|
||||||
|
import dev.loki.lovisual.util.logging.DebugLog;
|
||||||
import net.minecraft.client.Camera;
|
import net.minecraft.client.Camera;
|
||||||
import net.minecraft.client.CameraType;
|
import net.minecraft.client.CameraType;
|
||||||
import net.minecraft.client.Minecraft;
|
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
|
* 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.
|
* module/config system and its deferred GUI-render pipeline.
|
||||||
* <p>
|
* <p>
|
||||||
* The actual off-screen capture happens in {@code GuiRendererCaptureMixin} (which redirects the vanilla
|
* The off-screen capture follows the reference architecture: {@code GuiSpatialExtractMixin} diverts the open
|
||||||
* {@code GuiRenderer.draw()} pass into {@link SpatialGuiCapture} instead of the main render target while this
|
* screen's extraction from the vanilla main GUI state into {@link SpatialGuiCapture}'s isolated state,
|
||||||
* module is active), and mouse routing happens in {@code MouseHandlerSpatialGuiMixin} (which reprojects the
|
* {@code GameRendererSpatialGuiMixin} clears the capture texture and runs the isolated renderer at the end of
|
||||||
* real cursor position onto the plane published every frame via {@link SpatialGuiPlane}).
|
* 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(
|
@ModuleInfo(
|
||||||
id = "spatialgui",
|
id = "spatialgui",
|
||||||
|
|
@ -168,23 +171,29 @@ public class SpatialGui extends Module {
|
||||||
@Override
|
@Override
|
||||||
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
|
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta) {
|
||||||
Minecraft mc = Minecraft.getInstance();
|
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;
|
boolean liveScreen = trackedScreen != null;
|
||||||
float progress = animationProgress();
|
float progress = animationProgress();
|
||||||
if (progress <= 0.0F && !liveScreen) {
|
if (progress <= 0.0F && !liveScreen) {
|
||||||
SpatialGuiPlane.clear();
|
SpatialGuiPlane.clear();
|
||||||
|
DebugLog.renderThreadOnChange("sg.plane", "closed", "[SpatialGui] plane skipped: screen closed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SpatialGuiCapture capture = SpatialGuiCapture.get();
|
SpatialGuiCapture capture = SpatialGuiCapture.get();
|
||||||
if (!capture.isReady()) {
|
if (!capture.isReady()) {
|
||||||
SpatialGuiPlane.clear();
|
SpatialGuiPlane.clear();
|
||||||
|
DebugLog.renderThreadOnChange("sg.plane", "captureNotReady", "[SpatialGui] plane skipped: capture not ready");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
GpuTextureView textureView = capture.colorTextureView();
|
GpuTextureView textureView = capture.colorTextureView();
|
||||||
if (textureView == null) {
|
if (textureView == null) {
|
||||||
SpatialGuiPlane.clear();
|
SpatialGuiPlane.clear();
|
||||||
|
DebugLog.renderThreadOnChange("sg.plane", "noTextureView", "[SpatialGui] plane skipped: no texture view");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,6 +254,7 @@ public class SpatialGui extends Module {
|
||||||
float drawAlpha = "Fade".equals(style) ? baseAlpha * ease(progress) : baseAlpha;
|
float drawAlpha = "Fade".equals(style) ? baseAlpha * ease(progress) : baseAlpha;
|
||||||
if (drawAlpha <= 0.001F) {
|
if (drawAlpha <= 0.001F) {
|
||||||
SpatialGuiPlane.clear();
|
SpatialGuiPlane.clear();
|
||||||
|
DebugLog.renderThreadOnChange("sg.plane", "alpha0", "[SpatialGui] plane skipped: drawAlpha<=0");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -261,6 +271,7 @@ public class SpatialGui extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
MeshBuilder mesh = renderer.batchTextured(LoVisualRenderPipelines.WORLD_TEXTURED_PREMULTIPLIED, textureView, cachedSampler, Renderer3D.DepthMode.NONE);
|
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) {
|
if (mesh != null) {
|
||||||
// The capture holds premultiplied colour (GUI draws blended onto a transparent target), so the fade
|
// The capture holds premultiplied colour (GUI draws blended onto a transparent target), so the fade
|
||||||
// factor must scale RGB together with alpha.
|
// factor must scale RGB together with alpha.
|
||||||
|
|
@ -275,8 +286,11 @@ public class SpatialGui extends Module {
|
||||||
|
|
||||||
if (liveScreen) {
|
if (liveScreen) {
|
||||||
SpatialGuiPlane.publish(new SpatialGuiPlane(center, planeRight, planeUp, halfWidth, halfHeight, guiWidth, guiHeight));
|
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 {
|
} else {
|
||||||
SpatialGuiPlane.clear();
|
SpatialGuiPlane.clear();
|
||||||
|
DebugLog.renderThreadOnChange("sg.plane", "closing", "[SpatialGui] plane cleared: closing animation");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,92 +8,38 @@
|
||||||
*/
|
*/
|
||||||
package dev.loki.lovisual.mixins.render.spatialgui;
|
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.pipeline.RenderTarget;
|
||||||
import com.mojang.blaze3d.systems.RenderSystem;
|
|
||||||
import dev.loki.lovisual.features.module.modules.visuals.spatialgui.SpatialGui;
|
import dev.loki.lovisual.features.module.modules.visuals.spatialgui.SpatialGui;
|
||||||
import dev.loki.lovisual.render.spatialgui.SpatialGuiCapture;
|
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.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.Mixin;
|
||||||
import org.spongepowered.asm.mixin.Shadow;
|
|
||||||
import org.spongepowered.asm.mixin.injection.At;
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
import org.spongepowered.asm.mixin.injection.Inject;
|
import org.spongepowered.asm.mixin.injection.ModifyArg;
|
||||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adapted from Spatial-GUI's {@code ScreenExtractor}/{@code TextureTargetManager}: on this Minecraft version the
|
* Adapted from Spatial-GUI's {@code GuiRendererMixin}: every {@code draw()} executes its prepared draw list
|
||||||
* whole 2D GUI pass is a deferred draw list executed once per frame by {@link GuiRenderer#draw()} against an
|
* through {@code executeDrawRange(label, target, ...)} — when the instance is the isolated screen renderer
|
||||||
* explicit {@link RenderTarget}. Redirecting the currently open screen into a 3D plane therefore only requires
|
* built by {@link SpatialGuiCapture} while SpatialGui is capturing, the target argument is swapped for the
|
||||||
* pointing that one draw call at {@link SpatialGuiCapture}'s off-screen target instead of the main render
|
* off-screen capture texture. The vanilla main GuiRenderer (HUD) keeps its normal framebuffer, and the flat
|
||||||
* target, and cancelling the vanilla draw so nothing shows up as a flat overlay.
|
* screen overlay is naturally absent because the screen was extracted into the isolated state instead.
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
@Mixin(value = GuiRenderer.class, priority = 900)
|
@Mixin(GuiRenderer.class)
|
||||||
public abstract class GuiRendererCaptureMixin {
|
public abstract class GuiRendererCaptureMixin {
|
||||||
|
|
||||||
@Shadow
|
@ModifyArg(
|
||||||
@Final
|
method = "draw",
|
||||||
private List<?> draws;
|
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"),
|
||||||
@Shadow
|
index = 1)
|
||||||
private int firstDrawIndexAfterBlur;
|
private RenderTarget lovisual$redirectSpatialCapture(RenderTarget original) {
|
||||||
|
|
||||||
@Shadow
|
|
||||||
@Final
|
|
||||||
private Projection guiProjection;
|
|
||||||
|
|
||||||
@Shadow
|
|
||||||
@Final
|
|
||||||
private ProjectionMatrixBuffer guiProjectionMatrixBuffer;
|
|
||||||
|
|
||||||
@Shadow
|
|
||||||
private void executeDrawRange(Supplier<String> 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;
|
|
||||||
|
|
||||||
SpatialGui module = SpatialGui.getInstance();
|
SpatialGui module = SpatialGui.getInstance();
|
||||||
if (module == null || !module.isCapturing()) return;
|
if (module == null || !module.isCapturing()) return original;
|
||||||
|
|
||||||
Minecraft mc = Minecraft.getInstance();
|
|
||||||
if (mc == null || mc.gameRenderer == null) return;
|
|
||||||
WindowRenderState windowState = mc.gameRenderer.gameRenderState().windowRenderState;
|
|
||||||
if (windowState == null || windowState.guiScale <= 0) return;
|
|
||||||
|
|
||||||
SpatialGuiCapture capture = SpatialGuiCapture.get();
|
SpatialGuiCapture capture = SpatialGuiCapture.get();
|
||||||
RenderTarget target = capture.ensure(windowState.width, windowState.height);
|
if ((Object) this != capture.screenGuiRenderer()) return original;
|
||||||
capture.clearTransparent();
|
RenderTarget redirect = capture.target();
|
||||||
|
if (redirect == null) return original;
|
||||||
guiProjection.setupOrtho(1000.0F, 11000.0F,
|
DebugLog.renderThreadOnce("sg.swap", "[SpatialGui] draw target swapped to capture texture");
|
||||||
(float) windowState.width / windowState.guiScale, (float) windowState.height / windowState.guiScale, true);
|
return redirect;
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,16 +14,30 @@ import com.mojang.blaze3d.pipeline.TextureTarget;
|
||||||
import com.mojang.blaze3d.systems.RenderSystem;
|
import com.mojang.blaze3d.systems.RenderSystem;
|
||||||
import com.mojang.blaze3d.textures.GpuTexture;
|
import com.mojang.blaze3d.textures.GpuTexture;
|
||||||
import com.mojang.blaze3d.textures.GpuTextureView;
|
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 org.joml.Vector4f;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Owns the off-screen {@link RenderTarget} that the currently open {@link net.minecraft.client.gui.screens.Screen}
|
* 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.
|
||||||
* <p>
|
* <p>
|
||||||
* This replaces Spatial-GUI's {@code TextureTargetManager}/{@code ScreenExtractor} pair: on this Minecraft
|
* Follows the reference Spatial-GUI architecture: the screen is extracted into a private
|
||||||
* version the GUI is drawn through a deferred {@code GuiRenderer} that executes its draw list against an
|
* {@link GuiRenderState} served by a second {@link GuiRenderer} instance (see the spatialgui mixins), and that
|
||||||
* explicit {@link RenderTarget}, so capturing the screen only requires swapping that target for the duration
|
* instance's draw pass is redirected into {@link #target()} while the vanilla main GUI pass keeps rendering
|
||||||
* of one draw call (see {@code GuiRendererCaptureMixin}) instead of hooking a legacy immediate-mode GL pass.
|
* 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 {
|
public final class SpatialGuiCapture {
|
||||||
private static final SpatialGuiCapture INSTANCE = new SpatialGuiCapture();
|
private static final SpatialGuiCapture INSTANCE = new SpatialGuiCapture();
|
||||||
|
|
@ -32,6 +46,9 @@ public final class SpatialGuiCapture {
|
||||||
private int width;
|
private int width;
|
||||||
private int height;
|
private int height;
|
||||||
|
|
||||||
|
private GuiRenderState screenRenderState;
|
||||||
|
private GuiRenderer screenGuiRenderer;
|
||||||
|
|
||||||
private SpatialGuiCapture() {
|
private SpatialGuiCapture() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,6 +74,17 @@ public final class SpatialGuiCapture {
|
||||||
return target;
|
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() {
|
public void clearTransparent() {
|
||||||
if (target == null) return;
|
if (target == null) return;
|
||||||
GpuTexture colorTexture = target.getColorTexture();
|
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() {
|
public GpuTextureView colorTextureView() {
|
||||||
return target == null ? null : target.getColorTextureView();
|
return target == null ? null : target.getColorTextureView();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue