Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ repositories {
name = "Fabric"
url = "https://maven.fabricmc.net/"
}
maven {
name = "Modrinth"
url = "https://api.modrinth.com/maven"
content {
includeGroup "maven.modrinth"
}
}
mavenCentral()
}

Expand Down Expand Up @@ -59,6 +66,11 @@ dependencies {
minecraft "com.mojang:minecraft:${project.minecraft_version}"
implementation "net.fabricmc:fabric-loader:${project.loader_version}"
implementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}"

// FirstPerson Model, referenced only by the optional compat bridge for its render-state marker
// interface. Compile-only: the mod is neither bundled nor required at runtime, and the coordinate
// pins the Modrinth version id because the plain version number is shared across loaders.
compileOnly "maven.modrinth:first-person-model:6sgz2HEq"
testImplementation platform("org.junit:junit-bom:5.12.2")
testImplementation "org.junit.jupiter:junit-jupiter"
testRuntimeOnly "org.junit.platform:junit-platform-launcher"
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/dev/comfyfluffy/caustica/CausticaConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,8 @@ public static final class Entities {
intAtLeast("caustica.rt.beBuildsPerFrame", "entities.block-entities.builds-per-frame", 64, 0);
public static final BooleanSetting REFIT_ENABLED =
bool("caustica.rt.entityRefit", "entities.refit.enabled", true);
public static final BooleanSetting FIRST_PERSON_COMPAT_ENABLED =
bool("caustica.rt.firstPersonCompat", "entities.first-person-compat.enabled", false);

private Entities() {
}
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/dev/comfyfluffy/caustica/client/CausticaClient.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.comfyfluffy.caustica.client;

import dev.comfyfluffy.caustica.CausticaMod;
import dev.comfyfluffy.caustica.compat.firstperson.FirstPersonModelBridge;
import dev.comfyfluffy.caustica.rt.RtContext;
import dev.comfyfluffy.caustica.rt.RtDeviceBringup;
import dev.comfyfluffy.caustica.rt.RtComposite;
Expand All @@ -15,14 +16,18 @@
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.rendering.v1.InvalidateRenderStateCallback;
import net.fabricmc.loader.api.FabricLoader;

public final class CausticaClient implements ClientModInitializer {
private static final String FIRST_PERSON_MODEL_MOD_ID = "firstperson";
private static boolean rtInitDone = false;

@Override
public void onInitializeClient() {
CausticaMod.LOGGER.info("Caustica client initialized");

registerFirstPersonModelBridge();

// Class-init runs DebugScreenEntries.register(...) via its ID field; touching the class here
// makes the entry discoverable in F3's entry list. Off by default -- the player opts in the
// same way as any other optional vanilla entry (e.g. GPU utilization).
Expand Down Expand Up @@ -82,6 +87,20 @@ public void onInitializeClient() {
});
}

private static void registerFirstPersonModelBridge() {
// Guarding on the loader keeps the bridge class — and the mod types it links against — untouched
// when the mod is absent, which is the normal case and must stay silent.
if (!FabricLoader.getInstance().isModLoaded(FIRST_PERSON_MODEL_MOD_ID)) {
return;
}
try {
FirstPersonModelBridge.register();
} catch (LinkageError e) {
CausticaMod.LOGGER.warn("FirstPerson Model is installed but its bridge failed to link; "
+ "first-person ray-traced geometry stays disabled", e);
}
}

private static void shutdownRt() {
WorldRenderScaler.INSTANCE.destroy();
RtUiOverlay.destroy(); // GUI redirect is not gated by rtInitDone; always release its TextureTarget
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public static OptionInstance<?>[] runtimeOptions() {
maxBounces(),
entities(),
particles(),
firstPersonCompat(),
waterWaves(),
dlssQuality()
));
Expand Down Expand Up @@ -131,6 +132,11 @@ private static OptionInstance<Boolean> particles() {
return bool("caustica.options.rt.particles", CausticaConfig.Rt.Entities.PARTICLES_ENABLED);
}

private static OptionInstance<Boolean> firstPersonCompat() {
return bool("caustica.options.rt.firstPersonCompat",
CausticaConfig.Rt.Entities.FIRST_PERSON_COMPAT_ENABLED);
}

private static OptionInstance<Boolean> waterWaves() {
return bool("caustica.options.rt.waterWaves", CausticaConfig.Rt.Composite.WATER_WAVES);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package dev.comfyfluffy.caustica.compat.firstperson;

import dev.comfyfluffy.caustica.CausticaMod;
import dev.comfyfluffy.caustica.mixin.LevelRendererAccessor;
import dev.comfyfluffy.caustica.rt.entity.CameraSafetyDeclaration;
import dev.comfyfluffy.caustica.rt.entity.FirstPersonStateProvider;
import dev.comfyfluffy.caustica.rt.entity.FirstPersonStateRegistry;
import dev.tr7zw.firstperson.access.LivingEntityRenderStateAccess;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.LevelRenderer;
import net.minecraft.client.renderer.entity.state.AvatarRenderState;
import net.minecraft.client.renderer.entity.state.EntityRenderState;
import net.minecraft.client.renderer.state.level.LevelRenderState;
import net.minecraft.world.entity.Entity;
import org.jetbrains.annotations.Nullable;

/**
* Supplies Caustica with the first-person body state produced by the FirstPerson Model mod.
*
* <p>The mod appends one extra render state for the camera entity during vanilla's extract phase, taken
* with the entity temporarily displaced by its computed offset, and marks that state — and only that
* state — as the camera entity. Caustica therefore rebuilds no first-person geometry: it picks that
* state up and feeds it through the ordinary capture path, and the offset already baked into
* {@code x/y/z} places the instance correctly.
*
* <p>This class links against the mod, so it must only be touched once the loader has confirmed the mod
* is present. Nothing on the render path references it.
*/
public final class FirstPersonModelBridge implements FirstPersonStateProvider, CameraSafetyDeclaration {
private static final String PROVIDER_ID = "firstperson-model";
private static final int PROVIDER_PRIORITY = 200;

private boolean warnedAmbiguousCandidates;

private FirstPersonModelBridge() {
}

public static void register() {
FirstPersonModelBridge bridge = new FirstPersonModelBridge();
FirstPersonStateRegistry.instance().register(PROVIDER_ID, PROVIDER_PRIORITY, bridge, bridge);
}

@Nullable
@Override
public EntityRenderState provideState(Entity camera, float partialTick) {
LevelRenderer levelRenderer = Minecraft.getInstance().levelRenderer;
if (levelRenderer == null) {
return null;
}
LevelRenderState level = ((LevelRendererAccessor) levelRenderer).caustica$getLevelRenderState();
if (level == null) {
return null;
}

int cameraId = camera.getId();
EntityRenderState found = null;
for (EntityRenderState state : level.entityRenderStates) {
// The mod's marker interface is mixed in at runtime, so the cast goes through the vanilla
// supertype rather than through AvatarRenderState.
if (!(state instanceof AvatarRenderState avatar)
|| avatar.id != cameraId
|| !((LivingEntityRenderStateAccess) state).isCameraEntity()) {
continue;
}
if (found != null) {
// Two marked states for one camera entity contradicts the mod's own invariant; picking
// either by list order would be a guess, so this frame yields nothing.
if (!warnedAmbiguousCandidates) {
warnedAmbiguousCandidates = true;
CausticaMod.LOGGER.warn("FirstPerson Model marked more than one render state for entity {};"
+ " skipping the first-person instance", cameraId);
}
return null;
}
found = state;
}
return found;
}

@Override
public boolean isCameraSafe(Entity camera, EntityRenderState state, float partialTick) {
// The mod hides the head whenever it marks a state as the camera entity, so a marked state never
// encloses the camera origin. Selection already rejected every unmarked state.
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package dev.comfyfluffy.caustica.mixin;

import net.minecraft.client.renderer.LevelRenderer;
import net.minecraft.client.renderer.state.level.LevelRenderState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;

/**
* Exposes the level render state so optional first-person compatibility bridges can read the entity
* render states vanilla extracted this frame. {@code LevelExtractor.extract} clears and repopulates
* {@code entityRenderStates} before the render phase runs, so the list a bridge sees during Caustica's
* capture holds exactly this frame's states.
*/
@Mixin(LevelRenderer.class)
public interface LevelRendererAccessor {
@Accessor("levelRenderState")
LevelRenderState caustica$getLevelRenderState();
}
3 changes: 2 additions & 1 deletion src/main/java/dev/comfyfluffy/caustica/rt/RtFrameStats.java
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ public final class RtFrameStats {
"entityPackedBytes", "entityPackedPaddingBytes", "entityRetainedGeometryBytes",
"entityFrameListsWaits", "entityTableWaits", "entitySlotWaits",
"entityGraphicsWaitNanos", "entityMotionFlushes", "entityTableFlushes",
"entityBlockEntityRetirements", "entitySlotRetirements", "entityTableRetirements"},
"entityBlockEntityRetirements", "entitySlotRetirements", "entityTableRetirements",
"firstPersonInstances"},
true);

private static final List<GarbageCollectorMXBean> GC_BEANS = ManagementFactory.getGarbageCollectorMXBeans();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package dev.comfyfluffy.caustica.rt.entity;

import net.minecraft.client.renderer.entity.state.EntityRenderState;
import net.minecraft.world.entity.Entity;

/**
* Declares whether the provided first-person geometry is camera-safe.
* <p>
* Camera-safe means the geometry will not wrap or intersect the camera origin when rendered.
* This declaration must be made per-frame, as safety depends on dynamic factors like part
* visibility and position offsets.
*/
public interface CameraSafetyDeclaration {
/**
* Returns {@code true} if the first-person geometry is safe to render for primary camera
* rays, {@code false} otherwise.
* <p>
* If this returns {@code false}, throws an exception, or the provider does not implement
* this interface, the first-person instance will not be created.
* <p>
* This is an observational query on the current render frame. Caustica calls it <em>before</em> it
* extracts the camera entity's ordinary body, so an implementation must not mutate the camera entity,
* any world entity, vanilla's render state list, any render state object or its fields, Caustica's
* config, or the provider registry — any such mutation would change the body's extraction result.
*
* @param camera the camera entity
* @param state the first-person render state to evaluate
* @param partialTick sub-tick interpolation fraction
* @return {@code true} if camera-safe, {@code false} otherwise
* @throws Exception if safety cannot be determined
*/
boolean isCameraSafe(Entity camera, EntityRenderState state, float partialTick) throws Exception;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package dev.comfyfluffy.caustica.rt.entity;

import net.minecraft.client.renderer.entity.state.EntityRenderState;
import net.minecraft.world.entity.Entity;
import org.jetbrains.annotations.Nullable;

/**
* Provides the first-person body render state for the camera entity.
* <p>
* Implementations return a pre-extracted {@link EntityRenderState} that was produced by
* vanilla's or a mod's frame extraction. The returned state must be valid for the current
* frame and belong to the camera entity. Caustica does not perform position offsets, part
* hiding, or pose modifications — the provider must return a complete first-person state.
*/
public interface FirstPersonStateProvider {
/**
* Returns the first-person body render state for the camera entity, or {@code null} if
* unavailable this frame.
* <p>
* This is an observational query on the current render frame. Caustica calls it <em>before</em> it
* extracts the camera entity's ordinary body, so an implementation must not mutate the camera entity,
* any world entity, vanilla's render state list, any render state object or its fields, Caustica's
* config, or the provider registry — any such mutation would change the body's extraction result.
*
* @param camera the camera entity (typically the local player)
* @param partialTick sub-tick interpolation fraction
* @return the first-person render state, or {@code null} if not available
* @throws Exception if state extraction fails
*/
@Nullable
EntityRenderState provideState(Entity camera, float partialTick) throws Exception;
}
Loading