Plans yet to crumble

- Rename Engine#delete -> #invalidate per pepper's TODO.
- Plans
  - Remove thenMap and andMap from Plan API.
  - Add builder for MapContextPlan for better composition.
  - Add IfElsePlan and Builder to "fork" on a condition.
  - VisualizationManagerImpl no longer rolls a special Plan class and
    instead uses a plan composition chain.
- Crumbling
  - Not implemented yet!! But the skeleton is taking shape.
  - Remove LevelRendererAccessor, and instead directly pass the map of
    destructionProgress to the VisualizationManagerImpl when it's time
    to render crumbling instances.
  - Give Instances a Handle getter.
  - Add way to get a block entity visual at a given position.
  - Add Engine#renderCrumblingInstance stub.
This commit is contained in:
Jozufozu 2023-11-24 20:40:14 -08:00
parent e5cda8944e
commit 055ac161d8
19 changed files with 205 additions and 105 deletions

View file

@ -2,6 +2,7 @@ package com.jozufozu.flywheel.api.backend;
import com.jozufozu.flywheel.api.event.RenderContext;
import com.jozufozu.flywheel.api.event.RenderStage;
import com.jozufozu.flywheel.api.instance.Instance;
import com.jozufozu.flywheel.api.instance.InstancerProvider;
import com.jozufozu.flywheel.api.task.Plan;
import com.jozufozu.flywheel.api.task.TaskExecutor;
@ -14,6 +15,8 @@ public interface Engine extends InstancerProvider {
void renderStage(TaskExecutor executor, RenderContext context, RenderStage stage);
void renderCrumblingInstance(TaskExecutor taskExecutor, RenderContext context, Instance instance, int progress);
/**
* Maintain the render origin to be within a certain distance from the camera in all directions,
* preventing floating point precision issues at high coordinates.
@ -24,7 +27,5 @@ public interface Engine extends InstancerProvider {
Vec3i renderOrigin();
// TODO: "delete" implies that the object cannot be used afterwards, but all current implementations
// support the "invalidate" contract as well, meaning they can be reused after this call. Rename?
void delete();
void invalidate();
}

View file

@ -2,4 +2,6 @@ package com.jozufozu.flywheel.api.instance;
public interface Instance {
InstanceType<?> type();
InstanceHandle handle();
}

View file

@ -33,16 +33,6 @@ public interface Plan<C> {
*/
Plan<C> then(Plan<C> plan);
/**
* Create a new plan that executes this plan, then transforms the context to execute the given plan.
*
* @param map A function that transforms the plan context.
* @param plan The plan to execute after this plan with the transformed context.
* @param <D> The type of the transformed context.
* @return The composed plan.
*/
<D> Plan<C> thenMap(Function<C, D> map, Plan<D> plan);
/**
* Create a new plan that executes this plan and the given plan in parallel.
*
@ -51,16 +41,6 @@ public interface Plan<C> {
*/
Plan<C> and(Plan<C> plan);
/**
* Create a new plan that executes this plan and the given plan in parallel.
*
* @param map A function that transforms the plan context.
* @param plan The plan to execute in parallel with this plan than accepts the transformed context.
* @param <D> The type of the transformed context.
* @return The composed plan.
*/
<D> Plan<C> andMap(Function<C, D> map, Plan<D> plan);
/**
* If possible, create a new plan that accomplishes everything
* this plan does but with a simpler execution schedule.

View file

@ -3,7 +3,7 @@ package com.jozufozu.flywheel.backend.engine;
import com.jozufozu.flywheel.api.instance.InstanceHandle;
public class InstanceHandleImpl implements InstanceHandle {
private final AbstractInstancer<?> instancer;
public final AbstractInstancer<?> instancer;
private int index;
public InstanceHandleImpl(AbstractInstancer<?> instancer, int index) {

View file

@ -93,13 +93,18 @@ public class BatchingEngine extends AbstractEngine implements SimplyComposedPlan
drawTracker.draw(stage);
}
@Override
public void renderCrumblingInstance(TaskExecutor taskExecutor, RenderContext context, Instance instance, int progress) {
// TODO: implement
}
@Override
protected void onRenderOriginChanged() {
initializedInstancers.forEach(BatchedInstancer::clear);
}
@Override
public void delete() {
public void invalidate() {
instancers.clear();
meshPools.values()

View file

@ -68,6 +68,11 @@ public class IndirectEngine extends AbstractEngine {
}
}
@Override
public void renderCrumblingInstance(TaskExecutor taskExecutor, RenderContext context, Instance instance, int progress) {
// TODO: implement
}
private void setup() {
GlTextureUnit.T2.makeActive();
Minecraft.getInstance().gameRenderer.lightTexture().turnOnLightLayer();
@ -85,7 +90,7 @@ public class IndirectEngine extends AbstractEngine {
}
@Override
public void delete() {
public void invalidate() {
drawManager.invalidate();
}
}

View file

@ -75,6 +75,11 @@ public class InstancingEngine extends AbstractEngine {
}
}
@Override
public void renderCrumblingInstance(TaskExecutor taskExecutor, RenderContext context, Instance instance, int progress) {
// TODO: implement
}
private void setup() {
GlTextureUnit.T2.makeActive();
Minecraft.getInstance().gameRenderer.lightTexture().turnOnLightLayer();
@ -97,7 +102,7 @@ public class InstancingEngine extends AbstractEngine {
continue;
}
setup(shader);
setup(shader, context);
shader.material().setup();
@ -109,7 +114,7 @@ public class InstancingEngine extends AbstractEngine {
}
}
private void setup(ShaderState desc) {
private void setup(ShaderState desc, Context context) {
var material = desc.material();
var vertexType = desc.vertexType();
var instanceType = desc.instanceType();
@ -130,7 +135,7 @@ public class InstancingEngine extends AbstractEngine {
}
@Override
public void delete() {
public void invalidate() {
drawManager.invalidate();
}
}

View file

@ -10,7 +10,7 @@ import net.minecraft.core.Vec3i;
public record FrameContext(double cameraX, double cameraY, double cameraZ, FrustumIntersection frustum, float partialTick) {
@NotNull
public static FrameContext create(RenderContext context, Vec3i renderOrigin, float partialTick) {
public static FrameContext create(RenderContext context, Vec3i renderOrigin) {
var cameraPos = context.camera()
.getPosition();
double cameraX = cameraPos.x;
@ -21,6 +21,6 @@ public record FrameContext(double cameraX, double cameraY, double cameraZ, Frust
viewProjection.translate((float) (renderOrigin.getX() - cameraX), (float) (renderOrigin.getY() - cameraY), (float) (renderOrigin.getZ() - cameraZ));
FrustumIntersection frustum = new FrustumIntersection(viewProjection);
return new FrameContext(cameraX, cameraY, cameraZ, frustum, partialTick);
return new FrameContext(cameraX, cameraY, cameraZ, frustum, context.partialTick());
}
}

View file

@ -1,11 +1,14 @@
package com.jozufozu.flywheel.impl.visualization;
import java.util.SortedSet;
import org.jetbrains.annotations.Nullable;
import com.jozufozu.flywheel.api.backend.BackendManager;
import com.jozufozu.flywheel.api.backend.Engine;
import com.jozufozu.flywheel.api.event.RenderContext;
import com.jozufozu.flywheel.api.event.RenderStage;
import com.jozufozu.flywheel.api.instance.Instance;
import com.jozufozu.flywheel.api.task.Plan;
import com.jozufozu.flywheel.api.task.TaskExecutor;
import com.jozufozu.flywheel.api.visual.DynamicVisual;
@ -20,15 +23,17 @@ import com.jozufozu.flywheel.impl.visualization.manager.BlockEntityVisualManager
import com.jozufozu.flywheel.impl.visualization.manager.EffectVisualManager;
import com.jozufozu.flywheel.impl.visualization.manager.EntityVisualManager;
import com.jozufozu.flywheel.lib.task.Flag;
import com.jozufozu.flywheel.lib.task.MapContextPlan;
import com.jozufozu.flywheel.lib.task.NamedFlag;
import com.jozufozu.flywheel.lib.task.NestedPlan;
import com.jozufozu.flywheel.lib.task.RaisePlan;
import com.jozufozu.flywheel.lib.task.SimplyComposedPlan;
import com.jozufozu.flywheel.lib.task.IfElsePlan;
import com.jozufozu.flywheel.lib.util.LevelAttached;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.core.Vec3i;
import net.minecraft.server.level.BlockDestructionProgress;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.LevelAccessor;
import net.minecraft.world.level.block.entity.BlockEntity;
@ -50,7 +55,8 @@ public class VisualizationManagerImpl implements VisualizationManager {
private final Plan<RenderContext> framePlan;
private final Flag tickFlag = new NamedFlag("tick");
private final Flag frameFlag = new NamedFlag("frame");
private final Flag frameVisualsFlag = new NamedFlag("frameVisualUpdates");
private final Flag frameFlag = new NamedFlag("frameComplete");
private VisualizationManagerImpl(LevelAccessor level) {
engine = BackendManager.getBackend()
@ -66,7 +72,21 @@ public class VisualizationManagerImpl implements VisualizationManager {
.and(effects.createTickPlan())
.then(RaisePlan.raise(tickFlag))
.simplify();
framePlan = new FramePlan().then(RaisePlan.raise(frameFlag));
framePlan = IfElsePlan.on((RenderContext ctx) -> engine.updateRenderOrigin(ctx.camera()))
.ifTrue(MapContextPlan.map(RenderContext::partialTick)
.to(blockEntities.createRecreationPlan()
.and(entities.createRecreationPlan())
.and(effects.createRecreationPlan())))
.ifFalse(MapContextPlan.map((RenderContext ctx) -> FrameContext.create(ctx, engine.renderOrigin()))
.to(blockEntities.createFramePlan()
.and(entities.createFramePlan())
.and(effects.createFramePlan())))
.plan()
.then(RaisePlan.raise(frameVisualsFlag))
.then(engine.createFramePlan())
.then(RaisePlan.raise(frameFlag))
.simplify();
}
public static boolean supportsVisualization(@Nullable LevelAccessor level) {
@ -171,6 +191,8 @@ public class VisualizationManagerImpl implements VisualizationManager {
// Note we don't lower here because many frames may happen per tick.
taskExecutor.syncUntil(tickFlag::isRaised);
frameVisualsFlag.lower();
frameFlag.lower();
framePlan.execute(taskExecutor, context);
}
@ -181,6 +203,41 @@ public class VisualizationManagerImpl implements VisualizationManager {
engine.renderStage(taskExecutor, context, stage);
}
public void renderCrumbling(RenderContext context, Long2ObjectMap<SortedSet<BlockDestructionProgress>> destructionProgress) {
taskExecutor.syncUntil(frameVisualsFlag::isRaised);
for (var entry : destructionProgress.long2ObjectEntrySet()) {
var set = entry.getValue();
if (set == null || set.isEmpty()) {
// Nothing to do if there's no crumbling.
continue;
}
var visual = blockEntities.visualAtPos(entry.getLongKey());
if (visual == null) {
// The block doesn't have a visual, this is probably the common case.
continue;
}
var instanceList = visual.getCrumblingInstances();
if (instanceList.isEmpty()) {
// The visual doesn't want to render anything crumbling.
continue;
}
// now for the fun part
int progress = set.last()
.getProgress();
for (Instance instance : instanceList) {
engine.renderCrumblingInstance(taskExecutor, context, instance, progress);
}
}
}
/**
* Free all acquired resources and delete this manager.
*/
@ -192,29 +249,6 @@ public class VisualizationManagerImpl implements VisualizationManager {
blockEntities.invalidate();
entities.invalidate();
effects.invalidate();
engine.delete();
}
private class FramePlan implements SimplyComposedPlan<RenderContext> {
private final Plan<Float> recreationPlan = NestedPlan.of(blockEntities.createRecreationPlan(), entities.createRecreationPlan(), effects.createRecreationPlan());
private final Plan<FrameContext> normalPlan = blockEntities.createFramePlan()
.and(entities.createFramePlan())
.and(effects.createFramePlan());
private final Plan<RenderContext> enginePlan = engine.createFramePlan();
@Override
public void execute(TaskExecutor taskExecutor, RenderContext context, Runnable onCompletion) {
Runnable then = () -> enginePlan.execute(taskExecutor, context, onCompletion);
float partialTick = context.partialTick();
if (engine.updateRenderOrigin(context.camera())) {
recreationPlan.execute(taskExecutor, partialTick, then);
} else {
var frameContext = FrameContext.create(context, engine.renderOrigin(), partialTick);
normalPlan.execute(taskExecutor, frameContext, then);
}
}
engine.invalidate();
}
}

