added triple buffering option to advanced settings (potentially as an optimization for some cards) added scene buffering time option to advanced settings (mainly for sound devices) moved "Use CBR" to encoding section, made buffer size automatically default to the same as bitrate unless "Use Custom Buffer Size" is checked
916 lines
29 KiB
C++
916 lines
29 KiB
C++
/********************************************************************************
|
|
Copyright (C) 2012 Hugh Bailey <obs.jim@gmail.com>
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation; either version 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program 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 General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program; if not, write to the Free Software
|
|
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
|
|
********************************************************************************/
|
|
|
|
|
|
#include "Main.h"
|
|
|
|
#include <inttypes.h>
|
|
extern "C"
|
|
{
|
|
#include "../x264/x264.h"
|
|
}
|
|
|
|
|
|
void Convert444to420(LPBYTE input, int width, int pitch, int height, int startY, int endY, LPBYTE *output, bool bSSE2Available);
|
|
|
|
|
|
DWORD STDCALL OBS::MainCaptureThread(LPVOID lpUnused)
|
|
{
|
|
App->MainCaptureLoop();
|
|
return 0;
|
|
}
|
|
|
|
|
|
struct Convert444Data
|
|
{
|
|
LPBYTE input;
|
|
LPBYTE output[3];
|
|
bool bKillThread;
|
|
HANDLE hSignalConvert, hSignalComplete;
|
|
int width, height, pitch, startY, endY;
|
|
};
|
|
|
|
DWORD STDCALL Convert444Thread(Convert444Data *data)
|
|
{
|
|
do
|
|
{
|
|
WaitForSingleObject(data->hSignalConvert, INFINITE);
|
|
if(data->bKillThread) break;
|
|
|
|
Convert444to420(data->input, data->width, data->pitch, data->height, data->startY, data->endY, data->output, App->SSE2Available());
|
|
|
|
SetEvent(data->hSignalComplete);
|
|
}while(!data->bKillThread);
|
|
|
|
return 0;
|
|
}
|
|
|
|
bool OBS::BufferVideoData(const List<DataPacket> &inputPackets, const List<PacketType> &inputTypes, DWORD timestamp, VideoSegment &segmentOut)
|
|
{
|
|
VideoSegment &segmentIn = *bufferedVideo.CreateNew();
|
|
segmentIn.ctsOffset = ctsOffset;
|
|
segmentIn.timestamp = timestamp;
|
|
|
|
segmentIn.packets.SetSize(inputPackets.Num());
|
|
for(UINT i=0; i<inputPackets.Num(); i++)
|
|
{
|
|
segmentIn.packets[i].data.CopyArray(inputPackets[i].lpPacket, inputPackets[i].size);
|
|
segmentIn.packets[i].type = inputTypes[i];
|
|
}
|
|
|
|
if((bufferedVideo.Last().timestamp-bufferedVideo[0].timestamp) >= UINT(App->bufferingTime))
|
|
{
|
|
segmentOut.packets.TransferFrom(bufferedVideo[0].packets);
|
|
segmentOut.ctsOffset = bufferedVideo[0].ctsOffset;
|
|
segmentOut.timestamp = bufferedVideo[0].timestamp;
|
|
bufferedVideo.Remove(0);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
#define NUM_OUT_BUFFERS 3
|
|
|
|
struct FrameProcessInfo
|
|
{
|
|
x264_picture_t *picOut;
|
|
ID3D10Texture2D *prevTexture;
|
|
|
|
DWORD frameTimestamp;
|
|
QWORD firstFrameTime;
|
|
};
|
|
|
|
bool OBS::ProcessFrame(FrameProcessInfo &frameInfo)
|
|
{
|
|
List<DataPacket> videoPackets;
|
|
List<PacketType> videoPacketTypes;
|
|
|
|
//------------------------------------
|
|
// encode
|
|
|
|
bufferedTimes << frameInfo.frameTimestamp;
|
|
|
|
VideoSegment curSegment;
|
|
bool bProcessedFrame, bSendFrame = false;
|
|
int curCTSOffset = 0;
|
|
|
|
profileIn("call to encoder");
|
|
|
|
videoEncoder->Encode(frameInfo.picOut, videoPackets, videoPacketTypes, bufferedTimes[0], ctsOffset);
|
|
if(bUsing444) frameInfo.prevTexture->Unmap(0);
|
|
|
|
ctsOffsets << ctsOffset;
|
|
|
|
bProcessedFrame = (videoPackets.Num() != 0);
|
|
|
|
//buffer video data before sending out
|
|
if(bProcessedFrame)
|
|
{
|
|
bSendFrame = BufferVideoData(videoPackets, videoPacketTypes, bufferedTimes[0], curSegment);
|
|
bufferedTimes.Remove(0);
|
|
|
|
curCTSOffset = ctsOffsets[0];
|
|
ctsOffsets.Remove(0);
|
|
}
|
|
|
|
profileOut;
|
|
|
|
//------------------------------------
|
|
// upload
|
|
|
|
profileIn("sending stuff out");
|
|
|
|
//send headers before the first frame if not yet sent
|
|
if(bSendFrame)
|
|
{
|
|
if(!bSentHeaders)
|
|
{
|
|
network->BeginPublishing();
|
|
bSentHeaders = true;
|
|
}
|
|
|
|
OSEnterMutex(hSoundDataMutex);
|
|
|
|
if(pendingAudioFrames.Num())
|
|
{
|
|
while(pendingAudioFrames.Num())
|
|
{
|
|
if(frameInfo.firstFrameTime < pendingAudioFrames[0].timestamp)
|
|
{
|
|
UINT audioTimestamp = UINT(pendingAudioFrames[0].timestamp-frameInfo.firstFrameTime);
|
|
|
|
if(bFirstAudioPacket)
|
|
{
|
|
audioTimestamp = 0;
|
|
bFirstAudioPacket = false;
|
|
}
|
|
else
|
|
audioTimestamp += curCTSOffset;
|
|
|
|
//stop sending audio packets when we reach an audio timestamp greater than the video timestamp
|
|
if(audioTimestamp > curSegment.timestamp)
|
|
break;
|
|
|
|
if(audioTimestamp == 0 || audioTimestamp > lastAudioTimestamp)
|
|
{
|
|
List<BYTE> &audioData = pendingAudioFrames[0].audioData;
|
|
if(audioData.Num())
|
|
{
|
|
//Log(TEXT("a:%u, %llu, cts: %d"), audioTimestamp, frameInfo.firstFrameTime+audioTimestamp-curCTSOffset, curCTSOffset);
|
|
|
|
network->SendPacket(audioData.Array(), audioData.Num(), audioTimestamp, PacketType_Audio);
|
|
if(fileStream)
|
|
fileStream->AddPacket(audioData.Array(), audioData.Num(), audioTimestamp, PacketType_Audio);
|
|
|
|
audioData.Clear();
|
|
|
|
lastAudioTimestamp = audioTimestamp;
|
|
}
|
|
}
|
|
}
|
|
|
|
pendingAudioFrames[0].audioData.Clear();
|
|
pendingAudioFrames.Remove(0);
|
|
}
|
|
}
|
|
|
|
OSLeaveMutex(hSoundDataMutex);
|
|
|
|
for(UINT i=0; i<curSegment.packets.Num(); i++)
|
|
{
|
|
VideoPacketData &packet = curSegment.packets[i];
|
|
|
|
//Log(TEXT("v:%u, %llu"), curSegment.timestamp, frameInfo.firstFrameTime+curSegment.timestamp);
|
|
|
|
network->SendPacket(packet.data.Array(), packet.data.Num(), curSegment.timestamp, packet.type);
|
|
if(fileStream)
|
|
fileStream->AddPacket(packet.data.Array(), packet.data.Num(), curSegment.timestamp, packet.type);
|
|
}
|
|
}
|
|
|
|
profileOut;
|
|
|
|
return bProcessedFrame;
|
|
}
|
|
|
|
|
|
//#define USE_100NS_TIME 1
|
|
|
|
#ifdef USE_100NS_TIME
|
|
void STDCALL SleepTo(LONGLONG clockFreq, QWORD qw100NSTime)
|
|
{
|
|
QWORD t = GetQPCTime100NS(clockFreq);
|
|
|
|
unsigned int milliseconds = (unsigned int)((qw100NSTime - t)/10000);
|
|
if (milliseconds > 1) //also accounts for windows 8 sleep problem
|
|
Sleep(--milliseconds);
|
|
|
|
for (;;)
|
|
{
|
|
t = GetQPCTime100NS(clockFreq);
|
|
if (t >= qw100NSTime)
|
|
return;
|
|
Sleep(0);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
|
|
//todo: this function is an abomination, this is just disgusting. fix it.
|
|
//...seriously, this is really, really horrible. I mean this is amazingly bad.
|
|
void OBS::MainCaptureLoop()
|
|
{
|
|
int curRenderTarget = 0, curYUVTexture = 0, curCopyTexture = 0;
|
|
int copyWait = numRenderBuffers-1;
|
|
|
|
bSentHeaders = false;
|
|
bFirstAudioPacket = true;
|
|
|
|
Vect2 baseSize = Vect2(float(baseCX), float(baseCY));
|
|
Vect2 outputSize = Vect2(float(outputCX), float(outputCY));
|
|
Vect2 scaleSize = Vect2(float(scaleCX), float(scaleCY));
|
|
|
|
HANDLE hScaleVal = yuvScalePixelShader->GetParameterByName(TEXT("baseDimensionI"));
|
|
|
|
LPVOID nullBuff = NULL;
|
|
|
|
//----------------------------------------
|
|
// x264 input buffers
|
|
|
|
int curOutBuffer = 0;
|
|
|
|
x264_picture_t *lastPic = NULL;
|
|
x264_picture_t outPics[NUM_OUT_BUFFERS];
|
|
DWORD outTimes[NUM_OUT_BUFFERS] = {0, 0, 0};
|
|
|
|
for(int i=0; i<NUM_OUT_BUFFERS; i++)
|
|
x264_picture_init(&outPics[i]);
|
|
|
|
if(bUsing444)
|
|
{
|
|
for(int i=0; i<NUM_OUT_BUFFERS; i++)
|
|
{
|
|
outPics[i].img.i_csp = X264_CSP_BGRA; //although the x264 input says BGR, x264 actually will expect packed UYV
|
|
outPics[i].img.i_plane = 1;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for(int i=0; i<NUM_OUT_BUFFERS; i++)
|
|
x264_picture_alloc(&outPics[i], X264_CSP_I420, outputCX, outputCY);
|
|
}
|
|
|
|
//----------------------------------------
|
|
// time/timestamp stuff
|
|
|
|
LARGE_INTEGER clockFreq;
|
|
QueryPerformanceFrequency(&clockFreq);
|
|
|
|
bufferedTimes.Clear();
|
|
ctsOffsets.Clear();
|
|
|
|
#ifdef USE_100NS_TIME
|
|
QWORD streamTimeStart = GetQPCTime100NS(clockFreq.QuadPart);
|
|
QWORD frameTime100ns = 10000000/fps;
|
|
|
|
QWORD sleepTargetTime = 0;
|
|
bool bWasLaggedFrame = false;
|
|
#else
|
|
DWORD streamTimeStart = OSGetTime();
|
|
#endif
|
|
totalStreamTime = 0;
|
|
|
|
lastAudioTimestamp = 0;
|
|
|
|
latestVideoTime = firstSceneTimestamp = GetQPCTimeMS(clockFreq.QuadPart);
|
|
|
|
DWORD fpsTimeNumerator = 1000-(frameTime*fps);
|
|
DWORD fpsTimeDenominator = fps;
|
|
DWORD fpsTimeAdjust = 0;
|
|
|
|
DWORD cfrTime = 0;
|
|
DWORD cfrTimeAdjust = 0;
|
|
|
|
//----------------------------------------
|
|
// start audio capture streams
|
|
|
|
desktopAudio->StartCapture();
|
|
if(micAudio) micAudio->StartCapture();
|
|
|
|
//----------------------------------------
|
|
// status bar/statistics stuff
|
|
|
|
DWORD fpsCounter = 0;
|
|
|
|
int numLongFrames = 0;
|
|
int numTotalFrames = 0;
|
|
|
|
int numTotalDuplicatedFrames = 0;
|
|
|
|
bytesPerSec = 0;
|
|
captureFPS = 0;
|
|
curFramesDropped = 0;
|
|
curStrain = 0.0;
|
|
PostMessage(hwndMain, OBS_UPDATESTATUSBAR, 0, 0);
|
|
|
|
QWORD lastBytesSent[3] = {0, 0, 0};
|
|
DWORD lastFramesDropped = 0;
|
|
#ifdef USE_100NS_TIME
|
|
double bpsTime = 0.0;
|
|
#else
|
|
float bpsTime = 0.0f;
|
|
#endif
|
|
double lastStrain = 0.0f;
|
|
DWORD numSecondsWaited = 0;
|
|
|
|
//----------------------------------------
|
|
// 444->420 thread data
|
|
|
|
int numThreads = MAX(OSGetTotalCores()-2, 1);
|
|
HANDLE *h420Threads = (HANDLE*)Allocate(sizeof(HANDLE)*numThreads);
|
|
Convert444Data *convertInfo = (Convert444Data*)Allocate(sizeof(Convert444Data)*numThreads);
|
|
|
|
zero(h420Threads, sizeof(HANDLE)*numThreads);
|
|
zero(convertInfo, sizeof(Convert444Data)*numThreads);
|
|
|
|
for(int i=0; i<numThreads; i++)
|
|
{
|
|
convertInfo[i].width = outputCX;
|
|
convertInfo[i].height = outputCY;
|
|
convertInfo[i].hSignalConvert = CreateEvent(NULL, FALSE, FALSE, NULL);
|
|
convertInfo[i].hSignalComplete = CreateEvent(NULL, FALSE, FALSE, NULL);
|
|
|
|
if(i == 0)
|
|
convertInfo[i].startY = 0;
|
|
else
|
|
convertInfo[i].startY = convertInfo[i-1].endY;
|
|
|
|
if(i == (numThreads-1))
|
|
convertInfo[i].endY = outputCY;
|
|
else
|
|
convertInfo[i].endY = ((outputCY/numThreads)*(i+1)) & 0xFFFFFFFE;
|
|
}
|
|
|
|
bool bFirstFrame = true;
|
|
bool bFirstImage = true;
|
|
bool bFirst420Encode = true;
|
|
bool bUseThreaded420 = bUseMultithreadedOptimizations && (OSGetTotalCores() > 1) && !bUsing444;
|
|
|
|
List<HANDLE> completeEvents;
|
|
|
|
if(bUseThreaded420)
|
|
{
|
|
for(int i=0; i<numThreads; i++)
|
|
{
|
|
h420Threads[i] = OSCreateThread((XTHREAD)Convert444Thread, convertInfo+i);
|
|
completeEvents << convertInfo[i].hSignalComplete;
|
|
}
|
|
}
|
|
|
|
//----------------------------------------
|
|
|
|
QWORD curStreamTime = 0, lastStreamTime, firstFrameTime = GetQPCTimeMS(clockFreq.QuadPart);
|
|
lastStreamTime = firstFrameTime-frameTime;
|
|
|
|
bool bFirstAudioPacket = true;
|
|
|
|
while(bRunning)
|
|
{
|
|
#ifdef USE_100NS_TIME
|
|
QWORD renderStartTime = GetQPCTime100NS(clockFreq.QuadPart);
|
|
|
|
if(sleepTargetTime == 0 || bWasLaggedFrame)
|
|
sleepTargetTime = renderStartTime;
|
|
#else
|
|
DWORD renderStartTime = OSGetTime();
|
|
totalStreamTime = renderStartTime-streamTimeStart;
|
|
|
|
DWORD frameTimeAdjust = frameTime;
|
|
fpsTimeAdjust += fpsTimeNumerator;
|
|
if(fpsTimeAdjust > fpsTimeDenominator)
|
|
{
|
|
fpsTimeAdjust -= fpsTimeDenominator;
|
|
++frameTimeAdjust;
|
|
}
|
|
#endif
|
|
|
|
bool bRenderView = !IsIconic(hwndMain) && bRenderViewEnabled;
|
|
|
|
profileIn("frame");
|
|
|
|
#ifdef USE_100NS_TIME
|
|
QWORD qwTime = renderStartTime/10000;
|
|
latestVideoTime = qwTime;
|
|
|
|
QWORD frameDelta = renderStartTime-lastStreamTime;
|
|
double fSeconds = double(frameDelta)*0.0000001;
|
|
|
|
//Log(TEXT("frameDelta: %f"), fSeconds);
|
|
|
|
lastStreamTime = renderStartTime;
|
|
#else
|
|
QWORD qwTime = GetQPCTimeMS(clockFreq.QuadPart);
|
|
latestVideoTime = qwTime;
|
|
|
|
QWORD frameDelta = qwTime-lastStreamTime;
|
|
float fSeconds = float(frameDelta)*0.001f;
|
|
|
|
//Log(TEXT("frameDelta: %llu"), frameDelta);
|
|
|
|
lastStreamTime = qwTime;
|
|
#endif
|
|
|
|
if(!bPushToTalkDown && pushToTalkTimeLeft > 0)
|
|
{
|
|
pushToTalkTimeLeft -= int(frameDelta);
|
|
OSDebugOut(TEXT("time left: %d\r\n"), pushToTalkTimeLeft);
|
|
if(pushToTalkTimeLeft <= 0)
|
|
{
|
|
pushToTalkTimeLeft = 0;
|
|
bPushToTalkOn = false;
|
|
}
|
|
}
|
|
|
|
//------------------------------------
|
|
|
|
OSEnterMutex(hSceneMutex);
|
|
|
|
if(bResizeRenderView)
|
|
{
|
|
GS->ResizeView();
|
|
bResizeRenderView = false;
|
|
}
|
|
|
|
//------------------------------------
|
|
|
|
if(scene)
|
|
{
|
|
profileIn("scene->Preprocess");
|
|
scene->Preprocess();
|
|
|
|
for(UINT i=0; i<globalSources.Num(); i++)
|
|
globalSources[i].source->Preprocess();
|
|
|
|
profileOut;
|
|
|
|
scene->Tick(float(fSeconds));
|
|
|
|
for(UINT i=0; i<globalSources.Num(); i++)
|
|
globalSources[i].source->Tick(float(fSeconds));
|
|
}
|
|
|
|
//------------------------------------
|
|
|
|
QWORD curBytesSent = network->GetCurrentSentBytes();
|
|
curFramesDropped = network->NumDroppedFrames();
|
|
bool bUpdateBPS = false;
|
|
|
|
bpsTime += fSeconds;
|
|
if(bpsTime > 1.0f)
|
|
{
|
|
if(numSecondsWaited < 3)
|
|
++numSecondsWaited;
|
|
|
|
//bytesPerSec = DWORD(curBytesSent - lastBytesSent);
|
|
bytesPerSec = DWORD(curBytesSent - lastBytesSent[0]) / numSecondsWaited;
|
|
|
|
if(bpsTime > 2.0)
|
|
bpsTime = 0.0f;
|
|
else
|
|
bpsTime -= 1.0;
|
|
|
|
if(numSecondsWaited == 3)
|
|
{
|
|
lastBytesSent[0] = lastBytesSent[1];
|
|
lastBytesSent[1] = lastBytesSent[2];
|
|
lastBytesSent[2] = curBytesSent;
|
|
}
|
|
else
|
|
lastBytesSent[numSecondsWaited] = curBytesSent;
|
|
|
|
captureFPS = fpsCounter;
|
|
fpsCounter = 0;
|
|
|
|
bUpdateBPS = true;
|
|
}
|
|
|
|
fpsCounter++;
|
|
|
|
curStrain = network->GetPacketStrain();
|
|
|
|
EnableBlending(TRUE);
|
|
BlendFunction(GS_BLEND_SRCALPHA, GS_BLEND_INVSRCALPHA);
|
|
|
|
//------------------------------------
|
|
// render the mini render texture
|
|
|
|
LoadVertexShader(mainVertexShader);
|
|
LoadPixelShader(mainPixelShader);
|
|
|
|
SetRenderTarget(mainRenderTextures[curRenderTarget]);
|
|
|
|
Ortho(0.0f, baseSize.x, baseSize.y, 0.0f, -100.0f, 100.0f);
|
|
SetViewport(0, 0, baseSize.x, baseSize.y);
|
|
|
|
if(scene)
|
|
scene->Render();
|
|
|
|
//------------------------------------
|
|
|
|
if(bTransitioning)
|
|
{
|
|
if(!transitionTexture)
|
|
{
|
|
transitionTexture = CreateTexture(baseCX, baseCY, GS_BGRA, NULL, FALSE, TRUE);
|
|
if(transitionTexture)
|
|
{
|
|
D3D10Texture *d3dTransitionTex = static_cast<D3D10Texture*>(transitionTexture);
|
|
D3D10Texture *d3dSceneTex = static_cast<D3D10Texture*>(mainRenderTextures[lastRenderTarget]);
|
|
GetD3D()->CopyResource(d3dTransitionTex->texture, d3dSceneTex->texture);
|
|
}
|
|
else
|
|
bTransitioning = false;
|
|
}
|
|
else if(transitionAlpha >= 1.0f)
|
|
{
|
|
delete transitionTexture;
|
|
transitionTexture = NULL;
|
|
|
|
bTransitioning = false;
|
|
}
|
|
}
|
|
|
|
if(bTransitioning)
|
|
{
|
|
EnableBlending(TRUE);
|
|
transitionAlpha += float(fSeconds)*5.0f;
|
|
if(transitionAlpha > 1.0f)
|
|
transitionAlpha = 1.0f;
|
|
}
|
|
else
|
|
EnableBlending(FALSE);
|
|
|
|
//------------------------------------
|
|
// render the mini view thingy
|
|
|
|
if(bRenderView)
|
|
{
|
|
Vect2 renderFrameSize = Vect2(float(renderFrameWidth), float(renderFrameHeight));
|
|
|
|
SetRenderTarget(NULL);
|
|
|
|
LoadVertexShader(mainVertexShader);
|
|
LoadPixelShader(mainPixelShader);
|
|
|
|
Ortho(0.0f, renderFrameSize.x, renderFrameSize.y, 0.0f, -100.0f, 100.0f);
|
|
SetViewport(0.0f, 0.0f, renderFrameSize.x, renderFrameSize.y);
|
|
|
|
if(bTransitioning)
|
|
{
|
|
BlendFunction(GS_BLEND_ONE, GS_BLEND_ZERO);
|
|
DrawSprite(transitionTexture, 0xFFFFFFFF, 0.0f, 0.0f, renderFrameSize.x, renderFrameSize.y);
|
|
BlendFunction(GS_BLEND_FACTOR, GS_BLEND_INVFACTOR, transitionAlpha);
|
|
}
|
|
|
|
DrawSprite(mainRenderTextures[curRenderTarget], 0xFFFFFFFF, 0.0f, 0.0f, renderFrameSize.x, renderFrameSize.y);
|
|
|
|
Ortho(0.0f, renderFrameSize.x, renderFrameSize.y, 0.0f, -100.0f, 100.0f);
|
|
|
|
//draw selections if in edit mode
|
|
if(bEditMode && !bSizeChanging)
|
|
{
|
|
LoadVertexShader(solidVertexShader);
|
|
LoadPixelShader(solidPixelShader);
|
|
solidPixelShader->SetColor(solidPixelShader->GetParameter(0), 0xFFFF0000);
|
|
|
|
Ortho(0.0f, baseSize.x, baseSize.y, 0.0f, -100.0f, 100.0f);
|
|
|
|
if(scene)
|
|
scene->RenderSelections();
|
|
}
|
|
}
|
|
else if(bForceRenderViewErase)
|
|
{
|
|
InvalidateRect(hwndRenderFrame, NULL, TRUE);
|
|
UpdateWindow(hwndRenderFrame);
|
|
bForceRenderViewErase = false;
|
|
}
|
|
|
|
//------------------------------------
|
|
// actual stream output
|
|
|
|
LoadVertexShader(mainVertexShader);
|
|
LoadPixelShader(yuvScalePixelShader);
|
|
|
|
Texture *yuvRenderTexture = yuvRenderTextures[curRenderTarget];
|
|
SetRenderTarget(yuvRenderTexture);
|
|
|
|
yuvScalePixelShader->SetVector2(hScaleVal, 1.0f/baseSize);
|
|
|
|
Ortho(0.0f, outputSize.x, outputSize.y, 0.0f, -100.0f, 100.0f);
|
|
SetViewport(0.0f, 0.0f, outputSize.x, outputSize.y);
|
|
|
|
//why am I using scaleSize instead of outputSize for the texture?
|
|
//because outputSize can be trimmed by up to three pixels due to 128-bit alignment.
|
|
//using the scale function with outputSize can cause slightly inaccurate scaled images
|
|
if(bTransitioning)
|
|
{
|
|
BlendFunction(GS_BLEND_ONE, GS_BLEND_ZERO);
|
|
DrawSpriteEx(transitionTexture, 0xFFFFFFFF, 0.0f, 0.0f, scaleSize.x, scaleSize.y, 0.0f, 0.0f, scaleSize.x, scaleSize.y);
|
|
BlendFunction(GS_BLEND_FACTOR, GS_BLEND_INVFACTOR, transitionAlpha);
|
|
}
|
|
|
|
DrawSpriteEx(mainRenderTextures[curRenderTarget], 0xFFFFFFFF, 0.0f, 0.0f, outputSize.x, outputSize.y, 0.0f, 0.0f, outputSize.x, outputSize.y);
|
|
|
|
//------------------------------------
|
|
|
|
if(bRenderView && !copyWait)
|
|
static_cast<D3D10System*>(GS)->swap->Present(0, 0);
|
|
|
|
OSLeaveMutex(hSceneMutex);
|
|
|
|
//------------------------------------
|
|
// present/upload
|
|
|
|
profileIn("video encoding and uploading");
|
|
|
|
bool bEncode = true;
|
|
|
|
if(copyWait)
|
|
{
|
|
copyWait--;
|
|
bEncode = false;
|
|
}
|
|
else
|
|
{
|
|
//audio sometimes takes a bit to start -- do not start processing frames until audio has started capturing
|
|
if(!bRecievedFirstAudioFrame)
|
|
bEncode = false;
|
|
else if(bFirstFrame)
|
|
{
|
|
firstFrameTime = qwTime;
|
|
bFirstFrame = false;
|
|
}
|
|
|
|
if(!bEncode)
|
|
{
|
|
if(curYUVTexture == (numRenderBuffers-1))
|
|
curYUVTexture = 0;
|
|
else
|
|
curYUVTexture++;
|
|
}
|
|
}
|
|
|
|
if(bEncode)
|
|
{
|
|
curStreamTime = qwTime-firstFrameTime;
|
|
|
|
UINT prevCopyTexture = (curCopyTexture == 0) ? numRenderBuffers-1 : curCopyTexture-1;
|
|
|
|
ID3D10Texture2D *copyTexture = copyTextures[curCopyTexture];
|
|
profileIn("CopyResource");
|
|
|
|
if(!bFirst420Encode && bUseThreaded420)
|
|
{
|
|
WaitForMultipleObjects(completeEvents.Num(), completeEvents.Array(), TRUE, INFINITE);
|
|
copyTexture->Unmap(0);
|
|
}
|
|
|
|
D3D10Texture *d3dYUV = static_cast<D3D10Texture*>(yuvRenderTextures[curYUVTexture]);
|
|
GetD3D()->CopyResource(copyTexture, d3dYUV->texture);
|
|
profileOut;
|
|
|
|
ID3D10Texture2D *prevTexture = copyTextures[prevCopyTexture];
|
|
|
|
if(bFirstImage) //ignore the first frame
|
|
bFirstImage = false;
|
|
else
|
|
{
|
|
D3D10_MAPPED_TEXTURE2D map;
|
|
if(SUCCEEDED(prevTexture->Map(0, D3D10_MAP_READ, 0, &map)))
|
|
{
|
|
int prevOutBuffer = (curOutBuffer == 0) ? NUM_OUT_BUFFERS-1 : curOutBuffer-1;
|
|
int nextOutBuffer = (curOutBuffer == NUM_OUT_BUFFERS-1) ? 0 : curOutBuffer+1;
|
|
|
|
x264_picture_t &prevPicOut = outPics[prevOutBuffer];
|
|
x264_picture_t &picOut = outPics[curOutBuffer];
|
|
x264_picture_t &nextPicOut = outPics[nextOutBuffer];
|
|
|
|
if(!bUsing444)
|
|
{
|
|
profileIn("conversion to 4:2:0");
|
|
|
|
if(bUseThreaded420)
|
|
{
|
|
outTimes[nextOutBuffer] = (DWORD)curStreamTime;
|
|
|
|
for(int i=0; i<numThreads; i++)
|
|
{
|
|
convertInfo[i].input = (LPBYTE)map.pData;
|
|
convertInfo[i].pitch = map.RowPitch;
|
|
convertInfo[i].output[0] = nextPicOut.img.plane[0];
|
|
convertInfo[i].output[1] = nextPicOut.img.plane[1];
|
|
convertInfo[i].output[2] = nextPicOut.img.plane[2];
|
|
SetEvent(convertInfo[i].hSignalConvert);
|
|
}
|
|
|
|
if(bFirst420Encode)
|
|
bFirst420Encode = bEncode = false;
|
|
}
|
|
else
|
|
{
|
|
outTimes[curOutBuffer] = (DWORD)curStreamTime;
|
|
Convert444to420((LPBYTE)map.pData, outputCX, map.RowPitch, outputCY, 0, outputCY, picOut.img.plane, SSE2Available());
|
|
prevTexture->Unmap(0);
|
|
}
|
|
|
|
profileOut;
|
|
}
|
|
else
|
|
{
|
|
picOut.img.i_stride[0] = map.RowPitch;
|
|
picOut.img.plane[0] = (uint8_t*)map.pData;
|
|
}
|
|
|
|
if(bEncode)
|
|
{
|
|
DWORD curFrameTimestamp = outTimes[prevOutBuffer];
|
|
//Log(TEXT("curFrameTimestamp: %u"), curFrameTimestamp);
|
|
|
|
//------------------------------------
|
|
|
|
FrameProcessInfo frameInfo;
|
|
frameInfo.firstFrameTime = firstFrameTime;
|
|
frameInfo.prevTexture = prevTexture;
|
|
|
|
if(bUseCFR)
|
|
{
|
|
while(cfrTime < curFrameTimestamp)
|
|
{
|
|
DWORD frameTimeAdjust = frameTime;
|
|
cfrTimeAdjust += fpsTimeNumerator;
|
|
if(cfrTimeAdjust > fpsTimeDenominator)
|
|
{
|
|
cfrTimeAdjust -= fpsTimeDenominator;
|
|
++frameTimeAdjust;
|
|
}
|
|
|
|
DWORD halfTime = (frameTimeAdjust+1)/2;
|
|
|
|
x264_picture_t *nextPic = (curFrameTimestamp-cfrTime <= halfTime) ? &picOut : &prevPicOut;
|
|
//Log(TEXT("cfrTime: %u, time: %u"), cfrTime, curFrameTimestamp);
|
|
|
|
//these lines are just for counting duped frames
|
|
if(nextPic == lastPic)
|
|
++numTotalDuplicatedFrames;
|
|
else
|
|
lastPic = nextPic;
|
|
|
|
frameInfo.picOut = nextPic;
|
|
frameInfo.picOut->i_pts = cfrTime;
|
|
frameInfo.frameTimestamp = cfrTime;
|
|
ProcessFrame(frameInfo);
|
|
|
|
cfrTime += frameTimeAdjust;
|
|
|
|
//Log(TEXT("cfrTime: %u, chi frame: %u"), cfrTime, (curFrameTimestamp-cfrTime <= halfTime));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
picOut.i_pts = curFrameTimestamp;
|
|
|
|
frameInfo.picOut = &picOut;
|
|
frameInfo.frameTimestamp = curFrameTimestamp;
|
|
|
|
ProcessFrame(frameInfo);
|
|
}
|
|
}
|
|
|
|
curOutBuffer = nextOutBuffer;
|
|
}
|
|
}
|
|
|
|
if(curCopyTexture == (numRenderBuffers-1))
|
|
curCopyTexture = 0;
|
|
else
|
|
curCopyTexture++;
|
|
|
|
if(curYUVTexture == (numRenderBuffers-1))
|
|
curYUVTexture = 0;
|
|
else
|
|
curYUVTexture++;
|
|
}
|
|
|
|
lastRenderTarget = curRenderTarget;
|
|
|
|
if(curRenderTarget == (numRenderBuffers-1))
|
|
curRenderTarget = 0;
|
|
else
|
|
curRenderTarget++;
|
|
|
|
if(bUpdateBPS || !CloseDouble(curStrain, lastStrain) || curFramesDropped != lastFramesDropped)
|
|
{
|
|
PostMessage(hwndMain, OBS_UPDATESTATUSBAR, 0, 0);
|
|
lastStrain = curStrain;
|
|
|
|
lastFramesDropped = curFramesDropped;
|
|
}
|
|
|
|
profileOut;
|
|
profileOut;
|
|
|
|
//------------------------------------
|
|
// get audio while sleeping or capturing
|
|
//ReleaseSemaphore(hRequestAudioEvent, 1, NULL);
|
|
|
|
//------------------------------------
|
|
// frame sync
|
|
|
|
#ifdef USE_100NS_TIME
|
|
QWORD renderStopTime = GetQPCTime100NS(clockFreq.QuadPart);
|
|
sleepTargetTime += frameTime100ns;
|
|
|
|
if(bWasLaggedFrame = (sleepTargetTime <= renderStopTime))
|
|
numLongFrames++;
|
|
else
|
|
SleepTo(clockFreq.QuadPart, sleepTargetTime);
|
|
#else
|
|
DWORD renderStopTime = OSGetTime();
|
|
DWORD totalTime = renderStopTime-renderStartTime;
|
|
|
|
if(totalTime > frameTimeAdjust)
|
|
numLongFrames++;
|
|
else if(totalTime < frameTimeAdjust)
|
|
OSSleep(frameTimeAdjust-totalTime);
|
|
#endif
|
|
|
|
//OSDebugOut(TEXT("Frame adjust time: %d, "), frameTimeAdjust-totalTime);
|
|
|
|
numTotalFrames++;
|
|
}
|
|
|
|
if(!bUsing444)
|
|
{
|
|
if(bUseThreaded420)
|
|
{
|
|
for(int i=0; i<numThreads; i++)
|
|
{
|
|
if(h420Threads[i])
|
|
{
|
|
convertInfo[i].bKillThread = true;
|
|
SetEvent(convertInfo[i].hSignalConvert);
|
|
|
|
OSTerminateThread(h420Threads[i], 10000);
|
|
h420Threads[i] = NULL;
|
|
}
|
|
|
|
if(convertInfo[i].hSignalConvert)
|
|
{
|
|
CloseHandle(convertInfo[i].hSignalConvert);
|
|
convertInfo[i].hSignalConvert = NULL;
|
|
}
|
|
|
|
if(convertInfo[i].hSignalComplete)
|
|
{
|
|
CloseHandle(convertInfo[i].hSignalComplete);
|
|
convertInfo[i].hSignalComplete = NULL;
|
|
}
|
|
}
|
|
|
|
if(!bFirst420Encode)
|
|
{
|
|
ID3D10Texture2D *copyTexture = copyTextures[curCopyTexture];
|
|
copyTexture->Unmap(0);
|
|
}
|
|
}
|
|
|
|
for(int i=0; i<NUM_OUT_BUFFERS; i++)
|
|
x264_picture_clean(&outPics[i]);
|
|
}
|
|
|
|
Free(h420Threads);
|
|
Free(convertInfo);
|
|
|
|
Log(TEXT("Total frames rendered: %d, number of frames that lagged: %d (%0.2f%%) (it's okay for some frames to lag)"), numTotalFrames, numLongFrames, (double(numLongFrames)/double(numTotalFrames))*100.0);
|
|
if(bUseCFR)
|
|
Log(TEXT("Total duplicated CFR frames: %d"), numTotalDuplicatedFrames);
|
|
}
|