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