diff --git a/src/main/java/com/ejclaw/videoplayer/client/playback/VideoPlayback.java b/src/main/java/com/ejclaw/videoplayer/client/playback/VideoPlayback.java
index a61e991..aca892a 100644
--- a/src/main/java/com/ejclaw/videoplayer/client/playback/VideoPlayback.java
+++ b/src/main/java/com/ejclaw/videoplayer/client/playback/VideoPlayback.java
@@ -1,64 +1,110 @@
package com.ejclaw.videoplayer.client.playback;
+import com.ejclaw.videoplayer.VideoPlayerMod;
import com.ejclaw.videoplayer.block.VideoAnchorBlockEntity;
+import com.mojang.blaze3d.platform.NativeImage;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.Identifier;
+import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.Map;
import java.util.Set;
/**
- * SPEC §5 — per-anchor playback registry.
+ * SPEC §5 — per-anchor playback registry. Maps {@link BlockPos} → ({@link VideoBackend} +
+ * a {@link DynamicTexture} surface registered under a unique {@link Identifier}). The renderer
+ * reads {@link #currentTexture(BlockPos)} and binds it to the quad. {@link #tick()} pumps
+ * decoded RGBA frames into the texture.
*
- *
Status (26.1.2 port): the rendering pipeline was rewritten upstream
- * (render-state separation, removal of {@code DrawContext}/{@code GuiGraphics}).
- * Until the new {@code BlockEntityRenderer} pipeline is wired through with a
- * dynamic texture surface, this class operates as a stub: it tracks active anchors
- * but does not decode frames or upload textures. Audio backends are also paused
- * pending a Java 25 dependency audit (Watermedia / JavaCV).
+ * When no backend is available on the classpath (e.g. JavaCV jar not installed by the
+ * user), the texture stays at its initial placeholder pattern, so the anchor's screen quad
+ * still renders as a visible surface.
*/
@Environment(EnvType.CLIENT)
public final class VideoPlayback {
private VideoPlayback() {}
- private static final Map ENTRIES = new HashMap<>();
+ private static final int PLACEHOLDER_SIZE = 32;
+ private static final Map ENTRIES = new HashMap<>();
+ /**
+ * Ensure a playback entry exists for this anchor and return its texture identifier.
+ * Creates a backend + dynamic texture on first call. Returns {@code null} only if the
+ * URL is empty or autoplay is off.
+ */
public static Identifier getOrStart(VideoAnchorBlockEntity be) {
BlockPos pos = be.getBlockPos();
- String url = be.getUrl();
- if (url == null || url.isEmpty() || !be.isAutoplay()) {
- ENTRIES.remove(pos);
+ Entry e = ENTRIES.get(pos);
+
+ if (be.getUrl().isEmpty() || !be.isAutoplay()) {
+ if (e != null) {
+ stop(pos);
+ }
return null;
}
- ENTRIES.put(pos, url);
- return null; // no dynamic texture yet in 26.1.2 port
+
+ if (e != null && e.url.equals(be.getUrl())) {
+ return e.id;
+ }
+ if (e != null) {
+ stop(pos);
+ }
+
+ VideoBackend backend = WatermediaProbe.isAvailable() ? new WatermediaBackend() : new JavaCvBackend();
+ backend.play(be.getUrl(), be.isLoop());
+ backend.setVolume(be.isMuted() ? 0F : be.getVolume());
+
+ Entry created = new Entry(be.getUrl(), backend);
+ ENTRIES.put(pos, created);
+ return created.id;
}
public static Identifier currentTexture(BlockPos pos) {
- return null;
+ Entry e = ENTRIES.get(pos);
+ return e == null ? null : e.id;
}
public static void stop(BlockPos pos) {
- ENTRIES.remove(pos);
+ Entry e = ENTRIES.remove(pos);
+ if (e != null) e.close();
}
public static void onConfigChanged(VideoAnchorBlockEntity be) {
if (be == null) return;
- BlockPos pos = be.getBlockPos();
- String url = be.getUrl();
- if (url == null || url.isEmpty()) {
- ENTRIES.remove(pos);
- } else {
- ENTRIES.put(pos, url);
+ Entry e = ENTRIES.get(be.getBlockPos());
+ if (e == null) return;
+ if (!e.url.equals(be.getUrl())) {
+ stop(be.getBlockPos());
+ return;
}
+ e.backend.setVolume(be.isMuted() ? 0F : be.getVolume());
}
+ /** Called every client tick to upload new frames into the GPU texture. */
public static void tick() {
- // stub: no frames to pump
+ if (Minecraft.getInstance() == null) return;
+ Iterator> it = ENTRIES.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry me = it.next();
+ Entry e = me.getValue();
+ if (!e.backend.isReady()) continue;
+ ByteBuffer buf = e.backend.pollFrame();
+ if (buf == null) continue;
+ try {
+ e.upload(buf);
+ } catch (Throwable t) {
+ VideoPlayerMod.LOG.warn("[{}] texture upload failed: {}", VideoPlayerMod.MOD_ID, t.toString());
+ e.close();
+ it.remove();
+ }
+ }
}
public static Set activePositions() {
@@ -66,10 +112,92 @@ public final class VideoPlayback {
}
public static void setGain(BlockPos pos, float gain) {
- // stub
+ Entry e = ENTRIES.get(pos);
+ if (e != null) e.backend.setVolume(gain);
}
public static void stopAll() {
+ for (Entry e : ENTRIES.values()) e.close();
ENTRIES.clear();
}
+
+ /** Per-anchor playback state. */
+ private static final class Entry {
+ final String url;
+ final VideoBackend backend;
+ final Identifier id;
+ DynamicTexture texture;
+ int texW = 0, texH = 0;
+ boolean registered = false;
+
+ Entry(String url, VideoBackend backend) {
+ this.url = url;
+ this.backend = backend;
+ String tag = Integer.toHexString(System.identityHashCode(this));
+ this.id = Identifier.fromNamespaceAndPath(VideoPlayerMod.MOD_ID, "dynamic/" + tag);
+ ensureTexture(PLACEHOLDER_SIZE, PLACEHOLDER_SIZE, true);
+ }
+
+ /** Allocate or resize the dynamic texture, registering it on first allocation. */
+ private void ensureTexture(int w, int h, boolean fillPlaceholder) {
+ if (texture != null && w == texW && h == texH) return;
+ if (texture != null) texture.close();
+ NativeImage img = new NativeImage(w, h, false);
+ if (fillPlaceholder) {
+ fillPlaceholder(img, w, h);
+ }
+ texture = new DynamicTexture(() -> "video_player:" + id, img);
+ texW = w;
+ texH = h;
+ Minecraft mc = Minecraft.getInstance();
+ if (mc != null) {
+ mc.getTextureManager().register(id, texture);
+ registered = true;
+ }
+ texture.upload();
+ }
+
+ /** Dark gray surface with a thin border — visible "screen" until first frame arrives. */
+ private static void fillPlaceholder(NativeImage img, int w, int h) {
+ // ABGR int. 0xAABBGGRR.
+ int body = 0xFF202020; // dark gray
+ int border = 0xFF505050;
+ for (int y = 0; y < h; y++) {
+ for (int x = 0; x < w; x++) {
+ boolean isEdge = (x == 0 || y == 0 || x == w - 1 || y == h - 1);
+ img.setPixelABGR(x, y, isEdge ? border : body);
+ }
+ }
+ }
+
+ /** Copy an incoming RGBA byte buffer into the texture, resizing if dimensions changed. */
+ void upload(ByteBuffer rgba) {
+ int w = backend.videoWidth();
+ int h = backend.videoHeight();
+ if (w <= 0 || h <= 0) return;
+ ensureTexture(w, h, false);
+ NativeImage img = texture.getPixels();
+ if (img == null) return;
+
+ int pixels = w * h;
+ for (int i = 0; i < pixels; i++) {
+ int r = rgba.get() & 0xFF;
+ int g = rgba.get() & 0xFF;
+ int b = rgba.get() & 0xFF;
+ int a = rgba.get() & 0xFF;
+ int abgr = (a << 24) | (b << 16) | (g << 8) | r;
+ img.setPixelABGR(i % w, i / w, abgr);
+ }
+ texture.upload();
+ }
+
+ void close() {
+ try { backend.close(); } catch (Throwable ignored) {}
+ if (texture != null) {
+ try { texture.close(); } catch (Throwable ignored) {}
+ texture = null;
+ }
+ // texture manager keeps the registration; the texture itself is closed.
+ }
+ }
}
diff --git a/src/main/java/com/ejclaw/videoplayer/client/render/VideoAnchorRenderer.java b/src/main/java/com/ejclaw/videoplayer/client/render/VideoAnchorRenderer.java
index 30f2a9e..49fedb1 100644
--- a/src/main/java/com/ejclaw/videoplayer/client/render/VideoAnchorRenderer.java
+++ b/src/main/java/com/ejclaw/videoplayer/client/render/VideoAnchorRenderer.java
@@ -3,6 +3,7 @@ package com.ejclaw.videoplayer.client.render;
import com.ejclaw.videoplayer.block.VideoAnchorBlockEntity;
import com.ejclaw.videoplayer.client.playback.VideoPlayback;
import com.mojang.blaze3d.vertex.PoseStack;
+import com.mojang.math.Axis;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.renderer.SubmitNodeCollector;
@@ -10,24 +11,26 @@ import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState;
import net.minecraft.client.renderer.feature.ModelFeatureRenderer;
+import net.minecraft.client.renderer.rendertype.RenderType;
+import net.minecraft.client.renderer.rendertype.RenderTypes;
import net.minecraft.client.renderer.state.level.CameraRenderState;
+import net.minecraft.core.Direction;
+import net.minecraft.resources.Identifier;
import net.minecraft.world.phys.Vec3;
+import org.joml.Matrix4f;
/**
- * SPEC §5.2 — anchor renderer.
+ * SPEC §5.2 — submits a width×height textured quad in front of the anchor, oriented by facing.
*
- * Status (26.1.2 port): the rendering pipeline was rewritten upstream to use
- * a render-state separation pattern ({@code createRenderState} / {@code extractRenderState}
- * / {@code submit} via {@link SubmitNodeCollector}). This stub implements the new interface
- * but draws nothing — the anchor block itself is the only visible element. Frame upload
- * and quad submission will be re-introduced once the dynamic texture surface is wired up
- * (see {@link VideoPlayback}).
+ *
Ported to 26.1.2's render-state pipeline: per-frame BE state is captured in
+ * {@link State} via {@link #extractRenderState}, then drawn via
+ * {@link SubmitNodeCollector#submitCustomGeometry} during {@link #submit}.
*/
@Environment(EnvType.CLIENT)
public class VideoAnchorRenderer implements BlockEntityRenderer {
public VideoAnchorRenderer(BlockEntityRendererProvider.Context ctx) {
- // no-op — context retained for future texture/font lookups
+ // no-op
}
@Override
@@ -39,16 +42,57 @@ public class VideoAnchorRenderer implements BlockEntityRenderer {
+ // Front face (visible from the direction the anchor faces)
+ emit(vc, mat, 0F, 0F, 0F, 0F, 1F, light);
+ emit(vc, mat, w, 0F, 0F, 1F, 1F, light);
+ emit(vc, mat, w, h, 0F, 1F, 0F, light);
+ emit(vc, mat, 0F, h, 0F, 0F, 0F, light);
+ // Back face (visible from behind)
+ emit(vc, mat, 0F, h, 0F, 0F, 0F, light);
+ emit(vc, mat, w, h, 0F, 1F, 0F, light);
+ emit(vc, mat, w, 0F, 0F, 1F, 1F, light);
+ emit(vc, mat, 0F, 0F, 0F, 0F, 1F, light);
+ });
+
+ pose.popPose();
+ }
+
+ private static void emit(com.mojang.blaze3d.vertex.VertexConsumer vc, Matrix4f mat,
+ float x, float y, float z, float u, float v, int light) {
+ vc.addVertex(mat, x, y, z)
+ .setColor(255, 255, 255, 255)
+ .setUv(u, v)
+ .setOverlay(net.minecraft.client.renderer.texture.OverlayTexture.NO_OVERLAY)
+ .setLight(light)
+ .setNormal(0F, 0F, 1F);
}
@Override
@@ -61,10 +105,11 @@ public class VideoAnchorRenderer implements BlockEntityRenderer