View file

@ -15,6 +15,7 @@ import com.jozufozu.flywheel.impl.visualization.ratelimit.DistanceUpdateLimiterI
import com.jozufozu.flywheel.impl.visualization.ratelimit.NonLimiter;
import com.jozufozu.flywheel.impl.visualization.storage.Storage;
import com.jozufozu.flywheel.impl.visualization.storage.Transaction;
import com.jozufozu.flywheel.lib.task.MapContextPlan;
import com.jozufozu.flywheel.lib.task.SimplePlan;
public abstract class AbstractVisualManager<T> implements VisualManager<T> {
@ -88,7 +89,8 @@ public abstract class AbstractVisualManager<T> implements VisualManager<T> {
tickLimiter.tick();
processQueue(0);
})
.thenMap(this::createVisualTickContext, getStorage().getTickPlan());
.then(MapContextPlan.map(this::createVisualTickContext)
.to(getStorage().getTickPlan()));
}
public Plan<FrameContext> createFramePlan() {
@ -96,7 +98,8 @@ public abstract class AbstractVisualManager<T> implements VisualManager<T> {
frameLimiter.tick();
processQueue(context.partialTick());
})
.thenMap(this::createVisualContext, getStorage().getFramePlan());
.then(MapContextPlan.map(this::createVisualContext)
.to(getStorage().getFramePlan()));
}
private VisualFrameContext createVisualContext(FrameContext ctx) {

View file

@ -1,7 +1,5 @@
package com.jozufozu.flywheel.impl.visualization.manager;
import java.util.List;
import org.jetbrains.annotations.Nullable;
import com.jozufozu.flywheel.api.backend.Engine;
@ -30,11 +28,8 @@ public class BlockEntityVisualManager extends AbstractVisualManager<BlockEntity>
return storage;
}
public void getCrumblingVisuals(long pos, List<BlockEntityVisual<?>> visuals) {
BlockEntityVisual<?> visual = storage.posLookup.get(pos);
if (visual != null) {
visuals.add(visual);
}
public BlockEntityVisual<?> visualAtPos(long pos) {
return storage.posLookup.get(pos);
}
private static class BlockEntityStorage extends Storage<BlockEntity> {

View file

@ -18,6 +18,11 @@ public abstract class AbstractInstance implements Instance {
return type;
}
@Override
public InstanceHandle handle() {
return handle;
}
public final void setChanged() {
handle.setChanged();
}

View file

@ -0,0 +1,65 @@
package com.jozufozu.flywheel.lib.task;
import java.util.function.Predicate;
import com.jozufozu.flywheel.api.task.Plan;
import com.jozufozu.flywheel.api.task.TaskExecutor;
/**
* Executes one plan or another, depending on a dynamically evaluated condition.
* @param condition The condition to branch on.
* @param onTrue The plan to execute if the condition is true.
* @param onFalse The plan to execute if the condition is false.
* @param <C> The type of the context object.
*/
public record IfElsePlan<C>(Predicate<C> condition, Plan<C> onTrue, Plan<C> onFalse) implements SimplyComposedPlan<C> {
public static <C> Builder<C> on(Predicate<C> condition) {
return new Builder<>(condition);
}
@Override
public void execute(TaskExecutor taskExecutor, C context, Runnable onCompletion) {
if (condition.test(context)) {
onTrue.execute(taskExecutor, context, onCompletion);
} else {
onFalse.execute(taskExecutor, context, onCompletion);
}
}
@Override
public Plan<C> simplify() {
var maybeSimplifiedTrue = onTrue.simplify();
var maybeSimplifiedFalse = onFalse.simplify();
if (maybeSimplifiedTrue instanceof UnitPlan && maybeSimplifiedFalse instanceof UnitPlan) {
// The condition may have side effects that still need to be evaluated.
return SimplePlan.of(condition::test);
}
return new IfElsePlan<>(condition, maybeSimplifiedTrue, maybeSimplifiedFalse);
}
public static class Builder<C> {
private final Predicate<C> condition;
private Plan<C> onTrue = UnitPlan.of();
private Plan<C> onFalse = UnitPlan.of();
public Builder(Predicate<C> condition) {
this.condition = condition;
}
public Builder<C> ifTrue(Plan<C> onTrue) {
this.onTrue = onTrue;
return this;
}
public Builder<C> ifFalse(Plan<C> onFalse) {
this.onFalse = onFalse;
return this;
}
public IfElsePlan<C> plan() {
return new IfElsePlan<>(condition, onTrue, onFalse);
}
}
}

View file

@ -6,6 +6,10 @@ import com.jozufozu.flywheel.api.task.Plan;
import com.jozufozu.flywheel.api.task.TaskExecutor;
public record MapContextPlan<C, D>(Function<C, D> map, Plan<D> plan) implements SimplyComposedPlan<C> {
public static <C, D> Builder<C, D> map(Function<C, D> map) {
return new Builder<>(map);
}
@Override
public void execute(TaskExecutor taskExecutor, C context, Runnable onCompletion) {
D newContext = map.apply(context);
@ -22,4 +26,20 @@ public record MapContextPlan<C, D>(Function<C, D> map, Plan<D> plan) implements
return new MapContextPlan<>(map, maybeSimplified);
}
public static class Builder<C, D> {
private final Function<C, D> map;
public Builder(Function<C, D> map) {
this.map = map;
}
public MapContextPlan<C, D> to(Plan<D> plan) {
return new MapContextPlan<>(map, plan);
}
public MapContextPlan<C, D> plan() {
return new MapContextPlan<>(map, UnitPlan.of());
}
}
}

View file

@ -10,21 +10,11 @@ public interface SimplyComposedPlan<C> extends Plan<C> {
return new BarrierPlan<>(this, plan);
}
@Override
default <D> Plan<C> thenMap(Function<C, D> map, Plan<D> plan) {
return then(new MapContextPlan<>(map, plan));
}
@Override
default Plan<C> and(Plan<C> plan) {
return NestedPlan.of(this, plan);
}
@Override
default <D> Plan<C> andMap(Function<C, D> map, Plan<D> plan) {
return and(new MapContextPlan<>(map, plan));
}
@Override
default Plan<C> simplify() {
return this;

View file

@ -26,21 +26,11 @@ public class UnitPlan<C> implements Plan<C> {
return plan;
}
@Override
public <D> Plan<C> thenMap(Function<C, D> map, Plan<D> plan) {
return new MapContextPlan<>(map, plan);
}
@Override
public Plan<C> and(Plan<C> plan) {
return plan;
}
@Override
public <D> Plan<C> andMap(Function<C, D> map, Plan<D> plan) {
return new MapContextPlan<>(map, plan);
}
@Override
public Plan<C> simplify() {
return this;

View file

@ -1,16 +0,0 @@
package com.jozufozu.flywheel.mixin;
import java.util.SortedSet;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import net.minecraft.client.renderer.LevelRenderer;
import net.minecraft.server.level.BlockDestructionProgress;
@Mixin(LevelRenderer.class)
public interface LevelRendererAccessor {
@Accessor("destructionProgress")
Long2ObjectMap<SortedSet<BlockDestructionProgress>> flywheel$getDestructionProgress();
}

View file

@ -1,5 +1,7 @@
package com.jozufozu.flywheel.mixin;
import java.util.SortedSet;
import org.joml.Matrix4f;
import org.objectweb.asm.Opcodes;
import org.spongepowered.asm.mixin.Final;
@ -16,14 +18,17 @@ import com.jozufozu.flywheel.api.event.ReloadRenderersEvent;
import com.jozufozu.flywheel.api.event.RenderContext;
import com.jozufozu.flywheel.api.event.RenderStage;
import com.jozufozu.flywheel.api.event.RenderStageEvent;
import com.jozufozu.flywheel.impl.visualization.VisualizationManagerImpl;
import com.mojang.blaze3d.vertex.PoseStack;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import net.minecraft.client.Camera;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.GameRenderer;
import net.minecraft.client.renderer.LevelRenderer;
import net.minecraft.client.renderer.LightTexture;
import net.minecraft.client.renderer.RenderBuffers;
import net.minecraft.server.level.BlockDestructionProgress;
import net.minecraftforge.common.MinecraftForge;
@Mixin(value = LevelRenderer.class, priority = 1001) // Higher priority to go after Sodium
@ -35,6 +40,10 @@ public class LevelRendererMixin {
@Final
private RenderBuffers renderBuffers;
@Shadow
@Final
private Long2ObjectMap<SortedSet<BlockDestructionProgress>> destructionProgress;
@Unique
private RenderContext flywheel$renderContext;
@ -142,4 +151,12 @@ public class LevelRendererMixin {
private void flywheel$onStage$afterWeather(CallbackInfo ci) {
flywheel$dispatch(RenderStage.AFTER_WEATHER);
}
@Inject(method = "renderLevel", at = @At(value = "INVOKE_STRING", target = "Lnet/minecraft/util/profiling/ProfilerFiller;popPush(Ljava/lang/String;)V", args = "ldc=destroyProgress"))
private void flywheel$crumbling(CallbackInfo ci) {
var vm = VisualizationManagerImpl.get(level);
if (vm != null) {
vm.renderCrumbling(flywheel$renderContext, destructionProgress);
}
}
}

View file

@ -11,7 +11,6 @@
"EntityTypeMixin",
"FogUpdateMixin",
"GlStateManagerMixin",
"LevelRendererAccessor",
"LevelRendererMixin",
"LightUpdateMixin",
"RenderTypeMixin",