v0.4.7: smoother playback via memcpy upload + render-rate pump
Some checks failed
build / build (push) Has been cancelled

- Replace per-pixel RGBA->ABGR loop in Entry.upload() with a single
  MemoryUtil.memCopy() into NativeImage's native buffer. The two layouts
  are identical when viewed as little-endian bytes, so no swap is needed.
  Cuts 1080p upload time from a ~2M-iter Java loop to one native memcpy.
- Move the frame-pump tick from 20Hz client tick (END_CLIENT_TICK) to
  per-render-frame (LevelRenderEvents.START_MAIN). At 60+fps display vs
  24fps source, this removes the worst stutter window where a decoded
  frame waited up to 50ms for the next tick. Distance-gain math stays on
  20Hz where it's plenty.
- Bump version 0.4.6 -> 0.4.7 in gradle.properties and README.
This commit is contained in:
tkrmagid
2026-05-15 22:06:15 +09:00
parent d34dc97671
commit 7b7fd7f320
4 changed files with 23 additions and 18 deletions

View File

@@ -3,7 +3,7 @@
마인크래프트 안에서 임의의 동영상 URL을 벽·바닥·천장에 평면으로 재생하는 Fabric 모드. 마인크래프트 안에서 임의의 동영상 URL을 벽·바닥·천장에 평면으로 재생하는 Fabric 모드.
- 모드 ID: `video_player` - 모드 ID: `video_player`
- 현재 버전: **0.4.6** - 현재 버전: **0.4.7**
- 마인크래프트 버전: **26.1.2** - 마인크래프트 버전: **26.1.2**
- 필요 Java: **25** (마인크래프트 26.x 가 요구함) - 필요 Java: **25** (마인크래프트 26.x 가 요구함)
@@ -53,7 +53,7 @@ Fabric은 마인크래프트에 모드 기능을 추가해 주는 로더입니
- 받은 `fabric-api-0.149.0+26.1.2.jar``mods` 폴더에 넣습니다. - 받은 `fabric-api-0.149.0+26.1.2.jar``mods` 폴더에 넣습니다.
2. **video_player** (이 모드) 2. **video_player** (이 모드)
- 다운로드: https://git.tkrmagid.kr/tkrmagid/mc_video_player_mod/releases - 다운로드: https://git.tkrmagid.kr/tkrmagid/mc_video_player_mod/releases
- `video_player-0.4.6.jar` 를 다운로드해서 같은 `mods` 폴더에 넣습니다. - `video_player-0.4.7.jar` 를 다운로드해서 같은 `mods` 폴더에 넣습니다.
이전 버전(`video_player-0.4.0.jar`, `0.4.2.jar`, `0.4.3.jar`, `0.3.x.jar` 등)이 mods 폴더에 남아있다면 **반드시 삭제**하세요. 두 개가 같이 있으면 마인크래프트가 충돌로 켜지지 않습니다. 이전 버전(`video_player-0.4.0.jar`, `0.4.2.jar`, `0.4.3.jar`, `0.3.x.jar` 등)이 mods 폴더에 남아있다면 **반드시 삭제**하세요. 두 개가 같이 있으면 마인크래프트가 충돌로 켜지지 않습니다.
@@ -135,7 +135,7 @@ Fabric은 마인크래프트에 모드 기능을 추가해 주는 로더입니
게임 안에서 채팅창에 `/videostick` 을 입력하세요. 정상이라면: 게임 안에서 채팅창에 `/videostick` 을 입력하세요. 정상이라면:
- 인벤토리에 **비디오 스틱** 아이템이 들어옵니다 (보라/검정 missing-texture 가 아니라 작대기 모양 아이콘). - 인벤토리에 **비디오 스틱** 아이템이 들어옵니다 (보라/검정 missing-texture 가 아니라 작대기 모양 아이콘).
- 보라/검정 missing texture 가 나오면 **STEP 4** 에서 이전 버전 jar(`video_player-0.4.0.jar` / `0.4.1.jar` 등)가 mods 폴더에 같이 남아있는 경우입니다. 다 지우고 `0.4.6` 만 남기고 다시 시작하세요. (0.4.1 이하는 Fabric 26.1.2 model 로더가 unprefixed `item/generated` parent 를 거부해서 스틱 아이콘이 missing-model 큐브로 보입니다 — 0.4.2 에서 수정됨.) - 보라/검정 missing texture 가 나오면 **STEP 4** 에서 이전 버전 jar(`video_player-0.4.0.jar` / `0.4.1.jar` 등)가 mods 폴더에 같이 남아있는 경우입니다. 다 지우고 `0.4.7` 만 남기고 다시 시작하세요. (0.4.1 이하는 Fabric 26.1.2 model 로더가 unprefixed `item/generated` parent 를 거부해서 스틱 아이콘이 missing-model 큐브로 보입니다 — 0.4.2 에서 수정됨.)
--- ---
@@ -240,7 +240,7 @@ Fabric은 마인크래프트에 모드 기능을 추가해 주는 로더입니
JAVA_HOME=/usr/lib/jvm/java-25-openjdk-amd64 ./gradlew build JAVA_HOME=/usr/lib/jvm/java-25-openjdk-amd64 ./gradlew build
``` ```
산출물: `build/libs/video_player-0.4.6.jar` 산출물: `build/libs/video_player-0.4.7.jar`
JavaCV를 직접 의존성으로 가져오는 경우의 Maven 좌표: JavaCV를 직접 의존성으로 가져오는 경우의 Maven 좌표:
``` ```

