mirror of
https://github.com/RPCS3/rpcs3.git
synced 2025-04-20 11:36:13 +00:00
It can play video, but it can't
I don't know how to disable aggressive data caching that occures in vdecRead(). Also ReleaseAu function is disabled because it breaks everything.
This commit is contained in:
parent
c064c701e2
commit
8a4c67deab
7 changed files with 630 additions and 135 deletions
|
@ -3,6 +3,8 @@
|
|||
#include "Emu/SysCalls/SC_FUNC.h"
|
||||
#include "cellPamf.h"
|
||||
|
||||
extern SMutexGeneral g_mutex_avcodec_open2;
|
||||
|
||||
extern "C"
|
||||
{
|
||||
#include "libavcodec\avcodec.h"
|
||||
|
@ -14,6 +16,33 @@ extern "C"
|
|||
void cellAdec_init();
|
||||
Module cellAdec(0x0006, cellAdec_init);
|
||||
|
||||
int adecRead(void* opaque, u8* buf, int buf_size)
|
||||
{
|
||||
AudioDecoder& adec = *(AudioDecoder*)opaque;
|
||||
|
||||
if (adec.reader.size < (u32)buf_size)
|
||||
{
|
||||
buf_size = adec.reader.size;
|
||||
}
|
||||
|
||||
if (!buf_size)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
else if (!Memory.CopyToReal(buf, adec.reader.addr, buf_size))
|
||||
{
|
||||
ConLog.Error("adecRead: data reading failed (buf_size=0x%x)", buf_size);
|
||||
Emu.Pause();
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
adec.reader.addr += buf_size;
|
||||
adec.reader.size -= buf_size;
|
||||
return 0 + buf_size;
|
||||
}
|
||||
}
|
||||
|
||||
u32 adecOpen(AudioDecoder* data)
|
||||
{
|
||||
AudioDecoder& adec = *data;
|
||||
|
@ -56,16 +85,205 @@ u32 adecOpen(AudioDecoder* data)
|
|||
{
|
||||
case adecStartSeq:
|
||||
{
|
||||
// TODO: reset data
|
||||
ConLog.Warning("adecStartSeq:");
|
||||
|
||||
adec.reader.addr = 0;
|
||||
adec.reader.size = 0;
|
||||
adec.is_running = true;
|
||||
adec.just_started = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case adecEndSeq:
|
||||
{
|
||||
// TODO: finalize
|
||||
ConLog.Warning("adecEndSeq:");
|
||||
|
||||
Callback cb;
|
||||
cb.SetAddr(adec.cbFunc);
|
||||
cb.Handle(adec.id, CELL_ADEC_MSG_TYPE_SEQDONE, CELL_OK, adec.cbArg);
|
||||
cb.Branch(true); // ???
|
||||
|
||||
avcodec_close(adec.ctx);
|
||||
avformat_close_input(&adec.fmt);
|
||||
|
||||
adec.is_running = false;
|
||||
}
|
||||
break;
|
||||
|
||||
case adecDecodeAu:
|
||||
{
|
||||
int err;
|
||||
|
||||
adec.reader.addr = task.au.addr;
|
||||
adec.reader.size = task.au.size;
|
||||
|
||||
u64 last_pts = task.au.pts;
|
||||
|
||||
struct AVPacketHolder : AVPacket
|
||||
{
|
||||
AVPacketHolder(u32 size)
|
||||
{
|
||||
av_init_packet(this);
|
||||
|
||||
if (size)
|
||||
{
|
||||
data = (u8*)av_malloc(size + FF_INPUT_BUFFER_PADDING_SIZE);
|
||||
memset(data + size, 0, FF_INPUT_BUFFER_PADDING_SIZE);
|
||||
this->size = size + FF_INPUT_BUFFER_PADDING_SIZE;
|
||||
}
|
||||
else
|
||||
{
|
||||
data = NULL;
|
||||
size = 0;
|
||||
}
|
||||
}
|
||||
|
||||
~AVPacketHolder()
|
||||
{
|
||||
av_free(data);
|
||||
//av_free_packet(this);
|
||||
}
|
||||
|
||||
} au(0);
|
||||
|
||||
/*{
|
||||
wxFile dump;
|
||||
dump.Open(wxString::Format("audio pts-0x%llx.dump", task.au.pts), wxFile::write);
|
||||
u8* buf = (u8*)malloc(task.au.size);
|
||||
if (Memory.CopyToReal(buf, task.au.addr, task.au.size)) dump.Write(buf, task.au.size);
|
||||
free(buf);
|
||||
dump.Close();
|
||||
}
|
||||
|
||||
if (adec.just_started) // deferred initialization
|
||||
{
|
||||
err = avformat_open_input(&adec.fmt, NULL, NULL, NULL);
|
||||
if (err)
|
||||
{
|
||||
ConLog.Error("adecDecodeAu: avformat_open_input() failed");
|
||||
Emu.Pause();
|
||||
break;
|
||||
}
|
||||
err = avformat_find_stream_info(adec.fmt, NULL);
|
||||
if (err)
|
||||
{
|
||||
ConLog.Error("adecDecodeAu: avformat_find_stream_info() failed");
|
||||
Emu.Pause();
|
||||
break;
|
||||
}
|
||||
if (!adec.fmt->nb_streams)
|
||||
{
|
||||
ConLog.Error("adecDecodeAu: no stream found");
|
||||
Emu.Pause();
|
||||
break;
|
||||
}
|
||||
adec.ctx = adec.fmt->streams[0]->codec; // TODO: check data
|
||||
|
||||
AVCodec* codec = avcodec_find_decoder(adec.ctx->codec_id); // ???
|
||||
if (!codec)
|
||||
{
|
||||
ConLog.Error("adecDecodeAu: avcodec_find_decoder() failed");
|
||||
Emu.Pause();
|
||||
break;
|
||||
}
|
||||
|
||||
AVDictionary* opts;
|
||||
av_dict_set(&opts, "refcounted_frames", "1", 0);
|
||||
{
|
||||
SMutexGeneralLocker lock(g_mutex_avcodec_open2);
|
||||
// not multithread-safe
|
||||
err = avcodec_open2(adec.ctx, codec, &opts);
|
||||
}
|
||||
if (err)
|
||||
{
|
||||
ConLog.Error("adecDecodeAu: avcodec_open2() failed");
|
||||
Emu.Pause();
|
||||
break;
|
||||
}
|
||||
adec.just_started = false;
|
||||
}
|
||||
|
||||
while (av_read_frame(adec.fmt, &au) >= 0)*/ while (true)
|
||||
{
|
||||
if (!adec.ctx) // fake
|
||||
{
|
||||
AdecFrame frame;
|
||||
frame.pts = task.au.pts;
|
||||
frame.auAddr = task.au.addr;
|
||||
frame.auSize = task.au.size;
|
||||
frame.userdata = task.au.userdata;
|
||||
frame.size = 2048;
|
||||
frame.data = nullptr;
|
||||
adec.frames.Push(frame);
|
||||
|
||||
Callback cb;
|
||||
cb.SetAddr(adec.cbFunc);
|
||||
cb.Handle(adec.id, CELL_ADEC_MSG_TYPE_PCMOUT, CELL_OK, adec.cbArg);
|
||||
cb.Branch(false);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
struct VdecFrameHolder : AdecFrame
|
||||
{
|
||||
VdecFrameHolder()
|
||||
{
|
||||
data = av_frame_alloc();
|
||||
}
|
||||
|
||||
~VdecFrameHolder()
|
||||
{
|
||||
if (data)
|
||||
{
|
||||
av_frame_unref(data);
|
||||
av_frame_free(&data);
|
||||
}
|
||||
}
|
||||
|
||||
} frame;
|
||||
|
||||
if (!frame.data)
|
||||
{
|
||||
ConLog.Error("adecDecodeAu: av_frame_alloc() failed");
|
||||
Emu.Pause();
|
||||
break;
|
||||
}
|
||||
|
||||
int got_frame = 0;
|
||||
|
||||
int decode = avcodec_decode_audio4(adec.ctx, frame.data, &got_frame, &au);
|
||||
|
||||
if (decode < 0)
|
||||
{
|
||||
ConLog.Error("adecDecodeAu: AU decoding error(0x%x)", decode);
|
||||
break;
|
||||
}
|
||||
|
||||
if (got_frame)
|
||||
{
|
||||
ConLog.Write("got_frame (%d, vdec: pts=0x%llx, dts=0x%llx)", got_frame, au.pts, au.dts);
|
||||
|
||||
frame.pts = task.au.pts; // ???
|
||||
frame.auAddr = task.au.addr;
|
||||
frame.auSize = task.au.size;
|
||||
frame.userdata = task.au.userdata;
|
||||
frame.size = 32768; // ????
|
||||
adec.frames.Push(frame);
|
||||
frame.data = nullptr; // to prevent destruction
|
||||
|
||||
Callback cb;
|
||||
cb.SetAddr(adec.cbFunc);
|
||||
cb.Handle(adec.id, CELL_ADEC_MSG_TYPE_PCMOUT, CELL_OK, adec.cbArg);
|
||||
cb.Branch(false);
|
||||
}
|
||||
}
|
||||
|
||||
Callback cb;
|
||||
cb.SetAddr(adec.cbFunc);
|
||||
cb.Handle(adec.id, CELL_ADEC_MSG_TYPE_AUDONE, task.au.auInfo_addr, adec.cbArg);
|
||||
cb.Branch(false);
|
||||
}
|
||||
break;
|
||||
|
||||
|
@ -78,9 +296,9 @@ u32 adecOpen(AudioDecoder* data)
|
|||
|
||||
default:
|
||||
ConLog.Error("Audio Decoder error: unknown task(%d)", task.type);
|
||||
return;
|
||||
}
|
||||
}
|
||||
adec.is_finished = true;
|
||||
ConLog.Warning("Audio Decoder aborted");
|
||||
});
|
||||
|
||||
|
@ -196,31 +414,158 @@ int cellAdecClose(u32 handle)
|
|||
|
||||
int cellAdecStartSeq(u32 handle, u32 param_addr)
|
||||
{
|
||||
cellAdec.Error("cellAdecStartSeq(handle=%d, param_addr=0x%x)", handle, param_addr);
|
||||
cellAdec.Log("cellAdecStartSeq(handle=%d, param_addr=0x%x)", handle, param_addr);
|
||||
|
||||
AudioDecoder* adec;
|
||||
if (!Emu.GetIdManager().GetIDData(handle, adec))
|
||||
{
|
||||
return CELL_ADEC_ERROR_ARG;
|
||||
}
|
||||
|
||||
AdecTask task(adecStartSeq);
|
||||
/*if (adec->type == CELL_ADEC_TYPE_ATRACX_2CH)
|
||||
{
|
||||
|
||||
}
|
||||
else*/
|
||||
{
|
||||
cellAdec.Warning("cellAdecStartSeq: (TODO) initialization");
|
||||
}
|
||||
|
||||
adec->job.Push(task);
|
||||
return CELL_OK;
|
||||
}
|
||||
|
||||
int cellAdecEndSeq(u32 handle)
|
||||
{
|
||||
cellAdec.Error("cellAdecEndSeq(handle=%d)", handle);
|
||||
cellAdec.Warning("cellAdecEndSeq(handle=%d)", handle);
|
||||
|
||||
AudioDecoder* adec;
|
||||
if (!Emu.GetIdManager().GetIDData(handle, adec))
|
||||
{
|
||||
return CELL_ADEC_ERROR_ARG;
|
||||
}
|
||||
|
||||
adec->job.Push(AdecTask(adecEndSeq));
|
||||
return CELL_OK;
|
||||
}
|
||||
|
||||
int cellAdecDecodeAu(u32 handle, mem_ptr_t<CellAdecAuInfo> auInfo)
|
||||
{
|
||||
cellAdec.Error("cellAdecDecodeAu(handle=%d, auInfo_addr=0x%x)", handle, auInfo.GetAddr());
|
||||
cellAdec.Log("cellAdecDecodeAu(handle=%d, auInfo_addr=0x%x)", handle, auInfo.GetAddr());
|
||||
|
||||
AudioDecoder* adec;
|
||||
if (!Emu.GetIdManager().GetIDData(handle, adec))
|
||||
{
|
||||
return CELL_ADEC_ERROR_ARG;
|
||||
}
|
||||
|
||||
if (!auInfo.IsGood())
|
||||
{
|
||||
return CELL_ADEC_ERROR_FATAL;
|
||||
}
|
||||
|
||||
AdecTask task(adecDecodeAu);
|
||||
task.au.auInfo_addr = auInfo.GetAddr();
|
||||
task.au.addr = auInfo->startAddr;
|
||||
task.au.size = auInfo->size;
|
||||
task.au.pts = ((u64)auInfo->pts.upper << 32) | (u64)auInfo->pts.lower;
|
||||
task.au.userdata = auInfo->userData;
|
||||
|
||||
adec->job.Push(task);
|
||||
return CELL_OK;
|
||||
}
|
||||
|
||||
int cellAdecGetPcm(u32 handle, u32 outBuffer_addr)
|
||||
{
|
||||
cellAdec.Error("cellAdecGetPcm(handle=%d, outBuffer_addr=0x%x)", handle, outBuffer_addr);
|
||||
return CELL_OK;
|
||||
cellAdec.Log("cellAdecGetPcm(handle=%d, outBuffer_addr=0x%x)", handle, outBuffer_addr);
|
||||
|
||||
AudioDecoder* adec;
|
||||
if (!Emu.GetIdManager().GetIDData(handle, adec))
|
||||
{
|
||||
return CELL_ADEC_ERROR_ARG;
|
||||
}
|
||||
|
||||
if (adec->frames.IsEmpty())
|
||||
{
|
||||
return CELL_ADEC_ERROR_EMPTY;
|
||||
}
|
||||
|
||||
AdecFrame af;
|
||||
adec->frames.Pop(af);
|
||||
//AVFrame& frame = *af.data;
|
||||
|
||||
int result = CELL_OK;
|
||||
|
||||
if (!Memory.IsGoodAddr(outBuffer_addr, af.size))
|
||||
{
|
||||
result = CELL_ADEC_ERROR_FATAL;
|
||||
}
|
||||
else
|
||||
{
|
||||
// copy data
|
||||
if (!af.data) // fake: empty data
|
||||
{
|
||||
return CELL_OK;
|
||||
}
|
||||
}
|
||||
|
||||
if (af.data)
|
||||
{
|
||||
av_frame_unref(af.data);
|
||||
av_frame_free(&af.data);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int cellAdecGetPcmItem(u32 handle, u32 pcmItem_ptr_addr)
|
||||
int cellAdecGetPcmItem(u32 handle, mem32_t pcmItem_ptr)
|
||||
{
|
||||
cellAdec.Error("cellAdecGetPcmItem(handle=%d, pcmItem_ptr_addr=0x%x)", handle, pcmItem_ptr_addr);
|
||||
cellAdec.Log("cellAdecGetPcmItem(handle=%d, pcmItem_ptr_addr=0x%x)", handle, pcmItem_ptr.GetAddr());
|
||||
|
||||
AudioDecoder* adec;
|
||||
if (!Emu.GetIdManager().GetIDData(handle, adec))
|
||||
{
|
||||
return CELL_ADEC_ERROR_ARG;
|
||||
}
|
||||
|
||||
if (!pcmItem_ptr.IsGood())
|
||||
{
|
||||
return CELL_ADEC_ERROR_FATAL;
|
||||
}
|
||||
|
||||
AdecFrame& af = adec->frames.Peek();
|
||||
|
||||
if (adec->frames.IsEmpty())
|
||||
{
|
||||
return CELL_ADEC_ERROR_EMPTY;
|
||||
}
|
||||
|
||||
//AVFrame& frame = *af.data;
|
||||
|
||||
mem_ptr_t<CellAdecPcmItem> pcm(adec->memAddr + adec->memBias);
|
||||
|
||||
adec->memBias += 512;
|
||||
if (adec->memBias + 512 > adec->memSize)
|
||||
adec->memBias = 0;
|
||||
|
||||
pcm->pcmHandle = 0; // ???
|
||||
pcm->pcmAttr.bsiInfo_addr = pcm.GetAddr() + sizeof(CellAdecPcmItem);
|
||||
pcm->startAddr = 0x00000312; // invalid address (no output)
|
||||
pcm->size = af.size;
|
||||
pcm->status = CELL_OK;
|
||||
pcm->auInfo.pts.lower = af.pts; // ???
|
||||
pcm->auInfo.pts.upper = af.pts >> 32;
|
||||
pcm->auInfo.size = af.auSize;
|
||||
pcm->auInfo.startAddr = af.auAddr;
|
||||
pcm->auInfo.userData = af.userdata;
|
||||
|
||||
mem_ptr_t<CellAdecAtracXInfo> atx(pcm.GetAddr() + sizeof(CellAdecPcmItem));
|
||||
atx->samplingFreq = 48000; // ???
|
||||
atx->nbytes = 2048; // ???
|
||||
atx->channelConfigIndex = CELL_ADEC_CH_STEREO; // ???
|
||||
|
||||
pcmItem_ptr = pcm.GetAddr();
|
||||
|
||||
return CELL_OK;
|
||||
}
|
||||
|
||||
|
|
|
@ -1002,7 +1002,17 @@ enum AdecJobType : u32
|
|||
struct AdecTask
|
||||
{
|
||||
AdecJobType type;
|
||||
// ...
|
||||
union
|
||||
{
|
||||
struct
|
||||
{
|
||||
u32 auInfo_addr;
|
||||
u32 addr;
|
||||
u32 size;
|
||||
u64 pts;
|
||||
u64 userdata;
|
||||
} au;
|
||||
};
|
||||
|
||||
AdecTask(AdecJobType type)
|
||||
: type(type)
|
||||
|
@ -1016,11 +1026,16 @@ struct AdecTask
|
|||
|
||||
struct AdecFrame
|
||||
{
|
||||
// under construction
|
||||
AVFrame* data;
|
||||
u64 pts;
|
||||
u64 userdata;
|
||||
u32 auAddr;
|
||||
u32 auSize;
|
||||
u32 size;
|
||||
};
|
||||
|
||||
int adecRead(void* opaque, u8* buf, int buf_size);
|
||||
|
||||
class AudioDecoder
|
||||
{
|
||||
public:
|
||||
|
@ -1028,6 +1043,17 @@ public:
|
|||
u32 id;
|
||||
volatile bool is_running;
|
||||
volatile bool is_finished;
|
||||
bool just_started;
|
||||
|
||||
AVCodecContext* ctx;
|
||||
AVFormatContext* fmt;
|
||||
u8* io_buf;
|
||||
|
||||
struct AudioReader
|
||||
{
|
||||
u32 addr;
|
||||
u32 size;
|
||||
} reader;
|
||||
|
||||
SQueue<AdecFrame> frames;
|
||||
|
||||
|
@ -1036,19 +1062,66 @@ public:
|
|||
const u32 memSize;
|
||||
const u32 cbFunc;
|
||||
const u32 cbArg;
|
||||
u32 memBias;
|
||||
|
||||
AudioDecoder(AudioCodecType type, u32 addr, u32 size, u32 func, u32 arg)
|
||||
: type(type)
|
||||
, memAddr(addr)
|
||||
, memSize(size)
|
||||
, memBias(0)
|
||||
, cbFunc(func)
|
||||
, cbArg(arg)
|
||||
, is_running(false)
|
||||
, is_finished(false)
|
||||
, just_started(false)
|
||||
, ctx(nullptr)
|
||||
, fmt(nullptr)
|
||||
{
|
||||
AVCodec* codec = avcodec_find_decoder(AV_CODEC_ID_ATRAC3P);
|
||||
if (!codec)
|
||||
{
|
||||
ConLog.Error("AudioDecoder(): avcodec_find_decoder(ATRAC3P) failed");
|
||||
Emu.Pause();
|
||||
return;
|
||||
}
|
||||
fmt = avformat_alloc_context();
|
||||
if (!fmt)
|
||||
{
|
||||
ConLog.Error("AudioDecoder(): avformat_alloc_context failed");
|
||||
Emu.Pause();
|
||||
return;
|
||||
}
|
||||
io_buf = (u8*)av_malloc(4096);
|
||||
fmt->pb = avio_alloc_context(io_buf, 4096, 0, this, adecRead, NULL, NULL);
|
||||
if (!fmt->pb)
|
||||
{
|
||||
ConLog.Error("AudioDecoder(): avio_alloc_context failed");
|
||||
Emu.Pause();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
~AudioDecoder()
|
||||
{
|
||||
if (ctx)
|
||||
{
|
||||
for (u32 i = frames.GetCount() - 1; ~i; i--)
|
||||
{
|
||||
AdecFrame& af = frames.Peek(i);
|
||||
av_frame_unref(af.data);
|
||||
av_frame_free(&af.data);
|
||||
}
|
||||
avcodec_close(ctx);
|
||||
avformat_close_input(&fmt);
|
||||
}
|
||||
if (fmt)
|
||||
{
|
||||
if (io_buf)
|
||||
{
|
||||
av_free(io_buf);
|
||||
}
|
||||
if (fmt->pb) av_free(fmt->pb);
|
||||
avformat_free_context(fmt);
|
||||
}
|
||||
}
|
||||
};
|
|
@ -18,9 +18,9 @@ void dmuxQueryEsAttr(u32 info_addr /* may be 0 */, const mem_ptr_t<CellCodecEsFi
|
|||
const u32 esSpecificInfo_addr, mem_ptr_t<CellDmuxEsAttr> attr)
|
||||
{
|
||||
if (esFilterId->filterIdMajor >= 0xe0)
|
||||
attr->memSize = 0x6000000; // 0x45fa49 from ps3
|
||||
attr->memSize = 0x2000000; // 0x45fa49 from ps3
|
||||
else
|
||||
attr->memSize = 0x10000; // 0x73d9 from ps3
|
||||
attr->memSize = 0x400000; // 0x73d9 from ps3
|
||||
|
||||
cellDmux.Warning("*** filter(0x%x, 0x%x, 0x%x, 0x%x)", (u32)esFilterId->filterIdMajor, (u32)esFilterId->filterIdMinor,
|
||||
(u32)esFilterId->supplementalInfo1, (u32)esFilterId->supplementalInfo2);
|
||||
|
@ -102,12 +102,51 @@ u32 dmuxOpen(Demuxer* data)
|
|||
|
||||
case PRIVATE_STREAM_1:
|
||||
{
|
||||
DemuxerStream backup = stream;
|
||||
|
||||
// audio AT3+ (and probably LPCM or user data)
|
||||
stream.skip(4);
|
||||
stream.get(len);
|
||||
|
||||
// skipping...
|
||||
stream.skip(len);
|
||||
PesHeader pes(stream);
|
||||
|
||||
if (!pes.new_au) // temporarily
|
||||
{
|
||||
ConLog.Error("No pts info found");
|
||||
}
|
||||
|
||||
// read additional header:
|
||||
stream.peek(ch);
|
||||
//stream.skip(4);
|
||||
//pes.size += 4;
|
||||
|
||||
if (esATX[ch])
|
||||
{
|
||||
ElementaryStream& es = *esATX[ch];
|
||||
if (es.isfull())
|
||||
{
|
||||
stream = backup;
|
||||
Sleep(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
//ConLog.Write("*** AT3+ AU sent (pts=0x%llx, dts=0x%llx)", pes.pts, pes.dts);
|
||||
|
||||
es.push(stream, len - pes.size - 3, pes);
|
||||
es.finish(stream);
|
||||
|
||||
mem_ptr_t<CellDmuxEsMsg> esMsg(a128(dmux.memAddr) + (cb_add ^= 16));
|
||||
esMsg->msgType = CELL_DMUX_ES_MSG_TYPE_AU_FOUND;
|
||||
esMsg->supplementalInfo = stream.userdata;
|
||||
Callback cb;
|
||||
cb.SetAddr(es.cbFunc);
|
||||
cb.Handle(dmux.id, es.id, esMsg.GetAddr(), es.cbArg);
|
||||
cb.Branch(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
stream.skip(len - pes.size - 3);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
|
@ -133,12 +172,6 @@ u32 dmuxOpen(Demuxer* data)
|
|||
stream.get(len);
|
||||
PesHeader pes(stream);
|
||||
|
||||
if (!pes.new_au && !es.hasdata()) // fatal error
|
||||
{
|
||||
ConLog.Error("PES not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (pes.new_au && es.hasdata()) // new AU detected
|
||||
{
|
||||
if (es.hasunseen()) // hack, probably useless
|
||||
|
@ -285,6 +318,13 @@ task:
|
|||
{
|
||||
esAVC[es.fidMajor - 0xe0] = task.es.es_ptr;
|
||||
}
|
||||
else if (es.fidMajor == 0xbd &&
|
||||
es.fidMinor == 0 &&
|
||||
es.sup1 == 0 &&
|
||||
es.sup2 == 0)
|
||||
{
|
||||
esATX[0] = task.es.es_ptr;
|
||||
}
|
||||
else
|
||||
{
|
||||
ConLog.Warning("dmuxEnableEs: (TODO) unsupported filter (0x%x, 0x%x, 0x%x, 0x%x)", es.fidMajor, es.fidMinor, es.sup1, es.sup2);
|
||||
|
@ -853,7 +893,7 @@ int cellDmuxPeekAuEx(u32 esHandle, mem32_t auInfoEx_ptr, mem32_t auSpecificInfo_
|
|||
|
||||
int cellDmuxReleaseAu(u32 esHandle)
|
||||
{
|
||||
cellDmux.Warning("(disabled) cellDmuxReleaseAu(esHandle=0x%x)", esHandle);
|
||||
cellDmux.Log("cellDmuxReleaseAu(esHandle=0x%x)", esHandle);
|
||||
|
||||
return CELL_OK;
|
||||
|
||||
|
|
|
@ -329,11 +329,11 @@ struct DemuxerStream
|
|||
}
|
||||
|
||||
template<typename T>
|
||||
bool peek(T& out)
|
||||
bool peek(T& out, u32 shift = 0)
|
||||
{
|
||||
if (sizeof(T) > size) return false;
|
||||
if (sizeof(T) + shift > size) return false;
|
||||
|
||||
out = *(T*)Memory.VirtualToRealAddr(addr);
|
||||
out = *(T*)Memory.VirtualToRealAddr(addr + shift);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -364,14 +364,12 @@ struct PesHeader
|
|||
{
|
||||
u64 pts;
|
||||
u64 dts;
|
||||
u8 ch;
|
||||
u8 size;
|
||||
bool new_au;
|
||||
|
||||
PesHeader(DemuxerStream& stream)
|
||||
: pts(0xffffffffffffffff)
|
||||
, dts(0xffffffffffffffff)
|
||||
, ch(0)
|
||||
, size(0)
|
||||
, new_au(false)
|
||||
{
|
||||
|
@ -380,29 +378,40 @@ struct PesHeader
|
|||
stream.get(size);
|
||||
if (size)
|
||||
{
|
||||
//ConLog.Write(">>>>> Pes Header (size=%d)", size);
|
||||
if (size < 10)
|
||||
{
|
||||
stream.skip(size);
|
||||
return;
|
||||
}
|
||||
new_au = true;
|
||||
u8 empty = 0;
|
||||
u8 v;
|
||||
stream.get(v);
|
||||
if ((v & 0xF0) != 0x30)
|
||||
while (true)
|
||||
{
|
||||
ConLog.Error("Pts not found");
|
||||
Emu.Pause();
|
||||
}
|
||||
pts = stream.get_ts(v);
|
||||
stream.get(v);
|
||||
if ((v & 0xF0) != 0x10)
|
||||
stream.get(v);
|
||||
if (v != 0xFF) break; // skip padding bytes
|
||||
empty++;
|
||||
if (empty = size) return;
|
||||
};
|
||||
|
||||
if ((v & 0xF0) == 0x20 && (size - empty) >= 5) // pts only
|
||||
{
|
||||
ConLog.Error("Dts not found");
|
||||
Emu.Pause();
|
||||
new_au = true;
|
||||
pts = stream.get_ts(v);
|
||||
stream.skip(size - empty - 5);
|
||||
}
|
||||
else
|
||||
{
|
||||
new_au = true;
|
||||
if ((v & 0xF0) != 0x30 || (size - empty) < 10)
|
||||
{
|
||||
ConLog.Error("PesHeader(): pts not found");
|
||||
Emu.Pause();
|
||||
}
|
||||
pts = stream.get_ts(v);
|
||||
stream.get(v);
|
||||
if ((v & 0xF0) != 0x10)
|
||||
{
|
||||
ConLog.Error("PesHeader(): dts not found");
|
||||
Emu.Pause();
|
||||
}
|
||||
dts = stream.get_ts(v);
|
||||
stream.skip(size - empty - 10);
|
||||
}
|
||||
dts = stream.get_ts(v);
|
||||
stream.skip(size - 10);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -497,8 +506,8 @@ public:
|
|||
|
||||
ElementaryStream(Demuxer* dmux, u32 addr, u32 size, u32 fidMajor, u32 fidMinor, u32 sup1, u32 sup2, u32 cbFunc, u32 cbArg, u32 spec)
|
||||
: dmux(dmux)
|
||||
, memAddr(addr)
|
||||
, memSize(size)
|
||||
, memAddr(a128(addr))
|
||||
, memSize(size - (addr - memAddr))
|
||||
, fidMajor(fidMajor)
|
||||
, fidMinor(fidMinor)
|
||||
, sup1(sup1)
|
||||
|
@ -508,7 +517,7 @@ public:
|
|||
, spec(spec)
|
||||
, first_addr(0)
|
||||
, peek_addr(0)
|
||||
, last_addr(a128(addr))
|
||||
, last_addr(memAddr)
|
||||
, last_size(0)
|
||||
{
|
||||
}
|
||||
|
@ -590,7 +599,7 @@ public:
|
|||
mem_ptr_t<CellDmuxAuInfoEx> info(last_addr);
|
||||
info->auAddr = last_addr + 128;
|
||||
info->auSize = last_size;
|
||||
if (pes.size)
|
||||
if (pes.new_au)
|
||||
{
|
||||
info->dts.lower = (u32)pes.dts;
|
||||
info->dts.upper = (u32)(pes.dts >> 32);
|
||||
|
@ -607,7 +616,7 @@ public:
|
|||
mem_ptr_t<CellDmuxAuInfo> inf(last_addr + 64);
|
||||
inf->auAddr = last_addr + 128;
|
||||
inf->auSize = last_size;
|
||||
if (pes.size)
|
||||
if (pes.new_au)
|
||||
{
|
||||
inf->dtsLower = (u32)pes.dts;
|
||||
inf->dtsUpper = (u32)(pes.dts >> 32);
|
||||
|
@ -687,7 +696,7 @@ public:
|
|||
SMutexLocker lock(mutex);
|
||||
first_addr = 0;
|
||||
peek_addr = 0;
|
||||
last_addr = a128(memAddr);
|
||||
last_addr = memAddr;
|
||||
last_size = 0;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
#include "Emu/SysCalls/SC_FUNC.h"
|
||||
#include "cellPamf.h"
|
||||
|
||||
SMutexGeneral g_mutex_avcodec_open2;
|
||||
|
||||
extern "C"
|
||||
{
|
||||
#include "libavcodec\avcodec.h"
|
||||
|
@ -21,35 +23,56 @@ int vdecRead(void* opaque, u8* buf, int buf_size)
|
|||
|
||||
int res = 0;
|
||||
|
||||
/*if (vdec.reader.header_size)
|
||||
{
|
||||
assert(vdec.reader.header_size == 14);
|
||||
res = buf_size;
|
||||
if (vdec.reader.header_size < (u32)buf_size)
|
||||
res = vdec.reader.header_size;
|
||||
|
||||
buf[0] = 0;
|
||||
buf[1] = 0;
|
||||
buf[2] = 1;
|
||||
buf[3] = 0xba;
|
||||
buf[4] = 0x44;
|
||||
buf[5] = 0;
|
||||
buf[6] = 0x07;
|
||||
buf[7] = 0xaa;
|
||||
buf[8] = 0x75;
|
||||
buf[9] = 0xb1;
|
||||
buf[10] = 0x07;
|
||||
buf[11] = 0x53;
|
||||
buf[12] = 0x03;
|
||||
buf[13] = 0xf8;
|
||||
vdec.reader.header_size -= res;
|
||||
buf_size -= res;
|
||||
buf += res;
|
||||
}*/
|
||||
|
||||
if (vdec.reader.size < (u32)buf_size)
|
||||
{
|
||||
buf_size = vdec.reader.size;
|
||||
while (vdec.job.IsEmpty())
|
||||
{
|
||||
if (Emu.IsStopped())
|
||||
{
|
||||
ConLog.Warning("vdecRead() aborted");
|
||||
return 0;
|
||||
}
|
||||
Sleep(1);
|
||||
}
|
||||
|
||||
switch (vdec.job.Peek().type)
|
||||
{
|
||||
case vdecEndSeq:
|
||||
{
|
||||
buf_size = vdec.reader.size;
|
||||
}
|
||||
break;
|
||||
case vdecDecodeAu:
|
||||
{
|
||||
if (!Memory.CopyToReal(buf, vdec.reader.addr, vdec.reader.size))
|
||||
{
|
||||
ConLog.Error("vdecRead: data reading failed (reader.size=0x%x)", vdec.reader.size);
|
||||
Emu.Pause();
|
||||
return 0;
|
||||
}
|
||||
|
||||
buf += vdec.reader.size;
|
||||
buf_size -= vdec.reader.size;
|
||||
res += vdec.reader.size;
|
||||
|
||||
Callback cb;
|
||||
cb.SetAddr(vdec.cbFunc);
|
||||
cb.Handle(vdec.id, CELL_VDEC_MSG_TYPE_AUDONE, CELL_OK, vdec.cbArg);
|
||||
cb.Branch(false);
|
||||
|
||||
vdec.job.Pop(vdec.task);
|
||||
|
||||
vdec.reader.addr = vdec.task.addr;
|
||||
vdec.reader.size = vdec.task.size;
|
||||
|
||||
vdec.last_pts = vdec.task.pts;
|
||||
vdec.last_dts = vdec.task.dts;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
ConLog.Error("vdecRead(): sequence error (task %d)", vdec.job.Peek().type);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!buf_size)
|
||||
|
@ -60,7 +83,7 @@ int vdecRead(void* opaque, u8* buf, int buf_size)
|
|||
{
|
||||
ConLog.Error("vdecRead: data reading failed (buf_size=0x%x)", buf_size);
|
||||
Emu.Pause();
|
||||
return res;
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -100,7 +123,7 @@ u32 vdecOpen(VideoDecoder* data)
|
|||
{
|
||||
ConLog.Write("Video Decoder enter()");
|
||||
|
||||
VdecTask task;
|
||||
VdecTask& task = vdec.task;
|
||||
|
||||
while (true)
|
||||
{
|
||||
|
@ -142,11 +165,12 @@ u32 vdecOpen(VideoDecoder* data)
|
|||
|
||||
case vdecEndSeq:
|
||||
{
|
||||
// TODO: finalize
|
||||
ConLog.Warning("vdecEndSeq:");
|
||||
|
||||
Callback cb;
|
||||
cb.SetAddr(vdec.cbFunc);
|
||||
cb.Handle(vdec.id, CELL_VDEC_MSG_TYPE_SEQDONE, 0, vdec.cbArg);
|
||||
cb.Handle(vdec.id, CELL_VDEC_MSG_TYPE_SEQDONE, CELL_OK, vdec.cbArg);
|
||||
cb.Branch(true); // ???
|
||||
|
||||
avcodec_close(vdec.ctx);
|
||||
|
@ -169,7 +193,8 @@ u32 vdecOpen(VideoDecoder* data)
|
|||
vdec.reader.addr = task.addr;
|
||||
vdec.reader.size = task.size;
|
||||
|
||||
u64 last_pts = task.pts, last_dts = task.dts;
|
||||
vdec.last_pts = task.pts;
|
||||
vdec.last_dts = task.dts;
|
||||
|
||||
struct AVPacketHolder : AVPacket
|
||||
{
|
||||
|
@ -198,13 +223,6 @@ u32 vdecOpen(VideoDecoder* data)
|
|||
|
||||
} au(0);
|
||||
|
||||
/*{
|
||||
wxFile dump;
|
||||
dump.Open(wxString::Format("0x%llx-0x%llx.dump", au.pts, au.dts), wxFile::write);
|
||||
dump.Write(au.data, task.size + FF_INPUT_BUFFER_PADDING_SIZE);
|
||||
dump.Close();
|
||||
}*/
|
||||
|
||||
if (vdec.just_started) // deferred initialization
|
||||
{
|
||||
err = avformat_open_input(&vdec.fmt, NULL, NULL, NULL);
|
||||
|
@ -212,20 +230,20 @@ u32 vdecOpen(VideoDecoder* data)
|
|||
{
|
||||
ConLog.Error("vdecDecodeAu: avformat_open_input() failed");
|
||||
Emu.Pause();
|
||||
return;
|
||||
break;
|
||||
}
|
||||
err = avformat_find_stream_info(vdec.fmt, NULL);
|
||||
if (err)
|
||||
{
|
||||
ConLog.Error("vdecDecodeAu: avformat_find_stream_info() failed");
|
||||
Emu.Pause();
|
||||
return;
|
||||
break;
|
||||
}
|
||||
if (!vdec.fmt->nb_streams)
|
||||
{
|
||||
ConLog.Error("vdecDecodeAu: no stream found");
|
||||
Emu.Pause();
|
||||
return;
|
||||
break;
|
||||
}
|
||||
vdec.ctx = vdec.fmt->streams[0]->codec; // TODO: check data
|
||||
|
||||
|
@ -234,26 +252,37 @@ u32 vdecOpen(VideoDecoder* data)
|
|||
{
|
||||
ConLog.Error("vdecDecodeAu: avcodec_find_decoder() failed");
|
||||
Emu.Pause();
|
||||
return;
|
||||
break;
|
||||
}
|
||||
|
||||
AVDictionary* opts = nullptr;
|
||||
av_dict_set(&opts, "refcounted_frames", "1", 0);
|
||||
{
|
||||
static SMutexGeneral g_mutex_avcodec_open2;
|
||||
SMutexGeneralLocker lock(g_mutex_avcodec_open2);
|
||||
// not multithread-safe
|
||||
err = avcodec_open2(vdec.ctx, codec, &vdec.opts);
|
||||
err = avcodec_open2(vdec.ctx, codec, &opts);
|
||||
}
|
||||
if (err)
|
||||
{
|
||||
ConLog.Error("vdecDecodeAu: avcodec_open2() failed");
|
||||
Emu.Pause();
|
||||
return;
|
||||
break;
|
||||
}
|
||||
vdec.just_started = false;
|
||||
}
|
||||
|
||||
while (av_read_frame(vdec.fmt, &au) >= 0)
|
||||
bool last_frame = false;
|
||||
|
||||
while (true)
|
||||
{
|
||||
last_frame = av_read_frame(vdec.fmt, &au) < 0;
|
||||
if (last_frame)
|
||||
{
|
||||
av_free(au.data);
|
||||
au.data = NULL;
|
||||
au.size = 0;
|
||||
}
|
||||
|
||||
struct VdecFrameHolder : VdecFrame
|
||||
{
|
||||
VdecFrameHolder()
|
||||
|
@ -276,41 +305,43 @@ u32 vdecOpen(VideoDecoder* data)
|
|||
{
|
||||
ConLog.Error("vdecDecodeAu: av_frame_alloc() failed");
|
||||
Emu.Pause();
|
||||
return;
|
||||
break;
|
||||
}
|
||||
|
||||
int got_picture = 0;
|
||||
|
||||
int decode = avcodec_decode_video2(vdec.ctx, frame.data, &got_picture, &au);
|
||||
|
||||
if (decode < 0)
|
||||
if (decode <= 0)
|
||||
{
|
||||
ConLog.Error("vdecDecodeAu: AU decoding error(0x%x)", decode);
|
||||
break;
|
||||
if (!last_frame && decode < 0)
|
||||
{
|
||||
ConLog.Error("vdecDecodeAu: AU decoding error(0x%x)", decode);
|
||||
break;
|
||||
}
|
||||
if (!got_picture && vdec.reader.size == 0) break; // video end?
|
||||
}
|
||||
|
||||
if (got_picture)
|
||||
{
|
||||
ConLog.Write("got_picture (%d, vdec: pts=0x%llx, dts=0x%llx)", got_picture, au.pts, au.dts);
|
||||
//ConLog.Write("got_picture (%d, vdec: pts=0x%llx, dts=0x%llx)", got_picture, au.pts, au.dts);
|
||||
|
||||
frame.dts = last_dts; last_dts += 3003; // + duration???
|
||||
frame.pts = last_pts; last_pts += 3003;
|
||||
frame.dts = vdec.last_dts; vdec.last_dts += 3003; // + duration???
|
||||
frame.pts = vdec.last_pts; vdec.last_pts += 3003;
|
||||
frame.userdata = task.userData;
|
||||
vdec.frames.Push(frame);
|
||||
frame.data = nullptr; // to prevent destruction
|
||||
|
||||
Callback cb;
|
||||
cb.SetAddr(vdec.cbFunc);
|
||||
cb.Handle(vdec.id, CELL_VDEC_MSG_TYPE_PICOUT, 0, vdec.cbArg);
|
||||
cb.Handle(vdec.id, CELL_VDEC_MSG_TYPE_PICOUT, CELL_OK, vdec.cbArg);
|
||||
cb.Branch(false);
|
||||
}
|
||||
}
|
||||
|
||||
ConLog.Write("AU decoded (pts=0x%llx, dts=0x%llx, addr=0x%x, size=0x%x)", task.pts, task.dts, task.addr, task.size);
|
||||
|
||||
Callback cb;
|
||||
cb.SetAddr(vdec.cbFunc);
|
||||
cb.Handle(vdec.id, CELL_VDEC_MSG_TYPE_AUDONE, 0, vdec.cbArg);
|
||||
cb.Handle(vdec.id, CELL_VDEC_MSG_TYPE_AUDONE, CELL_OK, vdec.cbArg);
|
||||
cb.Branch(false);
|
||||
}
|
||||
break;
|
||||
|
@ -325,15 +356,15 @@ u32 vdecOpen(VideoDecoder* data)
|
|||
case vdecSetFrameRate:
|
||||
{
|
||||
ConLog.Error("TODO: vdecSetFrameRate(%d)", task.frc);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
ConLog.Error("Video Decoder error: unknown task(%d)", task.type);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
vdec.is_finished = true;
|
||||
ConLog.Warning("Video Decoder aborted");
|
||||
});
|
||||
|
||||
|
@ -603,7 +634,11 @@ int cellVdecGetPicItem(u32 handle, mem32_t picItem_ptr)
|
|||
|
||||
AVFrame& frame = *vf.data;
|
||||
|
||||
mem_ptr_t<CellVdecPicItem> info(vdec->memAddr);
|
||||
mem_ptr_t<CellVdecPicItem> info(vdec->memAddr + vdec->memBias);
|
||||
|
||||
vdec->memBias += 512;
|
||||
if (vdec->memBias + 512 > vdec->memSize)
|
||||
vdec->memBias = 0;
|
||||
|
||||
info->codecType = vdec->type;
|
||||
info->startAddr = 0x00000123; // invalid value (no address for picture)
|
||||
|
@ -621,9 +656,9 @@ int cellVdecGetPicItem(u32 handle, mem32_t picItem_ptr)
|
|||
info->auUserData[1] = 0;
|
||||
info->status = CELL_OK;
|
||||
info->attr = CELL_VDEC_PICITEM_ATTR_NORMAL;
|
||||
info->picInfo_addr = vdec->memAddr + sizeof(CellVdecPicItem);
|
||||
info->picInfo_addr = info.GetAddr() + sizeof(CellVdecPicItem);
|
||||
|
||||
mem_ptr_t<CellVdecAvcInfo> avc(vdec->memAddr + sizeof(CellVdecPicItem));
|
||||
mem_ptr_t<CellVdecAvcInfo> avc(info.GetAddr() + sizeof(CellVdecPicItem));
|
||||
|
||||
avc->horizontalSize = frame.width;
|
||||
avc->verticalSize = frame.height;
|
||||
|
|
|
@ -699,7 +699,6 @@ public:
|
|||
bool just_started;
|
||||
|
||||
AVCodecContext* ctx;
|
||||
AVDictionary* opts;
|
||||
AVFormatContext* fmt;
|
||||
u8* io_buf;
|
||||
|
||||
|
@ -717,34 +716,31 @@ public:
|
|||
const u32 memSize;
|
||||
const u32 cbFunc;
|
||||
const u32 cbArg;
|
||||
u32 memBias;
|
||||
|
||||
VdecTask task; // reference to current task variable
|
||||
u64 last_pts, last_dts;
|
||||
|
||||
VideoDecoder(CellVdecCodecType type, u32 profile, u32 addr, u32 size, u32 func, u32 arg)
|
||||
: type(type)
|
||||
, profile(profile)
|
||||
, memAddr(addr)
|
||||
, memSize(size)
|
||||
, memBias(0)
|
||||
, cbFunc(func)
|
||||
, cbArg(arg)
|
||||
, is_finished(false)
|
||||
, is_running(false)
|
||||
, just_started(false)
|
||||
, ctx(nullptr)
|
||||
{
|
||||
opts = nullptr;
|
||||
av_dict_set(&opts, "refcounted_frames", "1", 0);
|
||||
AVCodec* codec = avcodec_find_decoder(AV_CODEC_ID_H264);
|
||||
if (!codec)
|
||||
{
|
||||
ConLog.Error("vdecDecodeAu: avcodec_find_decoder(H264) failed");
|
||||
ConLog.Error("VideoDecoder(): avcodec_find_decoder(H264) failed");
|
||||
Emu.Pause();
|
||||
return;
|
||||
}
|
||||
ctx = nullptr; /*avcodec_alloc_context3(codec);
|
||||
if (!ctx)
|
||||
{
|
||||
ConLog.Error("VideoDecoder(): avcodec_alloc_context3 failed");
|
||||
Emu.Pause();
|
||||
return;
|
||||
}*/
|
||||
fmt = avformat_alloc_context();
|
||||
if (!fmt)
|
||||
{
|
||||
|
@ -764,7 +760,7 @@ public:
|
|||
|
||||
~VideoDecoder()
|
||||
{
|
||||
if (!is_finished && ctx)
|
||||
if (ctx)
|
||||
{
|
||||
for (u32 i = frames.GetCount() - 1; ~i; i--)
|
||||
{
|
||||
|
@ -772,20 +768,17 @@ public:
|
|||
av_frame_unref(vf.data);
|
||||
av_frame_free(&vf.data);
|
||||
}
|
||||
}
|
||||
if (io_buf)
|
||||
{
|
||||
av_free(io_buf);
|
||||
avcodec_close(ctx);
|
||||
avformat_close_input(&fmt);
|
||||
}
|
||||
if (fmt)
|
||||
{
|
||||
if (io_buf)
|
||||
{
|
||||
av_free(io_buf);
|
||||
}
|
||||
if (fmt->pb) av_free(fmt->pb);
|
||||
avformat_free_context(fmt);
|
||||
}
|
||||
if (!is_finished && ctx)
|
||||
{
|
||||
//avcodec_close(ctx); // crashes
|
||||
//avformat_close_input(&fmt);
|
||||
}
|
||||
}
|
||||
};
|
|
@ -77,7 +77,7 @@ int cellVpostClose(u32 handle)
|
|||
int cellVpostExec(u32 handle, const u32 inPicBuff_addr, const mem_ptr_t<CellVpostCtrlParam> ctrlParam,
|
||||
u32 outPicBuff_addr, mem_ptr_t<CellVpostPictureInfo> picInfo)
|
||||
{
|
||||
cellVpost.Warning("cellVpostExec(handle=0x%x, inPicBuff_addr=0x%x, ctrlParam_addr=0x%x, outPicBuff_addr=0x%x, picInfo_addr=0x%x)",
|
||||
cellVpost.Log("cellVpostExec(handle=0x%x, inPicBuff_addr=0x%x, ctrlParam_addr=0x%x, outPicBuff_addr=0x%x, picInfo_addr=0x%x)",
|
||||
handle, inPicBuff_addr, ctrlParam.GetAddr(), outPicBuff_addr, picInfo.GetAddr());
|
||||
|
||||
VpostInstance* vpost;
|
||||
|
|
Loading…
Add table
Reference in a new issue