From c45fcf30cfab687004ed1cdc06ebaa21f4262a0b Mon Sep 17 00:00:00 2001 From: Vittorio Giovara Date: Wed, 26 Aug 2015 11:31:41 +0200 Subject: [PATCH] DXV decoder Support all DXDI and DXD3 normal quality videos. --- Changelog | 1 + configure | 1 + doc/general.texi | 1 + libavcodec/Makefile | 1 + libavcodec/allcodecs.c | 1 + libavcodec/avcodec.h | 1 + libavcodec/dxv.c | 461 +++++++++++++++++++++++++++++++++++++++ libavcodec/version.h | 2 +- libavformat/isom.c | 3 + tests/fate/video.mak | 15 ++ tests/ref/fate/dxv-dxt1 | 2 + tests/ref/fate/dxv-dxt5 | 2 + tests/ref/fate/dxv3-dxt1 | 2 + tests/ref/fate/dxv3-dxt5 | 2 + 14 files changed, 494 insertions(+), 1 deletion(-) create mode 100644 libavcodec/dxv.c create mode 100644 tests/ref/fate/dxv-dxt1 create mode 100644 tests/ref/fate/dxv-dxt5 create mode 100644 tests/ref/fate/dxv3-dxt1 create mode 100644 tests/ref/fate/dxv3-dxt5 diff --git a/Changelog b/Changelog index 20944c26fe..461c5b1204 100644 --- a/Changelog +++ b/Changelog @@ -42,6 +42,7 @@ version : - bitstream filter for converting HEVC from MP4 to Annex B - Intel QSV-accelerated MPEG-2 video and HEVC decoding - Support DNx100 (1440x1080@8) +- DXV decoding version 11: diff --git a/configure b/configure index 9e83285f2c..852b931115 100755 --- a/configure +++ b/configure @@ -1841,6 +1841,7 @@ dnxhd_encoder_select="aandcttables blockdsp fdctdsp idctdsp mpegvideoenc pixbloc dvvideo_decoder_select="dvprofile idctdsp" dvvideo_encoder_select="dvprofile fdctdsp me_cmp pixblockdsp" dxa_decoder_deps="zlib" +dxv_decoder_select="lzf texturedsp" eac3_decoder_select="ac3_decoder" eac3_encoder_select="ac3_encoder" eamad_decoder_select="aandcttables blockdsp bswapdsp idctdsp mpegvideo" diff --git a/doc/general.texi b/doc/general.texi index a6ee1f71d9..7cd76933a0 100644 --- a/doc/general.texi +++ b/doc/general.texi @@ -399,6 +399,7 @@ library: @item RealMedia @tab X @tab X @item Redirector @tab @tab X @item Renderware TeXture Dictionary @tab @tab X +@item Resolume DXV @tab @tab X @item RL2 @tab @tab X @tab Audio and video format used in some games by Entertainment Software Partners. @item RPL/ARMovie @tab @tab X diff --git a/libavcodec/Makefile b/libavcodec/Makefile index 534fe4dfb0..7b105ccdfa 100644 --- a/libavcodec/Makefile +++ b/libavcodec/Makefile @@ -196,6 +196,7 @@ OBJS-$(CONFIG_DVVIDEO_DECODER) += dvdec.o dv.o dvdata.o OBJS-$(CONFIG_DVVIDEO_ENCODER) += dvenc.o dv.o dvdata.o OBJS-$(CONFIG_DXA_DECODER) += dxa.o OBJS-$(CONFIG_DXTORY_DECODER) += dxtory.o +OBJS-$(CONFIG_DXV_DECODER) += dxv.o OBJS-$(CONFIG_EAC3_DECODER) += eac3dec.o eac3_data.o OBJS-$(CONFIG_EAC3_ENCODER) += eac3enc.o eac3_data.o OBJS-$(CONFIG_EACMV_DECODER) += eacmv.o diff --git a/libavcodec/allcodecs.c b/libavcodec/allcodecs.c index 69790d6c77..49f5a70f4c 100644 --- a/libavcodec/allcodecs.c +++ b/libavcodec/allcodecs.c @@ -142,6 +142,7 @@ void avcodec_register_all(void) REGISTER_ENCDEC (DVVIDEO, dvvideo); REGISTER_DECODER(DXA, dxa); REGISTER_DECODER(DXTORY, dxtory); + REGISTER_DECODER(DXV, dxv); REGISTER_DECODER(EACMV, eacmv); REGISTER_DECODER(EAMAD, eamad); REGISTER_DECODER(EATGQ, eatgq); diff --git a/libavcodec/avcodec.h b/libavcodec/avcodec.h index 379311f29b..9b386551c9 100644 --- a/libavcodec/avcodec.h +++ b/libavcodec/avcodec.h @@ -298,6 +298,7 @@ enum AVCodecID { AV_CODEC_ID_HQ_HQA, AV_CODEC_ID_HAP, AV_CODEC_ID_DDS, + AV_CODEC_ID_DXV, /* various PCM "codecs" */ AV_CODEC_ID_FIRST_AUDIO = 0x10000, ///< A dummy id pointing at the start of audio codecs diff --git a/libavcodec/dxv.c b/libavcodec/dxv.c new file mode 100644 index 0000000000..dc11f38def --- /dev/null +++ b/libavcodec/dxv.c @@ -0,0 +1,461 @@ +/* + * Resolume DXV decoder + * Copyright (C) 2015 Vittorio Giovara + * + * This file is part of Libav. + * + * Libav is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * Libav is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Libav; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include + +#include "libavutil/imgutils.h" + +#include "avcodec.h" +#include "bytestream.h" +#include "internal.h" +#include "lzf.h" +#include "texturedsp.h" +#include "thread.h" + +typedef struct DXVContext { + TextureDSPContext texdsp; + GetByteContext gbc; + + uint8_t *tex_data; // Compressed texture + int tex_rat; // Compression ratio + int tex_step; // Distance between blocks + int64_t tex_size; // Texture size + + /* Optimal number of slices for parallel decoding */ + int slice_count; + + /* Pointer to the selected decompression function */ + int (*tex_funct)(uint8_t *dst, ptrdiff_t stride, const uint8_t *block); +} DXVContext; + +static int decompress_texture_thread(AVCodecContext *avctx, void *arg, + int slice, int thread_nb) +{ + DXVContext *ctx = avctx->priv_data; + AVFrame *frame = arg; + const uint8_t *d = ctx->tex_data; + int w_block = avctx->coded_width / TEXTURE_BLOCK_W; + int h_block = avctx->coded_height / TEXTURE_BLOCK_H; + int x, y; + int start_slice, end_slice; + int base_blocks_per_slice = h_block / ctx->slice_count; + int remainder_blocks = h_block % ctx->slice_count; + + /* When the frame height (in blocks) doesn't divide evenly between the + * number of slices, spread the remaining blocks evenly between the first + * operations */ + start_slice = slice * base_blocks_per_slice; + /* Add any extra blocks (one per slice) that have been added + * before this slice */ + start_slice += FFMIN(slice, remainder_blocks); + + end_slice = start_slice + base_blocks_per_slice; + /* Add an extra block if there are remainder blocks to be accounted for */ + if (slice < remainder_blocks) + end_slice++; + + for (y = start_slice; y < end_slice; y++) { + uint8_t *p = frame->data[0] + y * frame->linesize[0] * TEXTURE_BLOCK_H; + int off = y * w_block; + for (x = 0; x < w_block; x++) { + ctx->tex_funct(p + x * 16, frame->linesize[0], + d + (off + x) * ctx->tex_step); + } + } + + return 0; +} + +/* This scheme addresses already decoded elements depending on 2-bit status: + * 0 -> copy new element + * 1 -> copy one element from position -x + * 2 -> copy one element from position -(get_byte() + 2) * x + * 3 -> copy one element from position -(get_16le() + 0x102) * x + * x is always 2 for dxt1 and 4 for dxt5. */ +#define CHECKPOINT(x) \ + do { \ + if (state == 0) { \ + value = bytestream2_get_le32(gbc); \ + state = 16; \ + } \ + op = value & 0x3; \ + value >>= 2; \ + state--; \ + switch (op) { \ + case 1: \ + idx = x; \ + break; \ + case 2: \ + idx = (bytestream2_get_byte(gbc) + 2) * x; \ + break; \ + case 3: \ + idx = (bytestream2_get_le16(gbc) + 0x102) * x; \ + break; \ + } \ + } while(0) + +static int dxv_decompress_dxt1(AVCodecContext *avctx) +{ + DXVContext *ctx = avctx->priv_data; + GetByteContext *gbc = &ctx->gbc; + uint32_t value, prev, op; + int idx = 0, state = 0; + int pos = 2; + + /* Copy the first two elements */ + AV_WL32(ctx->tex_data, bytestream2_get_le32(gbc)); + AV_WL32(ctx->tex_data + 4, bytestream2_get_le32(gbc)); + + /* Process input until the whole texture has been filled */ + while (pos < ctx->tex_size / 4) { + CHECKPOINT(2); + + /* Copy two elements from a previous offset or from the input buffer */ + if (op) { + prev = AV_RL32(ctx->tex_data + 4 * (pos - idx)); + AV_WL32(ctx->tex_data + 4 * pos, prev); + pos++; + + prev = AV_RL32(ctx->tex_data + 4 * (pos - idx)); + AV_WL32(ctx->tex_data + 4 * pos, prev); + pos++; + } else { + CHECKPOINT(2); + + if (op) + prev = AV_RL32(ctx->tex_data + 4 * (pos - idx)); + else + prev = bytestream2_get_le32(gbc); + AV_WL32(ctx->tex_data + 4 * pos, prev); + pos++; + + CHECKPOINT(2); + + if (op) + prev = AV_RL32(ctx->tex_data + 4 * (pos - idx)); + else + prev = bytestream2_get_le32(gbc); + AV_WL32(ctx->tex_data + 4 * pos, prev); + pos++; + } + } + + return 0; +} + +static int dxv_decompress_dxt5(AVCodecContext *avctx) +{ + DXVContext *ctx = avctx->priv_data; + GetByteContext *gbc = &ctx->gbc; + uint32_t value, op; + int idx, prev, state = 0; + int pos = 4; + int run = 0; + int probe, check; + + /* Copy the first four elements */ + AV_WL32(ctx->tex_data + 0, bytestream2_get_le32(gbc)); + AV_WL32(ctx->tex_data + 4, bytestream2_get_le32(gbc)); + AV_WL32(ctx->tex_data + 8, bytestream2_get_le32(gbc)); + AV_WL32(ctx->tex_data + 12, bytestream2_get_le32(gbc)); + + /* Process input until the whole texture has been filled */ + while (pos < ctx->tex_size / 4) { + if (run) { + run--; + + prev = AV_RL32(ctx->tex_data + 4 * (pos - 4)); + AV_WL32(ctx->tex_data + 4 * pos, prev); + pos++; + prev = AV_RL32(ctx->tex_data + 4 * (pos - 4)); + AV_WL32(ctx->tex_data + 4 * pos, prev); + pos++; + } else { + if (state == 0) { + value = bytestream2_get_le32(gbc); + state = 16; + } + op = value & 0x3; + value >>= 2; + state--; + + switch (op) { + case 0: + /* Long copy */ + check = bytestream2_get_byte(gbc) + 1; + if (check == 256) { + do { + probe = bytestream2_get_le16(gbc); + check += probe; + } while (probe == 0xFFFF); + } + while (check && pos < ctx->tex_size / 4) { + prev = AV_RL32(ctx->tex_data + 4 * (pos - 4)); + AV_WL32(ctx->tex_data + 4 * pos, prev); + pos++; + + prev = AV_RL32(ctx->tex_data + 4 * (pos - 4)); + AV_WL32(ctx->tex_data + 4 * pos, prev); + pos++; + + prev = AV_RL32(ctx->tex_data + 4 * (pos - 4)); + AV_WL32(ctx->tex_data + 4 * pos, prev); + pos++; + + prev = AV_RL32(ctx->tex_data + 4 * (pos - 4)); + AV_WL32(ctx->tex_data + 4 * pos, prev); + pos++; + + check--; + } + + /* Restart (or exit) the loop */ + continue; + break; + case 1: + /* Load new run value */ + run = bytestream2_get_byte(gbc); + if (run == 255) { + do { + probe = bytestream2_get_le16(gbc); + run += probe; + } while (probe == 0xFFFF); + } + + /* Copy two dwords from previous data */ + prev = AV_RL32(ctx->tex_data + 4 * (pos - 4)); + AV_WL32(ctx->tex_data + 4 * pos, prev); + pos++; + + prev = AV_RL32(ctx->tex_data + 4 * (pos - 4)); + AV_WL32(ctx->tex_data + 4 * pos, prev); + pos++; + break; + case 2: + /* Copy two dwords from a previous index */ + idx = 8 + bytestream2_get_le16(gbc); + prev = AV_RL32(ctx->tex_data + 4 * (pos - idx)); + AV_WL32(ctx->tex_data + 4 * pos, prev); + pos++; + + prev = AV_RL32(ctx->tex_data + 4 * (pos - idx)); + AV_WL32(ctx->tex_data + 4 * pos, prev); + pos++; + break; + case 3: + /* Copy two dwords from input */ + prev = bytestream2_get_le32(gbc); + AV_WL32(ctx->tex_data + 4 * pos, prev); + pos++; + + prev = bytestream2_get_le32(gbc); + AV_WL32(ctx->tex_data + 4 * pos, prev); + pos++; + break; + } + } + + CHECKPOINT(4); + + /* Copy two elements from a previous offset or from the input buffer */ + if (op) { + prev = AV_RL32(ctx->tex_data + 4 * (pos - idx)); + AV_WL32(ctx->tex_data + 4 * pos, prev); + pos++; + + prev = AV_RL32(ctx->tex_data + 4 * (pos - idx)); + AV_WL32(ctx->tex_data + 4 * pos, prev); + pos++; + } else { + CHECKPOINT(4); + + if (op) + prev = AV_RL32(ctx->tex_data + 4 * (pos - idx)); + else + prev = bytestream2_get_le32(gbc); + AV_WL32(ctx->tex_data + 4 * pos, prev); + pos++; + + CHECKPOINT(4); + + if (op) + prev = AV_RL32(ctx->tex_data + 4 * (pos - idx)); + else + prev = bytestream2_get_le32(gbc); + AV_WL32(ctx->tex_data + 4 * pos, prev); + pos++; + } + } + + return 0; +} + +static int dxv_decompress_lzf(AVCodecContext *avctx) +{ + DXVContext *ctx = avctx->priv_data; + return ff_lzf_uncompress(&ctx->gbc, &ctx->tex_data, &ctx->tex_size); +} + +static int dxv_decode(AVCodecContext *avctx, void *data, + int *got_frame, AVPacket *avpkt) +{ + DXVContext *ctx = avctx->priv_data; + ThreadFrame tframe; + GetByteContext *gbc = &ctx->gbc; + int (*decompress_tex)(AVCodecContext *avctx); + uint32_t tag; + int channels, size = 0, old_type = 0; + int ret; + + bytestream2_init(gbc, avpkt->data, avpkt->size); + + tag = bytestream2_get_le32(gbc); + switch (tag) { + case MKBETAG('D', 'X', 'T', '1'): + decompress_tex = dxv_decompress_dxt1; + ctx->tex_funct = ctx->texdsp.dxt1_block; + ctx->tex_rat = 8; + ctx->tex_step = 8; + av_log(avctx, AV_LOG_DEBUG, "DXTR1 compression and DXT1 texture "); + break; + case MKBETAG('D', 'X', 'T', '5'): + decompress_tex = dxv_decompress_dxt5; + ctx->tex_funct = ctx->texdsp.dxt5_block; + ctx->tex_rat = 4; + ctx->tex_step = 16; + av_log(avctx, AV_LOG_DEBUG, "DXTR5 compression and DXT5 texture "); + break; + case MKBETAG('Y', 'C', 'G', '6'): + case MKBETAG('Y', 'G', '1', '0'): + avpriv_report_missing_feature(avctx, "Tag 0x%08X", tag); + return AVERROR_PATCHWELCOME; + default: + /* Old version does not have a real header, just size and type. */ + size = tag & 0x00FFFFFF; + old_type = tag >> 24; + channels = old_type & 0x0F; + if (old_type & 0x40) { + av_log(avctx, AV_LOG_DEBUG, "LZF compression and DXT5 texture "); + ctx->tex_funct = ctx->texdsp.dxt5_block; + ctx->tex_step = 16; + } else if (old_type & 0x20) { + av_log(avctx, AV_LOG_DEBUG, "LZF compression and DXT1 texture "); + ctx->tex_funct = ctx->texdsp.dxt1_block; + ctx->tex_step = 8; + } else { + av_log(avctx, AV_LOG_ERROR, "Unsupported header (0x%08X)\n.", tag); + return AVERROR_INVALIDDATA; + } + decompress_tex = dxv_decompress_lzf; + ctx->tex_rat = 1; + break; + } + + /* New header is 12 bytes long. */ + if (!old_type) { + channels = bytestream2_get_byte(gbc); + bytestream2_skip(gbc, 3); // unknown + size = bytestream2_get_le32(gbc); + } + av_log(avctx, AV_LOG_DEBUG, "(%d channels)\n", channels); + + if (size != bytestream2_get_bytes_left(gbc)) { + av_log(avctx, AV_LOG_ERROR, "Incomplete or invalid file (%u > %u)\n.", + size, bytestream2_get_bytes_left(gbc)); + return AVERROR_INVALIDDATA; + } + + ctx->tex_size = avctx->coded_width * avctx->coded_height * 4 / ctx->tex_rat; + ret = av_reallocp(&ctx->tex_data, ctx->tex_size); + if (ret < 0) + return ret; + + /* Decompress texture out of the intermediate compression. */ + ret = decompress_tex(avctx); + if (ret < 0) + return ret; + + tframe.f = data; + ret = ff_thread_get_buffer(avctx, &tframe, 0); + if (ret < 0) + return ret; + ff_thread_finish_setup(avctx); + + /* Now decompress the texture with the standard functions. */ + avctx->execute2(avctx, decompress_texture_thread, + tframe.f, NULL, ctx->slice_count); + + /* Frame is ready to be output. */ + tframe.f->pict_type = AV_PICTURE_TYPE_I; + tframe.f->key_frame = 1; + *got_frame = 1; + + return avpkt->size; +} + +static int dxv_init(AVCodecContext *avctx) +{ + DXVContext *ctx = avctx->priv_data; + int ret = av_image_check_size(avctx->width, avctx->height, 0, avctx); + + if (ret < 0) { + av_log(avctx, AV_LOG_ERROR, "Invalid image size %dx%d.\n", + avctx->width, avctx->height); + return ret; + } + + /* Codec requires 16x16 alignment. */ + avctx->coded_width = FFALIGN(avctx->width, 16); + avctx->coded_height = FFALIGN(avctx->height, 16); + + ff_texturedsp_init(&ctx->texdsp); + avctx->pix_fmt = AV_PIX_FMT_RGBA; + + ctx->slice_count = av_clip(avctx->thread_count, 1, + avctx->coded_height / TEXTURE_BLOCK_H); + + return 0; +} + +static int dxv_close(AVCodecContext *avctx) +{ + DXVContext *ctx = avctx->priv_data; + + av_freep(&ctx->tex_data); + + return 0; +} + +AVCodec ff_dxv_decoder = { + .name = "dxv", + .long_name = NULL_IF_CONFIG_SMALL("Resolume DXV"), + .type = AVMEDIA_TYPE_VIDEO, + .id = AV_CODEC_ID_DXV, + .init = dxv_init, + .decode = dxv_decode, + .close = dxv_close, + .priv_data_size = sizeof(DXVContext), + .capabilities = AV_CODEC_CAP_DR1 | + AV_CODEC_CAP_SLICE_THREADS | + AV_CODEC_CAP_FRAME_THREADS, + .caps_internal = FF_CODEC_CAP_INIT_THREADSAFE | + FF_CODEC_CAP_INIT_CLEANUP, +}; diff --git a/libavcodec/version.h b/libavcodec/version.h index 8dfa73558c..eefc3a93d1 100644 --- a/libavcodec/version.h +++ b/libavcodec/version.h @@ -29,7 +29,7 @@ #include "libavutil/version.h" #define LIBAVCODEC_VERSION_MAJOR 57 -#define LIBAVCODEC_VERSION_MINOR 0 +#define LIBAVCODEC_VERSION_MINOR 1 #define LIBAVCODEC_VERSION_MICRO 0 #define LIBAVCODEC_VERSION_INT AV_VERSION_INT(LIBAVCODEC_VERSION_MAJOR, \ diff --git a/libavformat/isom.c b/libavformat/isom.c index 0582cfdac4..d421a1a4bd 100644 --- a/libavformat/isom.c +++ b/libavformat/isom.c @@ -254,6 +254,9 @@ const AVCodecTag ff_codec_movvideo_tags[] = { { AV_CODEC_ID_HAP, MKTAG('H', 'a', 'p', '5') }, { AV_CODEC_ID_HAP, MKTAG('H', 'a', 'p', 'Y') }, + { AV_CODEC_ID_DXV, MKTAG('D', 'X', 'D', '3') }, + { AV_CODEC_ID_DXV, MKTAG('D', 'X', 'D', 'I') }, + { AV_CODEC_ID_NONE, 0 }, }; diff --git a/tests/fate/video.mak b/tests/fate/video.mak index 77cc9e7c6a..ef1d41da2f 100644 --- a/tests/fate/video.mak +++ b/tests/fate/video.mak @@ -136,6 +136,21 @@ fate-dxa-scummvm: CMD = framecrc -i $(TARGET_SAMPLES)/dxa/scummvm.dxa -pix_fmt r FATE_SAMPLES_AVCONV-$(call DEMDEC, DXA, DXA) += $(FATE_DXA) fate-dxa: $(FATE_DXA) +FATE_DXV += fate-dxv-dxt1 +fate-dxv-dxt1: CMD = framecrc -i $(TARGET_SAMPLES)/dxv/dxv-na.mov + +FATE_DXV += fate-dxv-dxt5 +fate-dxv-dxt5: CMD = framecrc -i $(TARGET_SAMPLES)/dxv/dxv-wa.mov + +FATE_DXV += fate-dxv3-dxt1 +fate-dxv3-dxt1: CMD = framecrc -i $(TARGET_SAMPLES)/dxv/dxv3-nqna.mov + +FATE_DXV += fate-dxv3-dxt5 +fate-dxv3-dxt5: CMD = framecrc -i $(TARGET_SAMPLES)/dxv/dxv3-nqwa.mov + +FATE_SAMPLES_AVCONV-$(call DEMDEC, MOV, DXV) += $(FATE_DXV) +fate-dxv: $(FATE_DXV) + FATE_SAMPLES_AVCONV-$(call DEMDEC, SEGAFILM, CINEPAK) += fate-film-cvid fate-film-cvid: CMD = framecrc -i $(TARGET_SAMPLES)/film/logo-capcom.cpk -an diff --git a/tests/ref/fate/dxv-dxt1 b/tests/ref/fate/dxv-dxt1 new file mode 100644 index 0000000000..9b493807ca --- /dev/null +++ b/tests/ref/fate/dxv-dxt1 @@ -0,0 +1,2 @@ +#tb 0: 1/30000 +0, 0, 0, 0, 8294400, 0x0797cd53 diff --git a/tests/ref/fate/dxv-dxt5 b/tests/ref/fate/dxv-dxt5 new file mode 100644 index 0000000000..9b493807ca --- /dev/null +++ b/tests/ref/fate/dxv-dxt5 @@ -0,0 +1,2 @@ +#tb 0: 1/30000 +0, 0, 0, 0, 8294400, 0x0797cd53 diff --git a/tests/ref/fate/dxv3-dxt1 b/tests/ref/fate/dxv3-dxt1 new file mode 100644 index 0000000000..c65ead9124 --- /dev/null +++ b/tests/ref/fate/dxv3-dxt1 @@ -0,0 +1,2 @@ +#tb 0: 1/30000 +0, 0, 0, 0, 8294400, 0x98bbcc85 diff --git a/tests/ref/fate/dxv3-dxt5 b/tests/ref/fate/dxv3-dxt5 new file mode 100644 index 0000000000..9b493807ca --- /dev/null +++ b/tests/ref/fate/dxv3-dxt5 @@ -0,0 +1,2 @@ +#tb 0: 1/30000 +0, 0, 0, 0, 8294400, 0x0797cd53