View File

@@ -5,7 +5,7 @@ org.gradle.configuration-cache=false
# Mod # Mod
mod_id=video_player mod_id=video_player
mod_version=0.4.6 mod_version=0.4.7
maven_group=com.ejclaw.videoplayer maven_group=com.ejclaw.videoplayer
archives_base_name=video_player archives_base_name=video_player

View File

@@ -14,6 +14,7 @@ import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientBlockEntityEvents
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.fabricmc.fabric.api.client.rendering.v1.BlockEntityRendererRegistry; import net.fabricmc.fabric.api.client.rendering.v1.BlockEntityRendererRegistry;
import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderEvents;
import net.fabricmc.fabric.api.event.player.AttackBlockCallback; import net.fabricmc.fabric.api.event.player.AttackBlockCallback;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.player.LocalPlayer; import net.minecraft.client.player.LocalPlayer;
@@ -54,10 +55,15 @@ public class VideoPlayerClient implements ClientModInitializer {
return InteractionResult.PASS; return InteractionResult.PASS;
}); });
ClientTickEvents.END_CLIENT_TICK.register(client -> { // Pump frame uploads on every render frame (60+Hz) rather than every client tick
VideoPlayback.tick(); // (20Hz). At 24fps source, a 20Hz pump can skip a frame whenever a tick window happens
updateDistanceGains(client); // to miss the decode boundary, producing visible micro-stutter even when frames are
}); // ready. Polling at render rate lets the texture latch as soon as a frame is decoded.
LevelRenderEvents.START_MAIN.register(ctx -> VideoPlayback.tick());
// Distance-gain math is cheap and only audible state — 20Hz is plenty and avoids
// recomputing it on every render frame.
ClientTickEvents.END_CLIENT_TICK.register(VideoPlayerClient::updateDistanceGains);
ClientTickEvents.END_LEVEL_TICK.register(world -> { ClientTickEvents.END_LEVEL_TICK.register(world -> {
// no-op for now // no-op for now

View File

@@ -9,6 +9,7 @@ import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.texture.DynamicTexture; import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.resources.Identifier; import net.minecraft.resources.Identifier;
import org.lwjgl.system.MemoryUtil;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.file.Path; import java.nio.file.Path;
@@ -196,15 +197,13 @@ public final class VideoPlayback {
NativeImage img = texture.getPixels(); NativeImage img = texture.getPixels();
if (img == null) return; if (img == null) return;
int pixels = w * h; // RGBA bytes from the backend already match NativeImage's ABGR-int layout when
for (int i = 0; i < pixels; i++) { // viewed as little-endian bytes: byte 0 = R (low byte of ABGR int), byte 1 = G,
int r = rgba.get() & 0xFF; // byte 2 = B, byte 3 = A. So a flat memcpy works — no per-pixel swap needed.
int g = rgba.get() & 0xFF; // This replaces a 2M-iteration Java loop with one native memcpy for 1080p frames,
int b = rgba.get() & 0xFF; // cutting upload time from ~20ms to <1ms and removing the main stutter source.
int a = rgba.get() & 0xFF; long bytes = (long) w * h * 4L;
int abgr = (a << 24) | (b << 16) | (g << 8) | r; MemoryUtil.memCopy(MemoryUtil.memAddress(rgba), img.getPointer(), bytes);
img.setPixelABGR(i % w, i / w, abgr);
}
texture.upload(); texture.upload();
} }