From d432aa2d15a7adcc827df7100469ee7046f094d6 Mon Sep 17 00:00:00 2001 From: gaoyu06 Date: Fri, 16 Oct 2026 22:12:13 -0700 Subject: [PATCH 1/4] =?UTF-8?q?perf(replay):=20=E5=9B=9E=E6=94=BE=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E6=B5=81=E5=BC=8F=E8=AF=BB=E5=8F=96=EF=BC=8C=E6=89=93?= =?UTF-8?q?=E5=BC=80=E4=B8=8E=E8=B7=B3=E8=BD=AC=E4=B8=8D=E5=86=8D=E9=98=BB?= =?UTF-8?q?=E5=A1=9E=E6=B8=B2=E6=9F=93=E7=BA=BF=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新录像写成格式 2:未压缩文件头 + 按块 zlib 压缩,块头记录时间范围、记录数、 长度和 CRC;flush 即封块,崩溃只丢最后一次 flush 之后的内容,损坏块读作结尾 - 旧格式 1(整文件 gzip)照常读取,按文件头魔数自动识别,不转换、不改原文件 - NovaReplayFile.scanIndex:格式 2 只读块头跳过正文,格式 1 后台整段扫描; 回放打开后在后台线程建索引得到时长 - ReplayStream:独立线程预读,内存只保留播放头前方有界窗口(堆的 1/32,8–64MB), 录像多长都一样 - ReplayCursor:跳转只改目标,每 tick 按墙钟 60% 的预算分片应用记录;往回跳仍需 从 0 重建世界(包流无法倒放),但改为分片进行;时钟在追赶期间暂停 - ReplayPlayback:打开只读文件头;重建期间原版载入界面结束后恢复先前界面; 覆盖层和回放界面显示"加载中 N%";去掉"录像过大只载入前 N 分钟"的截断 - 导演工作台:追赶期间不推进输出时钟,导出等世界追上后再截帧;索引到达后 自动补全工程时长 - 单测:读写往返、损坏块、索引、旧格式读取与跳转、正反向跳转定位、 零预算逐条推进、190MB 合成录像下预读峰值不超过窗口 --- .../fpsmaster/module/impl/auxiliary/Replay.kt | 8 - .../top/fpsmaster/replay/DirectorSession.kt | 25 +- .../top/fpsmaster/replay/NovaReplayFile.kt | 449 +++++++++++++++--- .../top/fpsmaster/replay/ReplayCursor.kt | 97 ++++ .../top/fpsmaster/replay/ReplayPlayback.kt | 258 ++++++---- .../top/fpsmaster/replay/ReplayStream.kt | 104 ++++ .../top/fpsmaster/ui/NativeReplayScreen.kt | 14 +- .../kotlin/top/fpsmaster/ui/ReplayOverlay.kt | 17 +- .../assets/fpsmaster/lang/en_us.lang | 2 +- .../assets/fpsmaster/lang/zh_cn.lang | 2 +- .../top/fpsmaster/replay/LegacyReplay.kt | 45 ++ .../fpsmaster/replay/NovaReplayFileTest.kt | 97 +++- .../fpsmaster/replay/ReplayStreamingTest.kt | 227 +++++++++ 13 files changed, 1155 insertions(+), 190 deletions(-) create mode 100644 src/main/kotlin/top/fpsmaster/replay/ReplayCursor.kt create mode 100644 src/main/kotlin/top/fpsmaster/replay/ReplayStream.kt create mode 100644 src/test/kotlin/top/fpsmaster/replay/LegacyReplay.kt create mode 100644 src/test/kotlin/top/fpsmaster/replay/ReplayStreamingTest.kt diff --git a/src/main/kotlin/top/fpsmaster/module/impl/auxiliary/Replay.kt b/src/main/kotlin/top/fpsmaster/module/impl/auxiliary/Replay.kt index 1eb932d..c87abfd 100644 --- a/src/main/kotlin/top/fpsmaster/module/impl/auxiliary/Replay.kt +++ b/src/main/kotlin/top/fpsmaster/module/impl/auxiliary/Replay.kt @@ -188,14 +188,6 @@ class Replay : Module("replay", Category.AUXILIARY) { project.ensureDuration(playback.durationMillis) session = DirectorSession(playback, store, project) logger.info("[replay] playing ${file.name}") - if (playback.truncated) { - NotificationManager.add( - Language.get("replay.title"), - Language.get("replay.status.truncated").format(playback.durationMillis / 60_000), - 5f, - NotificationCenter.Type.WARNING - ) - } return null } diff --git a/src/main/kotlin/top/fpsmaster/replay/DirectorSession.kt b/src/main/kotlin/top/fpsmaster/replay/DirectorSession.kt index 9395bbd..2ac94c9 100644 --- a/src/main/kotlin/top/fpsmaster/replay/DirectorSession.kt +++ b/src/main/kotlin/top/fpsmaster/replay/DirectorSession.kt @@ -76,19 +76,27 @@ class DirectorSession( applyCamera(source) } - /** Client thread, once per frame. */ + /** Client thread, once per tick. */ fun tick() { + // The length arrives from a background index a moment after opening. + project.ensureDuration(sourceDuration()) + // Whether the frame drawn since the last tick showed a fully loaded world. + val settled = !playback.isCatchingUp + playback.tick() val running = export if (running != null) { - running.step() + running.step(settled) if (running.isFinished) { export = null } return } pollRollKeys() - playback.tick() - advanceOutputClock() + // While the world is still loading towards the playhead, positionMillis is where it is + // going, not where it is; following clip edges off it would seek again mid-load. + if (!playback.isCatchingUp) { + advanceOutputClock() + } applyCamera(playback.positionMillis) } @@ -275,10 +283,17 @@ class DirectorSession( scrubTo(0L) } - fun step() { + /** + * [settled]: the world had caught up before this tick, so the frame drawn since shows the + * position set last time. A seek that has not finished loading skips capture until then. + */ + fun step(settled: Boolean) { if (isFinished) { return } + if (!settled || playback.isCatchingUp) { + return + } if (frameIndex >= plan.frameCount) { finish() return diff --git a/src/main/kotlin/top/fpsmaster/replay/NovaReplayFile.kt b/src/main/kotlin/top/fpsmaster/replay/NovaReplayFile.kt index f27c509..7bcef2c 100644 --- a/src/main/kotlin/top/fpsmaster/replay/NovaReplayFile.kt +++ b/src/main/kotlin/top/fpsmaster/replay/NovaReplayFile.kt @@ -2,15 +2,23 @@ package top.fpsmaster.replay import java.io.BufferedInputStream import java.io.BufferedOutputStream +import java.io.ByteArrayOutputStream import java.io.Closeable +import java.io.DataInput import java.io.DataInputStream import java.io.DataOutputStream +import java.io.EOFException import java.io.File import java.io.FileInputStream import java.io.FileOutputStream import java.io.IOException +import java.io.InputStream +import java.io.RandomAccessFile +import java.util.zip.CRC32 +import java.util.zip.DataFormatException +import java.util.zip.Deflater import java.util.zip.GZIPInputStream -import java.util.zip.GZIPOutputStream +import java.util.zip.Inflater /** The file is not a NOVAREPL container, or its layout is one this build cannot read. */ class ReplayFormatException(message: String) : IOException(message) @@ -28,12 +36,26 @@ class ReplayVersionMismatchException( " (this client is $currentVersion)" ) +/** + * What a scan of a whole recording found, without keeping any of it: how long it runs and how many + * records it holds. Built off the client thread ([NovaReplayFile.scanIndex]) when a replay opens. + */ +data class ReplayIndex(val durationMillis: Int, val recordCount: Long, val blockCount: Int) + /** * On-disk format for a recorded clientbound packet stream. * + * Format 2 (written by this build): + * ``` + * header UTF "NOVAREPL" | int 2 | UTF minecraftVersion | long wall-clock start + * recording profile (see RecordingProfile) -- uncompressed + * block int "NRBK" | int minMillis | int maxMillis | int recordCount + * int rawLength | int storedLength | int crc32(stored) | stored -- zlib + * ``` + * Format 1 (read only): the same header and records, the whole file one gzip stream. + * + * Inside a block, and in format 1, records are: * ``` - * header UTF "NOVAREPL" | int formatVersion | UTF minecraftVersion | long wall-clock start - * recording profile (see RecordingProfile) * record byte type | int millis-since-start | ... * type 0 int payload length | payload bytes * type 1 double x,y,z | float yaw,pitch | byte flags @@ -41,22 +63,30 @@ class ReplayVersionMismatchException( * type 3 int payload length | payload bytes * ``` * - * A packet payload is whatever [ReplayPacketAdapter] produced on the recording client: the packet - * id and body as that Minecraft version's own codec writes them. Nothing here interprets it, which - * is why the header's `minecraftVersion` is a hard gate on [openForRead] rather than advice. + * Blocks exist so a recording can be indexed without inflating it: [scanIndex] reads block headers + * and skips the bodies, so the duration of an hour-long file is known in milliseconds. A block never + * splits a record. Each flush closes a block, so a crash still costs only what came after the last + * flush, and a torn last block fails its length or CRC check and reads as the end of the recording. + * + * A packet payload is whatever [ReplayPacketAdapter][top.fpsmaster.replay.adapter.ReplayPacketAdapter] + * produced on the recording client: the packet id and body as that Minecraft version's own codec + * writes them. Nothing here interprets it, which is why the header's `minecraftVersion` is a hard + * gate on [openForRead] rather than advice. * * Type 1 and 2 are the recording client's own player. A server never sends you your own movement or * equipment, so replaying a session with the recorder visible in it means capturing those * separately. Type 3 is the world snapshot taken when recording starts mid-session: chunks and * entities already loaded are never re-sent, so without it the replay opens on empty space. - * - * Gzipped with `syncFlush`, so a periodic flush emits a complete deflate block: a crash mid-session - * costs everything since the last flush rather than the whole file. */ object NovaReplayFile { const val MAGIC: String = "NOVAREPL" - const val FORMAT_VERSION: Int = 1 + + /** The layout this build writes. */ + const val FORMAT_VERSION: Int = 2 + + /** The original single-gzip-stream layout. Still read; never written. */ + const val LEGACY_FORMAT_VERSION: Int = 1 const val TYPE_PACKET: Int = 0 const val TYPE_LOCAL_PLAYER: Int = 1 @@ -71,103 +101,174 @@ object NovaReplayFile { /** Sanity bound on one record. Comfortably above a full chunk packet, far below a bad length. */ private const val MAX_PAYLOAD = 8 shl 20 + /** A block is closed once its records reach this many bytes uncompressed. */ + internal const val BLOCK_TARGET_BYTES = 256 shl 10 + + /** One block holds at most a target's worth plus one maximal record; anything larger is damage. */ + private const val MAX_BLOCK_BYTES = BLOCK_TARGET_BYTES + MAX_PAYLOAD + 64 + + private const val BLOCK_TAG = 0x4E52424B // "NRBK" + private const val BLOCK_HEADER_BYTES = 7 * 4 + fun openForWrite( file: File, minecraftVersion: String, startMillis: Long, profile: RecordingProfile, ): Writer { - val out = DataOutputStream( - GZIPOutputStream(BufferedOutputStream(FileOutputStream(file), 1 shl 16), true) - ) - out.writeUTF(MAGIC) - out.writeInt(FORMAT_VERSION) - out.writeUTF(minecraftVersion) - out.writeLong(startMillis) - profile.write(out) - out.flush() + val out = DataOutputStream(BufferedOutputStream(FileOutputStream(file), 1 shl 16)) + try { + out.writeUTF(MAGIC) + out.writeInt(FORMAT_VERSION) + out.writeUTF(minecraftVersion) + out.writeLong(startMillis) + profile.write(out) + out.flush() + } catch (failure: IOException) { + out.close() + throw failure + } return Writer(out) } - class Writer(private val out: DataOutputStream) : Closeable { - fun write(record: ReplayRecord) { - out.writeByte(record.type) - out.writeInt(record.millis) - when (record.type) { - TYPE_LOCAL_PLAYER -> { - out.writeDouble(record.x) - out.writeDouble(record.y) - out.writeDouble(record.z) - out.writeFloat(record.yaw) - out.writeFloat(record.pitch) - out.writeByte(record.flags) - } - - TYPE_LOCAL_EQUIPMENT -> { - out.writeByte(record.slot) - writePayload(record.payload) - } + /** Buffers records into a block and writes the block, compressed, when it fills or is flushed. */ + class Writer internal constructor(private val out: DataOutputStream) : Closeable { + private val raw = ExposedBuffer(BLOCK_TARGET_BYTES + (BLOCK_TARGET_BYTES shr 2)) + private val records = DataOutputStream(raw) + private val deflater = Deflater(Deflater.DEFAULT_COMPRESSION) + private val crc = CRC32() + private var stored = ByteArray(BLOCK_TARGET_BYTES shr 1) + private var count = 0 + private var minMillis = 0 + private var maxMillis = 0 - else -> writePayload(record.payload) + fun write(record: ReplayRecord) { + writeRecord(records, record) + if (count == 0) { + minMillis = record.millis + maxMillis = record.millis + } else { + minMillis = minOf(minMillis, record.millis) + maxMillis = maxOf(maxMillis, record.millis) + } + count++ + if (raw.size() >= BLOCK_TARGET_BYTES) { + writeBlock() } } - private fun writePayload(payload: ByteArray?) { - val bytes = payload ?: ByteArray(0) - out.writeInt(bytes.size) - out.write(bytes) + /** Closes the current block and pushes it to the file, so everything so far is readable. */ + fun flush() { + writeBlock() + out.flush() } - fun flush() = out.flush() + override fun close() { + try { + writeBlock() + } finally { + deflater.end() + out.close() + } + } - override fun close() = out.close() + private fun writeBlock() { + if (count == 0) { + return + } + deflater.reset() + deflater.setInput(raw.array(), 0, raw.size()) + deflater.finish() + var length = 0 + while (!deflater.finished()) { + if (length == stored.size) { + stored = stored.copyOf(stored.size * 2) + } + length += deflater.deflate(stored, length, stored.size - length) + } + crc.reset() + crc.update(stored, 0, length) + out.writeInt(BLOCK_TAG) + out.writeInt(minMillis) + out.writeInt(maxMillis) + out.writeInt(count) + out.writeInt(raw.size()) + out.writeInt(length) + out.writeInt(crc.value.toInt()) + out.write(stored, 0, length) + raw.reset() + count = 0 + } } /** - * Opens a recording for playback, refusing anything this client cannot decode. + * Opens a recording for playback, refusing anything this client cannot decode. Reads either + * format; records come back the same way from both. * - * @throws ReplayFormatException the file is not a NOVAREPL container of this layout + * @throws ReplayFormatException the file is not a NOVAREPL container of a known layout * @throws ReplayVersionMismatchException it is, but from another Minecraft version */ fun openForRead(file: File, currentMinecraftVersion: String): Reader { - val input = DataInputStream(GZIPInputStream(BufferedInputStream(FileInputStream(file), 1 shl 16))) - val header = try { - readHeader(input) - } catch (failure: IOException) { - input.close() - if (failure is ReplayFormatException) { - throw failure - } - throw ReplayFormatException("${file.name} ends inside its header") + val opened = openContainer(file) + if (opened.header.minecraftVersion != currentMinecraftVersion) { + opened.records.close() + throw ReplayVersionMismatchException(opened.header.minecraftVersion, currentMinecraftVersion) } - if (header.minecraftVersion != currentMinecraftVersion) { - input.close() - throw ReplayVersionMismatchException(header.minecraftVersion, currentMinecraftVersion) - } - return Reader(input, header) + return Reader(opened.records, opened.header) } /** Header only, for listing recordings without committing to playing them. */ - fun readHeader(file: File): Header = - DataInputStream(GZIPInputStream(BufferedInputStream(FileInputStream(file), 1 shl 16))) - .use { readHeader(it) } + fun readHeader(file: File): Header = openContainer(file).let { opened -> + opened.records.close() + opened.header + } - private fun readHeader(input: DataInputStream): Header { - val magic = try { - input.readUTF() - } catch (malformed: IOException) { - throw ReplayFormatException("not a Nova replay: unreadable magic") - } - if (magic != MAGIC) { - throw ReplayFormatException("not a Nova replay: magic was \"$magic\"") + /** + * Walks the whole recording for its length. Format 2 reads block headers only; format 1 has no + * blocks, so it is inflated record by record — which is why this runs off the client thread. + * Stops early, with what it has, once [cancelled] says so. + */ + fun scanIndex(file: File, cancelled: () -> Boolean = { false }): ReplayIndex { + if (isLegacy(file)) { + return openContainer(file).let { opened -> + Reader(opened.records, opened.header).use { reader -> + var duration = 0 + var count = 0L + while (!cancelled()) { + val record = reader.read() ?: break + duration = maxOf(duration, record.millis) + count++ + } + ReplayIndex(duration, count, 0) + } + } } - val formatVersion = input.readInt() - if (formatVersion != FORMAT_VERSION) { - throw ReplayFormatException( - "replay format version $formatVersion, this build reads $FORMAT_VERSION" - ) + RandomAccessFile(file, "r").use { input -> + readHeader(input, FORMAT_VERSION) + var duration = 0 + var count = 0L + var blocks = 0 + val length = input.length() + while (!cancelled() && length - input.filePointer >= BLOCK_HEADER_BYTES) { + if (input.readInt() != BLOCK_TAG) { + break + } + input.readInt() // minMillis + val maxMillis = input.readInt() + val records = input.readInt() + input.readInt() // rawLength + val storedLength = input.readInt() + input.readInt() // crc + if (storedLength < 0 || input.filePointer + storedLength > length) { + break // torn last block: the stream reader stops here as well + } + input.seek(input.filePointer + storedLength) + duration = maxOf(duration, maxMillis) + count += records + blocks++ + } + return ReplayIndex(duration, count, blocks) } - return Header(input.readUTF(), input.readLong(), RecordingProfile.read(input)) } data class Header( @@ -176,7 +277,7 @@ object NovaReplayFile { val profile: RecordingProfile, ) - class Reader(private val input: DataInputStream, val header: Header) : Closeable { + class Reader internal constructor(private val input: DataInputStream, val header: Header) : Closeable { /** * The next record, or null once no complete one remains. * @@ -224,4 +325,196 @@ object NovaReplayFile { override fun close() = input.close() } + + // ------------------------------------------------------------------ containers + + private class Opened(val header: Header, val records: DataInputStream) + + /** gzip's magic bytes; a format 2 file starts with the UTF length of "NOVAREPL" instead. */ + private fun startsWithGzip(input: InputStream): Boolean { + input.mark(2) + val first = input.read() + val second = input.read() + input.reset() + return first == 0x1f && second == 0x8b + } + + private fun isLegacy(file: File): Boolean = + BufferedInputStream(FileInputStream(file), 2).use { startsWithGzip(it) } + + private fun openContainer(file: File): Opened { + val buffered = BufferedInputStream(FileInputStream(file), 1 shl 16) + try { + if (startsWithGzip(buffered)) { + val gzip = DataInputStream(GZIPInputStream(buffered, 1 shl 16)) + return Opened(readHeader(file, gzip, LEGACY_FORMAT_VERSION), gzip) + } + val plain = DataInputStream(buffered) + val header = readHeader(file, plain, FORMAT_VERSION) + return Opened(header, DataInputStream(BlockInputStream(plain))) + } catch (failure: IOException) { + buffered.close() + if (failure is ReplayFormatException) { + throw failure + } + throw ReplayFormatException("${file.name} ends inside its header") + } + } + + private fun readHeader(file: File, input: DataInput, expectedVersion: Int): Header = try { + readHeader(input, expectedVersion) + } catch (failure: IOException) { + if (failure is ReplayFormatException) throw failure + throw ReplayFormatException("${file.name} ends inside its header") + } + + private fun readHeader(input: DataInput, expectedVersion: Int): Header { + val magic = try { + input.readUTF() + } catch (malformed: IOException) { + throw ReplayFormatException("not a Nova replay: unreadable magic") + } + if (magic != MAGIC) { + throw ReplayFormatException("not a Nova replay: magic was \"$magic\"") + } + val formatVersion = input.readInt() + if (formatVersion != expectedVersion) { + throw ReplayFormatException( + "replay format version $formatVersion in a format $expectedVersion container;" + + " this build reads $LEGACY_FORMAT_VERSION and $FORMAT_VERSION" + ) + } + return Header(input.readUTF(), input.readLong(), RecordingProfile.read(input)) + } + + internal fun writeRecord(out: DataOutputStream, record: ReplayRecord) { + out.writeByte(record.type) + out.writeInt(record.millis) + when (record.type) { + TYPE_LOCAL_PLAYER -> { + out.writeDouble(record.x) + out.writeDouble(record.y) + out.writeDouble(record.z) + out.writeFloat(record.yaw) + out.writeFloat(record.pitch) + out.writeByte(record.flags) + } + + TYPE_LOCAL_EQUIPMENT -> { + out.writeByte(record.slot) + writePayload(out, record.payload) + } + + else -> writePayload(out, record.payload) + } + } + + private fun writePayload(out: DataOutputStream, payload: ByteArray?) { + val bytes = payload ?: ByteArray(0) + out.writeInt(bytes.size) + out.write(bytes) + } + + private class ExposedBuffer(size: Int) : ByteArrayOutputStream(size) { + fun array(): ByteArray = buf + } + + /** + * The records of every block, inflated one block at a time and read as one stream. A block that + * is torn, fails its CRC or does not inflate to its stated length ends the stream there. + */ + private class BlockInputStream(private val source: DataInputStream) : InputStream() { + private val inflater = Inflater() + private val crc = CRC32() + private var block = ByteArray(0) + private var length = 0 + private var position = 0 + private var ended = false + + override fun read(): Int { + if (position >= length && !nextBlock()) { + return -1 + } + return block[position++].toInt() and 0xff + } + + override fun read(target: ByteArray, offset: Int, count: Int): Int { + if (count == 0) { + return 0 + } + if (position >= length && !nextBlock()) { + return -1 + } + val n = minOf(count, length - position) + System.arraycopy(block, position, target, offset, n) + position += n + return n + } + + private fun nextBlock(): Boolean { + while (!ended) { + val loaded = try { + loadBlock() + } catch (end: EOFException) { + false + } catch (damaged: DataFormatException) { + false + } + if (!loaded) { + ended = true + return false + } + if (length > 0) { + return true + } + } + return false + } + + private fun loadBlock(): Boolean { + if (source.readInt() != BLOCK_TAG) { + return false + } + source.readInt() // minMillis + source.readInt() // maxMillis + source.readInt() // recordCount + val rawLength = source.readInt() + val storedLength = source.readInt() + val expectedCrc = source.readInt() + if (rawLength < 0 || rawLength > MAX_BLOCK_BYTES || storedLength < 0 || storedLength > MAX_BLOCK_BYTES) { + return false + } + val stored = ByteArray(storedLength) + source.readFully(stored) + crc.reset() + crc.update(stored, 0, storedLength) + if (crc.value.toInt() != expectedCrc) { + return false + } + if (block.size < rawLength) { + block = ByteArray(rawLength) + } + inflater.reset() + inflater.setInput(stored) + var inflated = 0 + while (inflated < rawLength && !inflater.finished()) { + val n = inflater.inflate(block, inflated, rawLength - inflated) + if (n == 0 && (inflater.needsInput() || inflater.needsDictionary())) { + break + } + inflated += n + } + if (inflated != rawLength) { + return false + } + length = rawLength + position = 0 + return true + } + + override fun close() { + inflater.end() + source.close() + } + } } diff --git a/src/main/kotlin/top/fpsmaster/replay/ReplayCursor.kt b/src/main/kotlin/top/fpsmaster/replay/ReplayCursor.kt new file mode 100644 index 0000000..ec677c5 --- /dev/null +++ b/src/main/kotlin/top/fpsmaster/replay/ReplayCursor.kt @@ -0,0 +1,97 @@ +package top.fpsmaster.replay + +import java.io.Closeable + +/** + * Where playback stands in a recording, and the work of getting it somewhere else. + * + * A packet stream only runs forwards: any packet may be the one that placed the block on screen, so + * the only way back is to tear the world down ([rebuild]) and apply everything again from the start. + * What this changes over holding the whole recording in memory is *when* that work happens: a seek + * only moves [targetMillis], and [pump] applies records towards it within a time budget, a slice + * per tick. The records come from a [ReplayStream], so memory holds a bounded window ahead of the + * playhead rather than the recording. + * + * Minecraft-free on purpose, so seeking can be tested against a synthetic recording. + */ +class ReplayCursor( + private val openStream: () -> ReplayStream, + private val apply: (ReplayRecord) -> Unit, + private val rebuild: () -> Unit, +) : Closeable { + + private var stream: ReplayStream = openStream() + + /** Where playback has been asked to be. */ + var targetMillis: Int = 0 + private set + + /** The newest record applied so far; -1 before anything, even the records at zero, has been. */ + var appliedMillis: Int = -1 + private set + + /** Every record stamped at or before this has been applied. Trails [targetMillis] while loading. */ + var reachedMillis: Int = -1 + private set + + /** Times the world had to be rebuilt from the start. */ + var rebuilds: Int = 0 + private set + + val isCaughtUp: Boolean + get() = reachedMillis >= targetMillis + + /** Caught up with nothing left to read: playback has reached the end of the recording. */ + val isAtEnd: Boolean + get() = isCaughtUp && stream.isExhausted + + /** + * Moves the target. Behind anything already applied, that means rebuilding the world and + * reading again from the beginning; anywhere else it costs nothing until [pump]. + */ + fun seekTo(millis: Int) { + val target = millis.coerceAtLeast(0) + if (target < appliedMillis) { + stream.close() + rebuild() + stream = openStream() + appliedMillis = -1 + reachedMillis = -1 + rebuilds++ + } + targetMillis = target + } + + /** + * Applies records towards [targetMillis] until caught up, until the reader has nothing ready, + * or until [deadlineNanos] passes — whichever comes first. Always applies at least one ready + * record, so a tiny budget still makes progress. Returns whether it caught up. + */ + fun pump(deadlineNanos: Long, clock: () -> Long = System::nanoTime): Boolean { + while (reachedMillis < targetMillis) { + // Read "finished" before looking at the buffer: the reader adds its last record and then + // finishes, so finished-then-empty really is the end. + val finished = stream.isFinished + val next = stream.peek() + if (next == null) { + if (finished) { + reachedMillis = targetMillis + } + break + } + if (next.millis > targetMillis) { + reachedMillis = targetMillis + break + } + stream.poll() + apply(next) + appliedMillis = maxOf(appliedMillis, next.millis) + if (clock() - deadlineNanos >= 0) { + break + } + } + return isCaughtUp + } + + override fun close() = stream.close() +} diff --git a/src/main/kotlin/top/fpsmaster/replay/ReplayPlayback.kt b/src/main/kotlin/top/fpsmaster/replay/ReplayPlayback.kt index 0eb19c9..f3605c9 100644 --- a/src/main/kotlin/top/fpsmaster/replay/ReplayPlayback.kt +++ b/src/main/kotlin/top/fpsmaster/replay/ReplayPlayback.kt @@ -10,6 +10,9 @@ import top.fpsmaster.replay.adapter.ReplayWorldAdapter import top.fpsmaster.screenCompat import java.io.File +import net.minecraft.client.gui.screens.Screen +import top.fpsmaster.setScreenCompat + /** * Plays a recording back on the client thread, into an isolated listener. * @@ -18,21 +21,21 @@ import java.io.File * would have handed them over anyway. The vanilla handlers then build the world exactly as they did * while it was being recorded. * - * Records are held in memory. Seeking backwards means tearing the world down and replaying from the - * start — a packet stream has no other way back, since any packet may be the one that placed the - * block you are looking at — and a stream that had to be re-read from disk for every scrub would - * make the director unusable. + * Nothing here holds the recording. A [ReplayStream] reads it on its own thread into a bounded + * window ahead of the playhead, a [ReplayCursor] applies records from that window a time-budgeted + * slice per tick, and the length comes from a [ReplayIndex] built on a background thread. Opening, + * and seeking — which backwards still means rebuilding the world from the start, since a packet + * stream cannot run in reverse — therefore never stall a frame; until the world has caught up with + * the playhead [isLoading] says so, and the clock waits. */ class ReplayPlayback private constructor( + private val file: File, val header: NovaReplayFile.Header, - private val records: List, - /** The file held more than [maxLoadedBytes]; only its beginning is playable. */ - val truncated: Boolean, ) { - /** Position on the recording's own timeline. */ - var positionMillis: Int = 0 - private set + /** Position on the recording's own timeline: where playback is, or is loading towards. */ + val positionMillis: Int + get() = cursor?.targetMillis ?: 0 var isPaused: Boolean = true private set @@ -42,22 +45,65 @@ class ReplayPlayback private constructor( field = value.coerceIn(SPEED_MIN, SPEED_MAX) } - val durationMillis: Int = records.lastOrNull()?.millis ?: 0 + /** Length of the recording; 0 until the background index lands, see [isIndexed]. */ + @Volatile + var durationMillis: Int = 0 + private set + + @Volatile + var isIndexed: Boolean = false + private set /** True while playback owns the client's world. */ val isOpen: Boolean get() = session != null + /** + * The world is behind the playhead, or a loading screen vanilla put up while rebuilding it is + * still waiting to be taken down. Nothing on screen is the playhead's moment yet. + */ + val isCatchingUp: Boolean + get() = cursor?.isCaughtUp == false || restorePending + + /** Behind by enough to be worth telling the viewer, rather than a tick's worth of packets. */ + val isLoading: Boolean + get() { + val active = cursor ?: return false + return restorePending || active.targetMillis - active.reachedMillis > LOADING_THRESHOLD_MILLIS + } + + /** How far loading has got towards the playhead, 0..1. */ + val loadingProgress: Float + get() { + val active = cursor ?: return 0f + if (active.targetMillis <= 0) { + return if (active.isCaughtUp) 1f else 0f + } + return (active.appliedMillis.coerceAtLeast(0).toFloat() / active.targetMillis).coerceIn(0f, 1f) + } + private var session: ReplayWorldAdapter.IsolatedSession? = null - private var cursor = 0 + private var cursor: ReplayCursor? = null private var lastFrameNanos = 0L + private var lastPumpNanos = 0L + + @Volatile + private var indexCancelled = false + + /** + * Opening and rebuilding go through vanilla's login handling, which puts up a level-loading + * screen. Once the world has caught up it is replaced by [restoreScreen]: the world itself after + * opening, whatever was open before a rebuild (the replay controls, mid-scrub). + */ + private var restorePending = false + private var restoreScreen: Screen? = null /** Set when the recorded player's own position should drive the camera. */ var followRecorder: Boolean = true /** - * Opens the world and applies everything stamped at time zero — the session packets and the - * terrain snapshot — before the first frame is drawn. + * Leaves whatever world is open and starts loading this one: the session packets and terrain + * snapshot stamped at time zero arrive over the next ticks, behind a loading screen. */ fun open() { if (session != null) { @@ -65,6 +111,11 @@ class ReplayPlayback private constructor( } leaveCurrentSession() startIsolatedSession() + cursor = ReplayCursor(::openStream, ::applyRecord, ::rebuild) + lastFrameNanos = System.nanoTime() + lastPumpNanos = lastFrameNanos + armRestore(null) + startIndexing() } /** @@ -95,10 +146,31 @@ class ReplayPlayback private constructor( private fun startIsolatedSession() { val profile = GameProfile(header.profile.recorderId, header.profile.recorderName) session = ReplayWorldAdapter.open(profile) - cursor = 0 - positionMillis = 0 - lastFrameNanos = System.nanoTime() - applyUntil(0) + } + + private fun openStream(): ReplayStream = ReplayStream( + { NovaReplayFile.openForRead(file, header.minecraftVersion) }, + windowBytes(), + ) + + private fun startIndexing() { + val worker = Thread({ + try { + val index = NovaReplayFile.scanIndex(file) { indexCancelled } + if (!indexCancelled) { + durationMillis = index.durationMillis + isIndexed = true + logger.info( + "[replay] indexed ${file.name}: ${index.recordCount} records," + + " ${index.durationMillis / 1000}s, ${index.blockCount} blocks" + ) + } + } catch (failure: Exception) { + logger.warn("[replay] could not index ${file.name}: ${failure.message}") + } + }, "nova-replay-index") + worker.isDaemon = true + worker.start() } fun play() { @@ -110,83 +182,114 @@ class ReplayPlayback private constructor( isPaused = true } - /** Client thread, once per frame. Advances the clock and applies whatever is now due. */ + /** Client thread, once per tick. Advances the clock and applies a slice of whatever is now due. */ fun tick() { val active = session ?: return + val playhead = cursor ?: return active.discardOutbound() - if (isPaused) { - lastFrameNanos = System.nanoTime() - return - } val now = System.nanoTime() - val deltaMillis = ((now - lastFrameNanos) / 1_000_000L * speed).toInt() - lastFrameNanos = now - if (deltaMillis <= 0) { - return + // The clock waits while the world catches up: otherwise the playhead would run on ahead + // of a world that is still loading, and the viewer would miss whatever happened meanwhile. + if (isPaused || isCatchingUp) { + lastFrameNanos = now + } else { + val deltaMillis = ((now - lastFrameNanos) / 1_000_000L * speed).toInt() + lastFrameNanos = now + if (deltaMillis > 0) { + var target = playhead.targetMillis + deltaMillis + if (isIndexed) { + target = minOf(target, durationMillis) + } + playhead.seekTo(target) + } } - applyUntil(positionMillis + deltaMillis) - if (positionMillis >= durationMillis) { + pump() + if (!isPaused && !isCatchingUp && + (playhead.isAtEnd || (isIndexed && playhead.targetMillis >= durationMillis)) + ) { pause() } } /** - * Moves to [millis]. Forwards this replays the packets in between as fast as it can; backwards - * it rebuilds the world from the start, because a packet stream cannot be run in reverse. + * Moves to [millis]. Forwards this applies the packets in between; backwards it rebuilds the + * world from the start, because a packet stream cannot be run in reverse. Either way only a + * budgeted slice runs now and [tick] carries on with the rest — see [isCatchingUp]. */ fun seekTo(millis: Int) { - val target = millis.coerceIn(0, durationMillis) - if (target < positionMillis) { - rebuild() - } - applyUntil(target) - // seek 花掉的墙钟不是「回放走过的时间」:rebuild 要拆掉世界、再从 0 毫秒同步重放 - // 整条包流,几秒是常事。不在这儿把时钟重新对齐的话,下一帧算出来的 delta 就是这 - // 几秒乘上 speed,播放头一步跨过好几个片段,又触发一次往回 seek——高倍速加密集 - // 切分时会自锁成「每帧重建一次世界」的死循环。 + val playhead = cursor ?: return + val target = if (isIndexed) millis.coerceIn(0, durationMillis) else millis.coerceAtLeast(0) + playhead.seekTo(target) + pump() + // seek 花掉的墙钟不是「回放走过的时间」。不在这儿把时钟重新对齐的话,下一帧算出来的 + // delta 就是这段时间乘上 speed,播放头一步跨过好几个片段,又触发一次往回 seek。 lastFrameNanos = System.nanoTime() } fun close() { + indexCancelled = true + cursor?.close() + cursor = null session?.let { ReplayWorldAdapter.close(it) } session = null + restorePending = false + restoreScreen = null // reset(), not clear(): clear() deliberately keeps the hand-set roll (an empty camera track // runs through it every tick), and leaving the replay is where that tilt has to go. DirectorRenderAdapter.reset() } - /** The recorder's pose at [millis], for the camera to follow and for keyframing against. */ - fun recorderPoseAt(millis: Int): ReplayRecord? = records - .lastOrNull { it.type == NovaReplayFile.TYPE_LOCAL_PLAYER && it.millis <= millis } + /** + * Applies records for at most a share of the wall time since the last call, so catching up + * after a seek leaves frames being drawn — including when the client runs several ticks in + * one frame to catch up with itself, where the share comes out at the floor. + */ + private fun pump() { + val playhead = cursor ?: return + val now = System.nanoTime() + val budget = ((now - lastPumpNanos) * PUMP_SHARE_PERCENT / 100).coerceIn(PUMP_MIN_NANOS, PUMP_MAX_NANOS) + lastPumpNanos = now + playhead.pump(now + budget) + restoreScreenWhenLoaded() + } + /** Cursor callback: the playhead went backwards, so the world starts over. */ private fun rebuild() { - val active = session ?: return - ReplayWorldAdapter.close(active) + armRestore(mc.screenCompat) + session?.let { ReplayWorldAdapter.close(it) } session = null startIsolatedSession() } - private fun applyUntil(target: Int) { - val active = session ?: return - val registries = active.registries - while (cursor < records.size && records[cursor].millis <= target) { - apply(active, records[cursor], registries) - cursor++ + private fun armRestore(screen: Screen?) { + // A rebuild while an earlier one is still loading keeps the first one's screen: what is + // open now is the loading screen that rebuild put up. + if (!restorePending) { + restorePending = true + restoreScreen = screen + } + } + + private fun restoreScreenWhenLoaded() { + if (!restorePending || cursor?.isCaughtUp != true || mc.level == null) { + return + } + restorePending = false + val target = restoreScreen + restoreScreen = null + if (mc.screenCompat !== target) { + mc.setScreenCompat(target) } - positionMillis = target } - private fun apply( - active: ReplayWorldAdapter.IsolatedSession, - record: ReplayRecord, - registries: net.minecraft.core.RegistryAccess, - ) { + private fun applyRecord(record: ReplayRecord) { + val active = session ?: return if (record.type == NovaReplayFile.TYPE_LOCAL_PLAYER) { applyRecorderPose(record) return } val payload = record.payload ?: return - val packet = ReplayPacketAdapter.decode(payload, registries) ?: return + val packet = ReplayPacketAdapter.decode(payload, active.registries) ?: return try { ReplayWorldAdapter.dispatch(active, packet) } catch (failure: Throwable) { @@ -215,34 +318,23 @@ class ReplayPlayback private constructor( const val SPEED_MIN: Float = 0.1f const val SPEED_MAX: Float = 8f + /** Behind the playhead by more than this, the overlay shows loading instead of nothing. */ + private const val LOADING_THRESHOLD_MILLIS = 250 + + private const val PUMP_SHARE_PERCENT = 60L + private const val PUMP_MIN_NANOS = 2_000_000L + private const val PUMP_MAX_NANOS = 40_000_000L + /** - * Reads a recording into memory. + * Opens a recording for playback. Reads the header only — the records stream in once + * [open] is called. * * @throws ReplayVersionMismatchException the file was recorded on another Minecraft version * @throws ReplayFormatException it is not a recording this build can read */ fun load(file: File): ReplayPlayback { - val reader = NovaReplayFile.openForRead(file, ReplayPacketAdapter.minecraftVersion) - val records = ArrayList() - val limit = maxLoadedBytes() - var truncated = false - reader.use { - var bytes = 0L - while (true) { - val record = it.read() ?: break - records.add(record) - bytes += (record.payload?.size ?: 0).toLong() + ClipRing.RECORD_OVERHEAD - if (bytes > limit) { - truncated = true - logger.warn( - "[replay] ${file.name} is larger than ${limit shr 20}MB;" + - " playing back the first ${records.size} records" - ) - break - } - } - } - return ReplayPlayback(reader.header, records, truncated) + val header = NovaReplayFile.openForRead(file, ReplayPacketAdapter.minecraftVersion).use { it.header } + return ReplayPlayback(file, header) } /** Header only, for listing recordings without reading them. */ @@ -253,9 +345,9 @@ class ReplayPlayback private constructor( } /** - * Ceiling on how much of a recording is held at once: a quarter of the heap, at most 512 MB. - * A fixed 512 MB on the common 2 GB default heap left too little for the world itself. + * How much of a recording is read ahead of the playhead: a thirty-second of the heap, between + * 8 and 64 MB. Whatever the recording's length, this is all of it that is in memory at once. */ - fun maxLoadedBytes(): Long = (Runtime.getRuntime().maxMemory() / 4).coerceIn(64L shl 20, 512L shl 20) + fun windowBytes(): Long = (Runtime.getRuntime().maxMemory() / 32).coerceIn(8L shl 20, 64L shl 20) } } diff --git a/src/main/kotlin/top/fpsmaster/replay/ReplayStream.kt b/src/main/kotlin/top/fpsmaster/replay/ReplayStream.kt new file mode 100644 index 0000000..7c3b3e2 --- /dev/null +++ b/src/main/kotlin/top/fpsmaster/replay/ReplayStream.kt @@ -0,0 +1,104 @@ +package top.fpsmaster.replay + +import java.io.Closeable + +/** + * Reads a recording ahead of playback on its own thread, holding at most [maxBufferedBytes] of + * records that have been read but not yet consumed. + * + * This is the whole of what playback keeps in memory: records are dropped as soon as they are + * applied, and the reader waits whenever the window is full. One record larger than the window is + * still let through on its own, so a big chunk packet cannot wedge the stream. + */ +class ReplayStream( + open: () -> NovaReplayFile.Reader, + private val maxBufferedBytes: Long, + threadName: String = "nova-replay-stream", +) : Closeable { + + private val lock = Object() + private val buffer = ArrayDeque() + private var bufferedBytes = 0L + + @Volatile + private var closed = false + + /** Set once the reader has nothing more to add: end of file, a truncated tail, or a failure. */ + @Volatile + var isFinished: Boolean = false + private set + + /** Why reading stopped early, if it did. A truncated tail is not a failure. */ + @Volatile + var failure: Throwable? = null + private set + + /** High-water mark of [bufferedBytes], for the memory bound to be checked against. */ + @Volatile + var peakBufferedBytes: Long = 0L + private set + + private val thread = Thread({ run(open) }, threadName).apply { + isDaemon = true + start() + } + + /** The next record if one has been read, without waiting for the reader. */ + fun peek(): ReplayRecord? = synchronized(lock) { buffer.firstOrNull() } + + fun poll(): ReplayRecord? = synchronized(lock) { + val record = buffer.removeFirstOrNull() ?: return null + bufferedBytes -= cost(record) + lock.notifyAll() + record + } + + /** True once every record has been read and consumed. */ + val isExhausted: Boolean + get() = isFinished && synchronized(lock) { buffer.isEmpty() } + + override fun close() { + closed = true + synchronized(lock) { + buffer.clear() + bufferedBytes = 0L + lock.notifyAll() + } + thread.interrupt() + } + + private fun run(open: () -> NovaReplayFile.Reader) { + try { + open().use { reader -> + while (!closed) { + val record = reader.read() ?: break + val size = cost(record) + synchronized(lock) { + while (!closed && buffer.isNotEmpty() && bufferedBytes + size > maxBufferedBytes) { + lock.wait() + } + if (closed) { + return + } + buffer.addLast(record) + bufferedBytes += size + if (bufferedBytes > peakBufferedBytes) { + peakBufferedBytes = bufferedBytes + } + } + } + } + } catch (interrupted: InterruptedException) { + // close(): nothing further is wanted. + } catch (failed: Throwable) { + failure = failed + } finally { + isFinished = true + } + } + + companion object { + /** What one buffered record costs: its payload plus the same overhead [ClipRing] charges. */ + fun cost(record: ReplayRecord): Long = (record.payload?.size ?: 0).toLong() + ClipRing.RECORD_OVERHEAD + } +} diff --git a/src/main/kotlin/top/fpsmaster/ui/NativeReplayScreen.kt b/src/main/kotlin/top/fpsmaster/ui/NativeReplayScreen.kt index 797b1ab..cd4dc9f 100644 --- a/src/main/kotlin/top/fpsmaster/ui/NativeReplayScreen.kt +++ b/src/main/kotlin/top/fpsmaster/ui/NativeReplayScreen.kt @@ -130,15 +130,10 @@ class NativeReplayScreen( return } if (Chrome.button(ui, x, y, 90f, Metrics.BTN_H, tr("replay.play"), Chrome.ButtonStyle.PRIMARY)) { + // 打开只读文件头,世界在后面几个 tick 里流式载入:原版先挂上载入地形的界面, + // 追上之后 ReplayPlayback 把界面收掉,直接落进世界,控制条交给 ReplayOverlay。 val failure = Replay.open(file) status = failure ?: tr("replay.status.playing").format(file.name) - // 只有真的进了世界才关界面:控制条挂在 ReplayOverlay 上,而它在 mc.player 为空时 - // 直接返回不画。从主菜单打开一个回放、世界还没起来就把界面关掉的话,玩家落到一张 - // 什么都没有的标题画面上——没有控制条,也没有刚才那个回放列表,只能以为点崩了。 - if (Replay.session != null && mc.level != null) { - // 直接掉进世界里看回放,控制条交给 ReplayOverlay,按 ESC 能把这个界面叫回来。 - mc.setScreenCompat(null) - } } } @@ -152,9 +147,12 @@ class NativeReplayScreen( val duration = session.outputDuration() val playback = session.playback + val loading = + if (playback.isLoading) " " + tr("replay.hud.loading").format((playback.loadingProgress * 100f).toInt()) + else "" ui.canvas().drawString( ui.font(10), - "${time(session.outputMillis)} / ${time(duration)} x${"%.2f".format(playback.speed)}", + "${time(session.outputMillis)} / ${time(duration)} x${"%.2f".format(playback.speed)}$loading", x, y, ui.theme().textPrimary(), diff --git a/src/main/kotlin/top/fpsmaster/ui/ReplayOverlay.kt b/src/main/kotlin/top/fpsmaster/ui/ReplayOverlay.kt index 26cc517..d04d280 100644 --- a/src/main/kotlin/top/fpsmaster/ui/ReplayOverlay.kt +++ b/src/main/kotlin/top/fpsmaster/ui/ReplayOverlay.kt @@ -82,15 +82,22 @@ object ReplayOverlay { val clock = "${time(session.outputMillis)} / ${time(duration)} x${"%.2f".format(session.playback.speed)}" val font = ui.font(10) ui.canvas().drawString(font, clock, x + 10f, PANEL_Y + 7f, ui.theme().textPrimary()) - if (session.playback.isPaused) { - val paused = Language.get("replay.hud.paused") + // 拖回去或刚打开时世界要从头重放到播放头,这段时间时钟停着,得让人知道不是卡死了。 + val playback = session.playback + val badge = when { + playback.isLoading -> + Language.get("replay.hud.loading").format((playback.loadingProgress * 100f).toInt()) + playback.isPaused -> Language.get("replay.hud.paused") + else -> null + } + if (badge != null) { val small = ui.font(9) ui.canvas().drawString( small, - paused, - x + w - 10f - small.measure(paused), + badge, + x + w - 10f - small.measure(badge), PANEL_Y + 8f, - ui.theme().danger(), + if (playback.isLoading) ui.theme().textSecondary() else ui.theme().danger(), ) } diff --git a/src/main/resources/assets/fpsmaster/lang/en_us.lang b/src/main/resources/assets/fpsmaster/lang/en_us.lang index 120aee3..4c7ea2b 100644 --- a/src/main/resources/assets/fpsmaster/lang/en_us.lang +++ b/src/main/resources/assets/fpsmaster/lang/en_us.lang @@ -1207,7 +1207,7 @@ replay.clip.saved=Clip saved: %s replay.clip.failed=Could not save the clip; see the log replay.clip.busy=The previous clip is still being saved replay.clip.empty=Nothing to save yet, try again shortly -replay.status.truncated=Recording too large; only the first %s minutes were loaded +replay.hud.loading=Loading %s%% configbackup.title=Config backup configbackup.restored=The config file was damaged and has been restored from the %s backup configbackup.reset=The config file was damaged and no backup loaded; defaults restored (original kept as %s) diff --git a/src/main/resources/assets/fpsmaster/lang/zh_cn.lang b/src/main/resources/assets/fpsmaster/lang/zh_cn.lang index 34e42dc..2c5e614 100644 --- a/src/main/resources/assets/fpsmaster/lang/zh_cn.lang +++ b/src/main/resources/assets/fpsmaster/lang/zh_cn.lang @@ -1210,7 +1210,7 @@ replay.clip.saved=已保存片段:%s replay.clip.failed=片段保存失败,详见日志 replay.clip.busy=上一个片段还在保存 replay.clip.empty=还没有可保存的片段,请稍候 -replay.status.truncated=录像过大,只载入了前 %s 分钟 +replay.hud.loading=加载中 %s%% configbackup.title=配置备份 configbackup.restored=配置文件损坏,已从 %s 的备份恢复 configbackup.reset=配置文件损坏且没有可用备份,已恢复默认设置(原文件保存为 %s) diff --git a/src/test/kotlin/top/fpsmaster/replay/LegacyReplay.kt b/src/test/kotlin/top/fpsmaster/replay/LegacyReplay.kt new file mode 100644 index 0000000..0000930 --- /dev/null +++ b/src/test/kotlin/top/fpsmaster/replay/LegacyReplay.kt @@ -0,0 +1,45 @@ +package top.fpsmaster.replay + +import java.io.DataOutputStream +import java.io.File +import java.util.zip.GZIPOutputStream + +/** + * Writes a format 1 recording byte for byte as the original writer did: one gzip stream holding the + * header and every record. Spelled out here rather than calling into [NovaReplayFile], so a change + * to the shared record encoding cannot quietly keep these tests passing. + */ +object LegacyReplay { + fun write(file: File, minecraftVersion: String, startMillis: Long, profile: RecordingProfile, records: List) { + DataOutputStream(GZIPOutputStream(file.outputStream(), true)).use { out -> + out.writeUTF("NOVAREPL") + out.writeInt(1) + out.writeUTF(minecraftVersion) + out.writeLong(startMillis) + profile.write(out) + for (record in records) { + out.writeByte(record.type) + out.writeInt(record.millis) + when (record.type) { + 1 -> { + out.writeDouble(record.x) + out.writeDouble(record.y) + out.writeDouble(record.z) + out.writeFloat(record.yaw) + out.writeFloat(record.pitch) + out.writeByte(record.flags) + } + 2 -> { + out.writeByte(record.slot) + out.writeInt(record.payload!!.size) + out.write(record.payload!!) + } + else -> { + out.writeInt(record.payload!!.size) + out.write(record.payload!!) + } + } + } + } + } +} diff --git a/src/test/kotlin/top/fpsmaster/replay/NovaReplayFileTest.kt b/src/test/kotlin/top/fpsmaster/replay/NovaReplayFileTest.kt index 0f15c87..3142ad1 100644 --- a/src/test/kotlin/top/fpsmaster/replay/NovaReplayFileTest.kt +++ b/src/test/kotlin/top/fpsmaster/replay/NovaReplayFileTest.kt @@ -105,7 +105,11 @@ class NovaReplayFileTest { fun `a truncated recording reads up to the cut`() { val file = temporary() NovaReplayFile.openForWrite(file, "1.21.11", 0L, profile).use { writer -> - repeat(8) { writer.write(ReplayRecord.packet(it * 50, ByteArray(64) { 7 })) } + repeat(8) { + writer.write(ReplayRecord.packet(it * 50, ByteArray(64) { 7 })) + // A crash costs what came after the last flush; flushes close blocks. + if (it == 3) writer.flush() + } } val whole = file.readBytes() file.writeBytes(whole.copyOf(whole.size - 40)) @@ -131,4 +135,95 @@ class NovaReplayFileTest { assertEquals(2, readBack.size) writer.close() } + + @Test + fun `a corrupted block ends the recording at the block before it`() { + val file = temporary() + NovaReplayFile.openForWrite(file, "1.21.11", 0L, profile).use { writer -> + repeat(3) { writer.write(ReplayRecord.packet(it, byteArrayOf(1, 2, 3))) } + writer.flush() + repeat(3) { writer.write(ReplayRecord.packet(10 + it, byteArrayOf(4, 5, 6))) } + } + val bytes = file.readBytes() + bytes[bytes.size - 3] = (bytes[bytes.size - 3] + 1).toByte() + file.writeBytes(bytes) + + val records = NovaReplayFile.openForRead(file, "1.21.11").use { reader -> + generateSequence { reader.read() }.toList() + } + assertEquals(listOf(0, 1, 2), records.map { it.millis }) + } + + @Test + fun `the index reads the length without inflating and agrees with a full read`() { + val file = temporary() + NovaReplayFile.openForWrite(file, "1.21.11", 0L, profile).use { writer -> + for (i in 0 until 5_000) { + writer.write(ReplayRecord.packet(i * 10, ByteArray(200) { (i + it).toByte() })) + if (i % 700 == 0) writer.flush() + } + } + + val index = NovaReplayFile.scanIndex(file) + assertEquals(49_990, index.durationMillis) + assertEquals(5_000L, index.recordCount) + assertTrue(index.blockCount > 1, "a recording this size spans several blocks") + } + + // ------------------------------------------------------------------ format 1 compatibility + + @Test + fun `a format 1 recording still reads back record for record`() { + val file = temporary() + LegacyReplay.write(file, "1.21.11", 99L, profile, listOf( + ReplayRecord.snapshotPacket(0, byteArrayOf(1, 2, 3)), + ReplayRecord.packet(120, byteArrayOf(9, 8, 7, 6)), + ReplayRecord.localPlayer(240, 1.5, 64.0, -2.5, 90f, -12f, 5), + ReplayRecord.equipment(360, 2, byteArrayOf(4, 4)), + )) + + assertEquals(99L, NovaReplayFile.readHeader(file).startMillis) + val records = NovaReplayFile.openForRead(file, "1.21.11").use { reader -> + assertEquals(profile, reader.header.profile) + generateSequence { reader.read() }.toList() + } + assertEquals(listOf(0, 120, 240, 360), records.map { it.millis }) + assertEquals( + listOf( + NovaReplayFile.TYPE_SNAPSHOT_PACKET, + NovaReplayFile.TYPE_PACKET, + NovaReplayFile.TYPE_LOCAL_PLAYER, + NovaReplayFile.TYPE_LOCAL_EQUIPMENT, + ), + records.map { it.type }, + ) + assertContentEquals(byteArrayOf(9, 8, 7, 6), records[1].payload) + assertEquals(-12f, records[2].pitch) + assertEquals(2, records[3].slot) + + val index = NovaReplayFile.scanIndex(file) + assertEquals(360, index.durationMillis) + assertEquals(4L, index.recordCount) + } + + @Test + fun `a format 1 recording from another version is still refused by name`() { + val file = temporary() + LegacyReplay.write(file, "1.20.1", 0L, profile, listOf(ReplayRecord.packet(0, byteArrayOf(1)))) + + assertFailsWith { NovaReplayFile.openForRead(file, "1.21.11") } + } + + @Test + fun `new recordings are written as format 2`() { + val file = temporary() + NovaReplayFile.openForWrite(file, "1.21.11", 0L, profile).close() + + val input = java.io.DataInputStream(file.inputStream()) + input.use { + assertEquals(NovaReplayFile.MAGIC, it.readUTF()) + assertEquals(NovaReplayFile.FORMAT_VERSION, it.readInt()) + } + assertEquals(2, NovaReplayFile.FORMAT_VERSION) + } } diff --git a/src/test/kotlin/top/fpsmaster/replay/ReplayStreamingTest.kt b/src/test/kotlin/top/fpsmaster/replay/ReplayStreamingTest.kt new file mode 100644 index 0000000..38ae656 --- /dev/null +++ b/src/test/kotlin/top/fpsmaster/replay/ReplayStreamingTest.kt @@ -0,0 +1,227 @@ +package top.fpsmaster.replay + +import java.io.File +import java.nio.ByteBuffer +import java.util.UUID +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlin.test.fail + +class ReplayStreamingTest { + + private val profile = RecordingProfile("Nova", UUID(3L, 4L), "minecraft:overworld", "local", 20, true) + private val version = "1.21.11" + private val opened = ArrayList() + + @AfterTest + fun closeAll() = opened.forEach { it.close() } + + private fun temporary(): File = File.createTempFile("nova-stream", ".novareplay").also { it.deleteOnExit() } + + /** A payload that says which record it is, so the order things were applied in can be checked. */ + private fun tagged(ordinal: Int, size: Int = 8): ByteArray = + ByteBuffer.allocate(maxOf(size, 4)).putInt(ordinal).array() + + private fun ordinalOf(record: ReplayRecord): Int = ByteBuffer.wrap(record.payload!!).int + + /** + * Five terrain snapshot records at zero, then one packet every 10 ms up to [last], flushed every + * so often so the file has many blocks. Returns every record in file order. + */ + private fun synthetic(file: File, last: Int = 9_990): List { + val records = ArrayList() + repeat(5) { records.add(ReplayRecord.snapshotPacket(0, tagged(records.size))) } + for (millis in 0..last step 10) { + records.add(ReplayRecord.packet(millis, tagged(records.size))) + } + NovaReplayFile.openForWrite(file, version, 0L, profile).use { writer -> + records.forEachIndexed { i, record -> + writer.write(record) + if (i % 97 == 0) writer.flush() + } + } + return records + } + + private class World { + val applied = ArrayList() + var rebuilds = 0 + } + + private fun cursor(file: File, world: World, window: Long = 1L shl 20): ReplayCursor = + ReplayCursor( + openStream = { ReplayStream({ NovaReplayFile.openForRead(file, version) }, window).also { opened.add(it) } }, + apply = { world.applied.add(it) }, + rebuild = { + world.applied.clear() + world.rebuilds++ + }, + ).also { opened.add(it) } + + /** Pumps without a time limit until caught up, giving the reader thread time to fill the window. */ + private fun settle(cursor: ReplayCursor) { + val deadline = System.currentTimeMillis() + 20_000 + while (!cursor.pump(Long.MAX_VALUE)) { + if (System.currentTimeMillis() > deadline) fail("never caught up to ${cursor.targetMillis}") + Thread.sleep(1) + } + } + + private fun expectedUpTo(records: List, millis: Int): List = + records.filter { it.millis <= millis }.map { ordinalOf(it) } + + @Test + fun `seeking forward applies exactly what lies between`() { + val file = temporary() + val records = synthetic(file) + val world = World() + val cursor = cursor(file, world) + + settle(cursor) + assertEquals(expectedUpTo(records, 0), world.applied.map { ordinalOf(it) }, "time zero is the snapshot and first packet") + + cursor.seekTo(5_000) + settle(cursor) + assertEquals(expectedUpTo(records, 5_000), world.applied.map { ordinalOf(it) }) + assertEquals(0, world.rebuilds, "forwards never rebuilds") + } + + @Test + fun `seeking backwards rebuilds and lands on the same state as playing there directly`() { + val file = temporary() + val records = synthetic(file) + val world = World() + val cursor = cursor(file, world) + + cursor.seekTo(7_000) + settle(cursor) + cursor.seekTo(2_505) + settle(cursor) + + assertEquals(1, world.rebuilds) + assertEquals(expectedUpTo(records, 2_505), world.applied.map { ordinalOf(it) }) + assertEquals(2_505, cursor.targetMillis) + + // Already there: no second rebuild, nothing applied twice. + cursor.seekTo(2_505) + settle(cursor) + assertEquals(1, world.rebuilds) + assertEquals(expectedUpTo(records, 2_505), world.applied.map { ordinalOf(it) }) + } + + @Test + fun `a seek part way through loading that goes backwards still rebuilds`() { + val file = temporary() + val records = synthetic(file) + val world = World() + val cursor = cursor(file, world) + + cursor.seekTo(9_000) + Thread.sleep(200) // let the reader buffer ahead; the window holds this whole file + // One record per pump: the deadline is always already past. + repeat(400) { cursor.pump(0L) { 1L } } + assertFalse(cursor.isCaughtUp) + val reached = cursor.appliedMillis + assertTrue(reached in 1..8_999, "loading was interrupted part way, at $reached") + + cursor.seekTo(reached - 500) + settle(cursor) + assertEquals(1, world.rebuilds, "the world was already past the new target") + assertEquals(expectedUpTo(records, reached - 500), world.applied.map { ordinalOf(it) }) + } + + @Test + fun `running off the end reports the end`() { + val file = temporary() + val records = synthetic(file, last = 990) + val world = World() + val cursor = cursor(file, world) + + cursor.seekTo(60_000) + settle(cursor) + assertTrue(cursor.isAtEnd) + assertEquals(records.size, world.applied.size) + } + + @Test + fun `a budget of nothing still makes progress one record at a time`() { + val file = temporary() + synthetic(file) + val world = World() + val cursor = cursor(file, world) + cursor.seekTo(1_000) + + val deadline = System.currentTimeMillis() + 10_000 + while (world.applied.isEmpty() && System.currentTimeMillis() < deadline) { + cursor.pump(0L) { 1L } + Thread.sleep(1) + } + assertEquals(1, world.applied.size, "exactly one record per exhausted budget") + } + + @Test + fun `memory holds a bounded window however long the recording is`() { + val file = temporary() + val payload = 64 shl 10 + val count = 3_000 // ~190 MB of records, compressing to a few MB on disk + NovaReplayFile.openForWrite(file, version, 0L, profile).use { writer -> + for (i in 0 until count) { + writer.write(ReplayRecord.packet(i * 50, tagged(i, payload))) + if (i % 40 == 0) writer.flush() + } + } + val window = 2L shl 20 + val world = World() + var appliedBytes = 0L + var lastOrdinal = -1 + var stream: ReplayStream? = null + val cursor = ReplayCursor( + openStream = { + ReplayStream({ NovaReplayFile.openForRead(file, version) }, window).also { + stream = it + opened.add(it) + } + }, + apply = { + // Dropped as soon as it is applied, the way playback does: only the window is held. + appliedBytes += it.payload!!.size + val ordinal = ordinalOf(it) + assertEquals(lastOrdinal + 1, ordinal, "records arrive in order") + lastOrdinal = ordinal + }, + rebuild = { world.rebuilds++ }, + ).also { opened.add(it) } + + // A slow consumer first, so the reader fills the window and has to wait. + cursor.seekTo(count * 50) + Thread.sleep(300) + settle(cursor) + + assertEquals(count - 1, lastOrdinal) + assertTrue(appliedBytes > window * 50, "the recording ($appliedBytes bytes) dwarfs the window") + val peak = stream!!.peakBufferedBytes + assertTrue(peak <= window, "read-ahead peaked at $peak bytes, over the $window byte window") + assertTrue(peak > window / 2, "the reader did read ahead into the window ($peak bytes)") + } + + @Test + fun `a format 1 recording streams and seeks like a new one`() { + val file = temporary() + val records = (0 until 300).map { ReplayRecord.packet(it * 20, tagged(it)) } + LegacyReplay.write(file, version, 0L, profile, records) + val world = World() + val cursor = cursor(file, world) + + cursor.seekTo(4_000) + settle(cursor) + cursor.seekTo(1_000) + settle(cursor) + + assertEquals(1, world.rebuilds) + assertEquals(expectedUpTo(records, 1_000), world.applied.map { ordinalOf(it) }) + assertEquals(5_980, NovaReplayFile.scanIndex(file).durationMillis) + } +} From 4ab0ddc8a0d54104a2a4fdca25cc68d66be1c631 Mon Sep 17 00:00:00 2001 From: gaoyu06 Date: Fri, 16 Oct 2026 22:16:35 -0700 Subject: [PATCH 2/4] =?UTF-8?q?build(mcef):=20=E5=8D=87=E7=BA=A7=20mcef-no?= =?UTF-8?q?va=20=E5=88=B0=201.0.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 依赖与 bundledRuntime 改为 1.0.2;vendor/maven 换成本地构建的 1.0.2 (mcef-nova release/1.0.2 @ f0c9055),移除 1.0.1 副本 - GitHub Packages 上目前只有 1.0.2-SNAPSHOT,CI 注释里留 TODO,待正式发布后 填 tag 并替换 vendor 副本 --- .github/workflows/ci-release.yml | 3 ++ build.gradle.kts | 4 +- .../mcef-nova-1.0.2-sources.jar} | Bin 53642 -> 50763 bytes .../mcef-nova-1.0.2.jar} | Bin 261505 -> 258277 bytes .../mcef-nova-1.0.2.module} | 44 +++++++++--------- .../mcef-nova-1.0.2.pom} | 2 +- 6 files changed, 28 insertions(+), 25 deletions(-) rename vendor/maven/com/github/FPSMasterTeam/mcef-nova/{1.0.1/mcef-nova-1.0.1-sources.jar => 1.0.2/mcef-nova-1.0.2-sources.jar} (55%) rename vendor/maven/com/github/FPSMasterTeam/mcef-nova/{1.0.1/mcef-nova-1.0.1.jar => 1.0.2/mcef-nova-1.0.2.jar} (78%) rename vendor/maven/com/github/FPSMasterTeam/mcef-nova/{1.0.1/mcef-nova-1.0.1.module => 1.0.2/mcef-nova-1.0.2.module} (53%) rename vendor/maven/com/github/FPSMasterTeam/mcef-nova/{1.0.1/mcef-nova-1.0.1.pom => 1.0.2/mcef-nova-1.0.2.pom} (97%) diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index 5f90b2d..e7dad07 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -177,6 +177,9 @@ jobs: env: # Lets Gradle resolve the private mcef-nova artifact from GitHub Packages. Works once the # package grants read access to this repo; until then the vendor/maven fallback covers it. + # TODO(mcef 1.0.2): GitHub Packages only has 1.0.2-SNAPSHOT so far. vendor/maven carries a + # local build of mcef-nova release/1.0.2 @ f0c9055 (unpushed) until the release is published; + # once tag v1.0.2 exists, record it here and replace the vendored copy with the published one. GITHUB_ACTOR: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION_NAME: ${{ needs.decide.outputs.version_name }} diff --git a/build.gradle.kts b/build.gradle.kts index d2f32e9..bb8477d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -251,7 +251,7 @@ dependencies { } // Version-agnostic CEF fork: a plain library (no net.minecraft), so no Loom remapping. - implementation("com.github.FPSMasterTeam:mcef-nova:1.0.1") + implementation("com.github.FPSMasterTeam:mcef-nova:1.0.2") // modRuntimeOnly(group = "maven.modrinth", name = "ImmediatelyFast", version = "1.14.2+1.21.11-fabric") // modApi(group = "maven.modrinth", name = "sodium", version = "mc1.21.11-0.8.12-fabric") @@ -319,7 +319,7 @@ dependencies { bundledRuntime("org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.4.0") bundledRuntime("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0") bundledRuntime("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.11.0") - bundledRuntime("com.github.FPSMasterTeam:mcef-nova:1.0.1") + bundledRuntime("com.github.FPSMasterTeam:mcef-nova:1.0.2") bundledRuntime("io.github.vlouboos:standaloneevent-common:1.6") // 只打 Cadence 自己的类:它的两个传递依赖 kotlin-stdlib(上面已 bundle)与 gson(MC 自带) // 都已在运行时就位,transitive 打进来只会重复/覆盖。 diff --git a/vendor/maven/com/github/FPSMasterTeam/mcef-nova/1.0.1/mcef-nova-1.0.1-sources.jar b/vendor/maven/com/github/FPSMasterTeam/mcef-nova/1.0.2/mcef-nova-1.0.2-sources.jar similarity index 55% rename from vendor/maven/com/github/FPSMasterTeam/mcef-nova/1.0.1/mcef-nova-1.0.1-sources.jar rename to vendor/maven/com/github/FPSMasterTeam/mcef-nova/1.0.2/mcef-nova-1.0.2-sources.jar index 2a9c24e047363a5116506dbf3f6800b3f93da5c2..86b6a26f33a1d26aa708172a88fe65f17a1a9420 100644 GIT binary patch delta 17367 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%0Dp-998#>;uaIgLyWj^RAm~V9$044PifMu1 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 delta 20121 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 diff --git a/vendor/maven/com/github/FPSMasterTeam/mcef-nova/1.0.1/mcef-nova-1.0.1.jar b/vendor/maven/com/github/FPSMasterTeam/mcef-nova/1.0.2/mcef-nova-1.0.2.jar similarity index 78% rename from vendor/maven/com/github/FPSMasterTeam/mcef-nova/1.0.1/mcef-nova-1.0.1.jar rename to vendor/maven/com/github/FPSMasterTeam/mcef-nova/1.0.2/mcef-nova-1.0.2.jar index 5c9873c3974fce8d10554ee6f07861630770614e..de2bcfc76157d034d1a904e57fc9d10d944f2257 100644 GIT binary patch delta 37123 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.0.0 com.github.FPSMasterTeam mcef-nova - 1.0.1 + 1.0.2 com.squareup.okhttp3 From 074adab1e4906f3eeb42511e6dd90d943c83131a Mon Sep 17 00:00:00 2001 From: gaoyu06 Date: Fri, 16 Oct 2026 22:16:35 -0700 Subject: [PATCH 3/4] =?UTF-8?q?fix(cef):=20=E7=A7=BB=E6=A4=8D=20dev=20?= =?UTF-8?q?=E5=88=86=E6=94=AF=E4=B8=8A=E4=BB=8D=E9=80=82=E7=94=A8=E7=9A=84?= =?UTF-8?q?=E4=B8=89=E5=A4=84=20mcef=20=E7=9B=B8=E5=85=B3=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BrowserOwnedTexture(26.2):每帧上传前 dst.clear()。上一帧把 limit 缩到当帧 大小,窗口先缩后放时绝对 put 越界,网页永久黑屏 - AcceleratedBrowserTexture(1.21.5–1.21.11):GlTexture 反射构造参数顺序改为 usage 在第 1 位、glId 在第 8 位(已用 1.21.8/1.21.11 字节码核对:参数 1 写入 GpuTexture.usage,参数 8 写入 GlTexture.id) - AcceleratedBrowserTexture(26.2):从桩实现换成零拷贝实现,把 mcef 导入的纹理 用 glCopyImageSubData 拷进设备纹理并以 GL swizzle 处理 BGRA;ClientBrowser 26.2 分支在加速时绘制它,失败回落 CPU 路径(已核对 26.2 GlTexture/GpuDevice 布局) - Client.configureHost:开启硬件加速且平台支持时设置 mcef.pumpOnRenderThread, mcef 1.0.2 只在渲染线程泵模式下启用共享纹理 --- .../mixin/impl/MixinGameRenderer.java | 13 +- src/main/kotlin/top/fpsmaster/Client.kt | 8 + .../web/cef/AcceleratedBrowserTexture.kt | 247 +++++++++++++++++- .../fpsmaster/web/cef/BrowserOwnedTexture.kt | 4 + .../top/fpsmaster/web/cef/ClientBrowser.kt | 22 +- 5 files changed, 272 insertions(+), 22 deletions(-) diff --git a/src/main/java/top/fpsmaster/mixin/impl/MixinGameRenderer.java b/src/main/java/top/fpsmaster/mixin/impl/MixinGameRenderer.java index 1860c91..ed3a687 100644 --- a/src/main/java/top/fpsmaster/mixin/impl/MixinGameRenderer.java +++ b/src/main/java/top/fpsmaster/mixin/impl/MixinGameRenderer.java @@ -75,11 +75,14 @@ public void hookGameRender(CallbackInfo callbackInfo) { top.fpsmaster.ui.kit.NovaBlur.INSTANCE.beginFrame(); if (MCEF.INSTANCE.isInitialized()) { try { - // On Windows/Linux CEF runs on its own message-loop thread (mcef-nova's - // CefMessageLoopThread) and this only uploads the frames that thread buffered — - // the render thread never blocks in CefDoMessageLoopWork again (that block, ~13ms - // per frame while the browser was idle, was the "40 fps until the ClickGUI opens" - // Windows bug). On macOS this still pumps CEF on this thread, then uploads. + // With hardware acceleration OFF (default), Windows/Linux CEF runs on its own + // message-loop thread (mcef-nova's CefMessageLoopThread) and this only uploads the + // frames that thread buffered — the render thread never blocks in + // CefDoMessageLoopWork (that block, ~13ms per frame while the browser was idle, was + // the "40 fps until the ClickGUI opens" Windows bug). With acceleration ON + // (mcef.pumpOnRenderThread) — and always on macOS — this pumps CEF on this thread + // first (CEF's accelerated OSR only delivers frames continuously in that mode), + // then adopts/uploads. MCEF.INSTANCE.update(); } catch (Exception e) { LogUtil.logger.error("Failed to update browser frames", e); diff --git a/src/main/kotlin/top/fpsmaster/Client.kt b/src/main/kotlin/top/fpsmaster/Client.kt index b74111b..f78e095 100644 --- a/src/main/kotlin/top/fpsmaster/Client.kt +++ b/src/main/kotlin/top/fpsmaster/Client.kt @@ -57,6 +57,14 @@ class Client : ModInitializer { return } hostConfigured = true + // mcef-nova 1.0.2 only enables CEF's shared-texture (zero-copy) paint when its message loop is + // pumped on the render thread: on its dedicated CEF thread, accelerated OSR stalls after one + // frame. The pump mode is fixed at CEF init, so decide it here from the persisted toggle AND + // actual platform/GPU support — a stale enabled setting can travel to an unsupported machine, + // where pumping on the render thread would only bring back CEF's message-loop cost per frame. + if (ClientSettings.hardwareAcceleration.getValue() && BasicBrowser.isAccelerationAvailable()) { + System.setProperty("mcef.pumpOnRenderThread", "true") + } // Legacy immediate-mode (<1.21.5) draws the imported zero-copy accel texture through a plain // position_tex shader, so have mcef R/B-swizzle imported accel textures (CEF ships BGRA imported // as RGBA8) at import time. 1.21.5+ swaps R/B in a dedicated shader instead and leaves this off. diff --git a/src/main/kotlin/top/fpsmaster/web/cef/AcceleratedBrowserTexture.kt b/src/main/kotlin/top/fpsmaster/web/cef/AcceleratedBrowserTexture.kt index d7bf31c..68f81a4 100644 --- a/src/main/kotlin/top/fpsmaster/web/cef/AcceleratedBrowserTexture.kt +++ b/src/main/kotlin/top/fpsmaster/web/cef/AcceleratedBrowserTexture.kt @@ -1,14 +1,237 @@ package top.fpsmaster.web.cef -// 26.2 defers zero-copy accelerated web-UI texture wrapping: TextureFormat→GpuFormat and the -// GlTexture constructor signature changed. Stub reports unsupported so the CEF bridge never takes the -// accelerated path on 26.2. [[nova-mc26-unobfuscated-build]] +// 26.2 accelerated web-UI texture bridge. Three 26.2 obstacles, each solved here: +// (1) GlTexture's protected ctor is `(int usage, String, GpuFormat, int w, int h, int depthOrLayers, +// int mipLevels, int glId, FrameBufferCache)` — usage FIRST, the wrapped GL id 8th. The +// FrameBufferCache comes reflectively off the GpuDevice's package-private GlDevice backend. +// We wrap mcef's imported external GL id with it as a copy SOURCE. +// (2) 26.2's deferred GUI renderer only samples textures the DEVICE created — an externally-wrapped id +// records into the render state but draws blank. So we GPU-copy the imported frame into a device +// texture (glCopyImageSubData, not the device copyTextureToTexture whose FBO blit rejects the +// memory-object source) and draw that. The CPU never touches the pixels. +// (3) The fixed RGBA blit has no swizzle, so a GL texture swizzle on the destination reads CEF's BGRA +// back as RGBA (shader-free). +// Frame delivery requires the CEF message loop to be pumped ON THE RENDER THREAD +// (-Dmcef.pumpOnRenderThread=true, set by Client.configureHost when hardware-acceleration is enabled); +// with mcef's dedicated CEF thread, CEF stops after a single accelerated frame. Zero-copy stays +// opt-in via the hardware-acceleration setting. +// [[nova-mc26-unobfuscated-build]] //? if >=26 { -/*class AcceleratedBrowserTexture { - fun reset() {} +/*import com.mojang.blaze3d.GpuFormat +import com.mojang.blaze3d.opengl.FrameBufferCache +import com.mojang.blaze3d.opengl.GlStateManager +import com.mojang.blaze3d.systems.RenderSystem +import com.mojang.blaze3d.textures.FilterMode +import com.mojang.blaze3d.textures.GpuSampler +import com.mojang.blaze3d.textures.GpuTexture +import com.mojang.blaze3d.textures.GpuTextureView +import org.lwjgl.opengl.GL11 +import org.lwjgl.opengl.GL13 +import org.lwjgl.opengl.GL33 +import org.lwjgl.opengl.GL43 +import top.fpsmaster.logger +import java.lang.reflect.Constructor +import java.util.function.Supplier + +/** + * Draws mcef-nova's ZERO-COPY imported CEF GPU texture on 26.2 by GPU-copying it into a device-owned + * texture the deferred GUI blit can sample (see the file header for why a straight wrap draws blank). + * + * The imported id is mcef's — the [com.mojang.blaze3d.opengl.GlTexture] we wrap it in as the copy source + * MUST NEVER be closed (that would glDeleteTextures an id mcef still manages). Only our destination texture + * is ours to [close]. If wrapping ever fails on an unexpected blaze3d layout, [isSupported] flips false and + * the caller falls back to the CPU path. + */ +class AcceleratedBrowserTexture { + + private var srcTexId = 0 + private var srcTexture: GpuTexture? = null + private var dstTexture: GpuTexture? = null + private var dstView: GpuTextureView? = null + private var dstWidth = 0 + private var dstHeight = 0 + + /** Clamp-to-edge linear sampler (SamplerCache dedupes, so this getter is cheap per frame). */ + val sampler: GpuSampler + get() = RenderSystem.getSamplerCache().getClampToEdge(FilterMode.LINEAR, false) + + /** + * GPU-copy CEF's imported [texId] frame ([width]x[height]) into our device-owned texture and return + * its view. Returns null if wrapping isn't possible. + * + * Why a copy and not a straight wrap-and-draw: 26.2's deferred GUI renderer only samples textures the + * device created/owns — an externally-wrapped GL id (mcef's imported texture) records into the render + * state but draws blank at flush. So we wrap the external id as the copy SOURCE and blit it, GPU-side, + * into a device texture the deferred blit can sample. The CPU never touches the pixels; the CEF→GL + * import stays zero-copy and the BGRA→RGBA swap is a free GL texture swizzle on the destination. + */ + fun setup(texId: Int, width: Int, height: Int): GpuTextureView? { + if (texId == 0 || width <= 0 || height <= 0 || !isSupported) { + return null + } + return try { + copyIntoOwnedTexture(texId, width, height) + } catch (t: Throwable) { + logger.error("Failed to draw accelerated browser texture (id={}, {}x{})", texId, width, height, t) + supported = false + null + } + } + + private fun copyIntoOwnedTexture(texId: Int, width: Int, height: Int): GpuTextureView { + val device = RenderSystem.getDevice() + // Wrap mcef's imported external GL id as a GlTexture (copy SOURCE); cache while the id is unchanged. + if (texId != srcTexId || srcTexture == null) { + srcTexture = wrapExternalTexture(texId, width, height) + srcTexId = texId + } + // Device-owned destination the deferred GUI blit can actually sample; recreate on size change. Its + // GL swizzle reads CEF's BGRA bytes back as RGBA at sample time (no CPU swizzle, no shader). + if (dstTexture == null || dstWidth != width || dstHeight != height) { + close() + val created = device.createTexture( + Supplier { "FPSMaster browser accel" }, + GpuTexture.USAGE_TEXTURE_BINDING or GpuTexture.USAGE_COPY_DST, + GpuFormat.RGBA8_UNORM, width, height, 1, 1 + ) + dstTexture = created + dstView = device.createTextureView(created) + dstWidth = width + dstHeight = height + applyBgraSwizzle(created) + } + // Direct GPU image copy of CEF's imported frame into our texture. We use glCopyImageSubData (not + // the device's copyTextureToTexture, which drives an FBO blit — the imported memory-object texture + // can't be an FBO colour attachment, giving GL_INVALID_FRAMEBUFFER_OPERATION). Image copy needs no + // FBO and no bind, and the CPU never touches the pixels. + // Raw OpenGL reports most failures through its error flag instead of throwing. Isolate this + // call from any older error and turn a failed copy into an exception so setup() disables the + // accelerated path; BasicBrowser will recreate a software-rendered browser on the next frame. + while (GL11.glGetError() != GL11.GL_NO_ERROR) { + // Drain stale errors. + } + GL43.glCopyImageSubData( + glId(srcTexture!!), GL11.GL_TEXTURE_2D, 0, 0, 0, 0, + glId(dstTexture!!), GL11.GL_TEXTURE_2D, 0, 0, 0, 0, + width, height, 1 + ) + val copyError = GL11.glGetError() + check(copyError == GL11.GL_NO_ERROR) { + "glCopyImageSubData failed with GL error 0x${copyError.toString(16)}" + } + return dstView!! + } + + private fun wrapExternalTexture(texId: Int, width: Int, height: Int): GpuTexture { + // GlTexture(int usage, String label, GpuFormat, int width, int height, int depthOrLayers, + // int mipLevels, int glId, FrameBufferCache) — usage FIRST, the existing GL id 8th + // (matches LiquidBounce's DirectGlTexture super() call). Wraps the id, no glGen. + // frameBufferCache() lives on the package-private GlDevice backend; reach it reflectively. + return glTextureCtor().newInstance( + GpuTexture.USAGE_TEXTURE_BINDING or GpuTexture.USAGE_COPY_SRC, + "FPSMaster browser accel src", + GpuFormat.RGBA8_UNORM, + width, + height, + 1, + 1, + texId, + frameBufferCache() + ) as GpuTexture + } + + /** + * Tear down our device-owned copy texture. NEVER touches the source GL id — that's mcef's (closing it + * would glDeleteTextures an id mcef still manages). Called on size change and browser teardown. + */ + fun close() { + dstView?.close() + dstView = null + dstTexture?.close() + dstTexture = null + dstWidth = 0 + dstHeight = 0 + } + + /** Browser teardown: drop our copy texture and forget the external id. */ + fun reset() { + close() + srcTexId = 0 + srcTexture = null + } + + /** + * Swap the destination texture's R/B channels via a GL texture swizzle so CEF's BGRA copy samples as + * RGBA — the free, shader-less BGRA fix (the fixed GUI blit has no swizzle). glId() is public but its + * declaring class (GlTexture) is package-private, so the reflective invoke needs setAccessible. + */ + private fun applyBgraSwizzle(texture: GpuTexture) { + GlStateManager._activeTexture(GL13.GL_TEXTURE0) + GlStateManager._bindTexture(glId(texture)) + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL33.GL_TEXTURE_SWIZZLE_R, GL11.GL_BLUE) + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL33.GL_TEXTURE_SWIZZLE_B, GL11.GL_RED) + } + companion object { - val isSupported: Boolean get() = false - fun probe(): Boolean = false + @Volatile + private var supported = true + + /** GlTexture.glId() is public but its declaring class is package-private — invoke needs access. */ + private fun glId(texture: GpuTexture): Int = + texture.javaClass.getMethod("glId").apply { isAccessible = true }.invoke(texture) as Int + + /** Whether external-GL-texture wrapping is available (false once a build attempt fails). */ + val isSupported: Boolean + get() = supported + + private var ctorCache: Constructor<*>? = null + + /** The GlDevice backend's [FrameBufferCache], read from GpuDevice's private `backend` field. */ + private fun frameBufferCache(): FrameBufferCache { + val device = RenderSystem.getDevice() + val backendField = device.javaClass.getDeclaredField("backend") + backendField.isAccessible = true + val backend = backendField.get(device) + // frameBufferCache() is public but its declaring class (GlDevice) is package-private, so + // reflective invoke from here needs setAccessible to pass the access check. + val method = backend.javaClass.getMethod("frameBufferCache").apply { isAccessible = true } + return method.invoke(backend) as FrameBufferCache + } + + private fun glTextureCtor(): Constructor<*> { + ctorCache?.let { return it } + val cls = Class.forName("com.mojang.blaze3d.opengl.GlTexture") + val ctor = cls.getDeclaredConstructor( + Int::class.javaPrimitiveType, + String::class.java, + GpuFormat::class.java, + Int::class.javaPrimitiveType, + Int::class.javaPrimitiveType, + Int::class.javaPrimitiveType, + Int::class.javaPrimitiveType, + Int::class.javaPrimitiveType, + FrameBufferCache::class.java + ) + ctor.isAccessible = true + ctorCache = ctor + return ctor + } + + /** + * Probe once whether the external-id GlTexture wrapping constructor exists. Called during + * acceleration gating so a blaze3d layout we can't wrap disables zero-copy up front (CPU path). + */ + fun probe(): Boolean { + if (!supported) return false + return try { + glTextureCtor() + true + } catch (t: Throwable) { + logger.warn("Accelerated browser texture wrapping unavailable; GPU zero-copy disabled: {}", t.toString()) + supported = false + false + } + } } } *///?} @@ -68,17 +291,19 @@ class AcceleratedBrowserTexture { } private fun buildSetup(texId: Int, width: Int, height: Int): TextureSetup { - // GlTexture(int glId, String label, TextureFormat format, int width, int height, - // int depthOrLayers, int mipLevels, int usage) — wraps an EXISTING id (no glGen). + // GlTexture(int usage, String label, TextureFormat format, int width, int height, + // int depthOrLayers, int mipLevels, int glId) — usage FIRST, the existing GL id + // LAST (verified against the GlTexture constructor bytecode on 1.21.8/1.21.11: arg 1 goes to + // GpuTexture.usage, arg 8 to GlTexture.id). Wraps the id, no glGen. val glTexture = glTextureCtor().newInstance( - texId, + GpuTexture.USAGE_TEXTURE_BINDING, "FPSMaster browser accel", TextureFormat.RGBA8, width, height, 1, 1, - GpuTexture.USAGE_TEXTURE_BINDING + texId ) as GpuTexture // Reflect GlTextureView directly (rather than device.createTextureView) so nothing on the GL diff --git a/src/main/kotlin/top/fpsmaster/web/cef/BrowserOwnedTexture.kt b/src/main/kotlin/top/fpsmaster/web/cef/BrowserOwnedTexture.kt index 043a4a2..880c259 100644 --- a/src/main/kotlin/top/fpsmaster/web/cef/BrowserOwnedTexture.kt +++ b/src/main/kotlin/top/fpsmaster/web/cef/BrowserOwnedTexture.kt @@ -55,6 +55,10 @@ class BrowserOwnedTexture : AutoCloseable { dst = ByteBuffer.allocateDirect(pixels) scratch = dst } + // Reset limit to capacity: the previous upload shrank it to that frame's size (below), and a + // grow-after-shrink resize would otherwise fail every absolute put past the stale limit + // (IndexOutOfBounds each frame -> permanently black webview after window resizes). + dst.clear() // Absolute get/put so neither buffer's position is disturbed (CEF buffer starts at 0). var i = 0 while (i < pixels) { diff --git a/src/main/kotlin/top/fpsmaster/web/cef/ClientBrowser.kt b/src/main/kotlin/top/fpsmaster/web/cef/ClientBrowser.kt index 9b8536c..fc726f4 100644 --- a/src/main/kotlin/top/fpsmaster/web/cef/ClientBrowser.kt +++ b/src/main/kotlin/top/fpsmaster/web/cef/ClientBrowser.kt @@ -220,13 +220,23 @@ class ClientBrowser( navigationPending = false } - // 26.2 deferred-render draw: record the CPU-uploaded browser frame as a textured quad straight - // into the GUI render state via GuiGraphicsExtractor.blit(view, sampler, …). The old submit-node - // path (IGuiGraphics/GuiRenderState.submitGuiElement/TexQuad) is gone; the accelerated zero-copy - // path is deferred (accelerate is hardcoded false, so useAccelerated is always false here — mcef - // does software rendering and BrowserOwnedTexture already swizzled BGRA→RGBA on upload). + // 26.2 deferred-render draw via GuiGraphicsExtractor.blit(view, sampler, …) (the old submit-node + // path IGuiGraphics/GuiRenderState.submitGuiElement/TexQuad is gone). Accelerated frames arrive as + // a GL texture id from mcef; AcceleratedBrowserTexture copies it into a device texture (with a BGRA + // swizzle) the deferred blit can sample. Otherwise draw the CPU-uploaded owned texture. //? if >=26 { - /*if (ownedTexture.ready) { + /*if (useAccelerated) { + val accelView = acceleratedTexture.setup( + accelTexId, browser.displayedAcceleratedWidth, browser.displayedAcceleratedHeight + ) + if (accelView != null) { + // CEF's GPU shared texture is top-down, same as the software-paint texture — draw it + // with the same UVs as the CPU path. + guiGraphics.delegate.blit(accelView, acceleratedTexture.sampler, 0, 0, width, height, 0f, 1f, 0f, 1f) + return + } + } + if (ownedTexture.ready) { guiGraphics.delegate.blit( ownedTexture.view!!, ownedTexture.sampler, 0, 0, width, height, From b046e733af71c338cb2b79e1c06caa1fddd61609 Mon Sep 17 00:00:00 2001 From: gaoyu06 Date: Fri, 16 Oct 2026 22:21:57 -0700 Subject: [PATCH 4/4] ci: mcef-nova 1.0.2 is released; drop the pending-release TODO --- .github/workflows/ci-release.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index e7dad07..f3cc527 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -177,9 +177,8 @@ jobs: env: # Lets Gradle resolve the private mcef-nova artifact from GitHub Packages. Works once the # package grants read access to this repo; until then the vendor/maven fallback covers it. - # TODO(mcef 1.0.2): GitHub Packages only has 1.0.2-SNAPSHOT so far. vendor/maven carries a - # local build of mcef-nova release/1.0.2 @ f0c9055 (unpushed) until the release is published; - # once tag v1.0.2 exists, record it here and replace the vendored copy with the published one. + # mcef-nova v1.0.2 (tag v1.0.2, published to GitHub Packages). The vendor/maven copy has the + # same classes as a build of that tag. GITHUB_ACTOR: ${{ github.actor }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION_NAME: ${{ needs.decide.outputs.version_name }}