Move video decoding and subtitle rendering to a worker thread
Makes the click event handler for the grid no longer slow when video autoscroll is enabled, making it harder to accidently select multiple lines. Makes seeking speed no longer limited by decoding/rendering speed; seeking faster than video can be decoded simply results in dropped frames. Makes editing the file while a slow-rendering frame is visible far more responsive. Originally committed to SVN as r4702.
This commit is contained in:
parent
926b6152f1
commit
397b234fba
10 changed files with 461 additions and 275 deletions
|
@ -1383,14 +1383,6 @@
|
|||
RelativePath="..\..\src\export_framerate.h"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath="..\..\src\export_visible_lines.cpp"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath="..\..\src\export_visible_lines.h"
|
||||
>
|
||||
</File>
|
||||
</Filter>
|
||||
<Filter
|
||||
Name="Video backend"
|
||||
|
@ -1403,6 +1395,14 @@
|
|||
RelativePath="..\..\src\keyframe.h"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath="..\..\src\threaded_frame_source.cpp"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath="..\..\src\threaded_frame_source.h"
|
||||
>
|
||||
</File>
|
||||
<File
|
||||
RelativePath="..\..\src\video_frame.cpp"
|
||||
>
|
||||
|
|
|
@ -251,7 +251,6 @@ aegisub_2_2_SOURCES = \
|
|||
export_clean_info.cpp \
|
||||
export_fixstyle.cpp \
|
||||
export_framerate.cpp \
|
||||
export_visible_lines.cpp \
|
||||
fft.cpp \
|
||||
frame_main.cpp \
|
||||
frame_main_events.cpp \
|
||||
|
@ -293,6 +292,7 @@ aegisub_2_2_SOURCES = \
|
|||
thesaurus.cpp \
|
||||
thesaurus_myspell.cpp \
|
||||
timeedit_ctrl.cpp \
|
||||
threaded_frame_source.cpp \
|
||||
toggle_bitmap.cpp \
|
||||
tooltip_manager.cpp \
|
||||
utils.cpp \
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
// Copyright (c) 2005, Niels Martin Hansen
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright notice,
|
||||
// this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
// * Neither the name of the Aegisub Group nor the names of its contributors
|
||||
// may be used to endorse or promote products derived from this software
|
||||
// without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
// POSSIBILITY OF SUCH DAMAGE.
|
||||
//
|
||||
// Aegisub Project http://www.aegisub.org/
|
||||
//
|
||||
// $Id$
|
||||
|
||||
/// @file export_visible_lines.cpp
|
||||
/// @brief Limit to Visible Lines export filter
|
||||
/// @ingroup export
|
||||
///
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "ass_dialogue.h"
|
||||
#include "ass_file.h"
|
||||
#include "ass_override.h"
|
||||
#include "export_visible_lines.h"
|
||||
#include "video_context.h"
|
||||
|
||||
/// @brief Constructor
|
||||
///
|
||||
AssLimitToVisibleFilter::AssLimitToVisibleFilter() {
|
||||
initialized = false;
|
||||
frame = -1;
|
||||
}
|
||||
|
||||
void AssLimitToVisibleFilter::Init() {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
autoExporter = true;
|
||||
hidden = false;
|
||||
Register(_("Limit to Visible Lines"),1000000);
|
||||
description = _("Limit to Visible Lines");
|
||||
}
|
||||
|
||||
/// @brief Process
|
||||
/// @param subs
|
||||
/// @param export_dialog
|
||||
void AssLimitToVisibleFilter::ProcessSubs(AssFile *subs, wxWindow *) {
|
||||
if (frame == -1) return;
|
||||
|
||||
int time = VideoContext::Get()->TimeAtFrame(frame);
|
||||
|
||||
for (entryIter cur = subs->Line.begin(); cur != subs->Line.end(); ) {
|
||||
AssDialogue *diag = dynamic_cast<AssDialogue*>(*cur);
|
||||
if (diag && (diag->Start.GetMS() > time || diag->End.GetMS() <= time)) {
|
||||
delete *cur;
|
||||
cur = subs->Line.erase(cur);
|
||||
}
|
||||
else {
|
||||
++cur;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief Set limitation time
|
||||
/// @param _frame
|
||||
///
|
||||
void AssLimitToVisibleFilter::SetFrame(int _frame) {
|
||||
instance.frame = _frame;
|
||||
}
|
||||
|
||||
/// DOCME
|
||||
AssLimitToVisibleFilter AssLimitToVisibleFilter::instance;
|
|
@ -1,68 +0,0 @@
|
|||
// Copyright (c) 2007, Rodrigo Braz Monteiro
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright notice,
|
||||
// this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
// * Neither the name of the Aegisub Group nor the names of its contributors
|
||||
// may be used to endorse or promote products derived from this software
|
||||
// without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
// POSSIBILITY OF SUCH DAMAGE.
|
||||
//
|
||||
// Aegisub Project http://www.aegisub.org/
|
||||
//
|
||||
// $Id$
|
||||
|
||||
/// @file export_visible_lines.h
|
||||
/// @see export_visible_lines.cpp
|
||||
/// @ingroup export
|
||||
///
|
||||
|
||||
|
||||
|
||||
|
||||
///////////
|
||||
// Headers
|
||||
#include "ass_export_filter.h"
|
||||
|
||||
|
||||
/// DOCME
|
||||
/// @class AssLimitToVisibleFilter
|
||||
/// @brief DOCME
|
||||
///
|
||||
/// DOCME
|
||||
class AssLimitToVisibleFilter : public AssExportFilter {
|
||||
private:
|
||||
|
||||
/// DOCME
|
||||
static AssLimitToVisibleFilter instance;
|
||||
|
||||
/// DOCME
|
||||
int frame;
|
||||
|
||||
AssLimitToVisibleFilter();
|
||||
void Init();
|
||||
|
||||
public:
|
||||
static void SetFrame(int frame=-1);
|
||||
|
||||
void ProcessSubs(AssFile *subs, wxWindow *export_dialog);
|
||||
};
|
||||
|
||||
|
212
aegisub/src/threaded_frame_source.cpp
Normal file
212
aegisub/src/threaded_frame_source.cpp
Normal file
|
@ -0,0 +1,212 @@
|
|||
// Copyright (c) 2010, Thomas Goyne <plorkyeran@aegisub.org>
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright notice,
|
||||
// this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
// * Neither the name of the Aegisub Group nor the names of its contributors
|
||||
// may be used to endorse or promote products derived from this software
|
||||
// without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
// POSSIBILITY OF SUCH DAMAGE.
|
||||
//
|
||||
// Aegisub Project http://www.aegisub.org/
|
||||
//
|
||||
// $Id$
|
||||
|
||||
/// @file threaded_frame_source.cpp
|
||||
/// @see threaded_frame_source.h
|
||||
/// @ingroup video
|
||||
///
|
||||
|
||||
#include "threaded_frame_source.h"
|
||||
|
||||
#ifndef AGI_PRE
|
||||
#include <iterator>
|
||||
#include <functional>
|
||||
#endif
|
||||
|
||||
#include "ass_dialogue.h"
|
||||
#include "ass_exporter.h"
|
||||
#include "ass_file.h"
|
||||
#include "compat.h"
|
||||
#include "subtitles_provider_manager.h"
|
||||
#include "video_provider_manager.h"
|
||||
|
||||
// Test if a line is a dialogue line which is not visible at the given time
|
||||
struct invisible_line : public std::unary_function<const AssEntry*, bool> {
|
||||
double time;
|
||||
invisible_line(double time) : time(time * 1000.) { }
|
||||
bool operator()(const AssEntry *entry) const {
|
||||
const AssDialogue *diag = dynamic_cast<const AssDialogue*>(entry);
|
||||
return diag && (diag->Start.GetMS() > time || diag->End.GetMS() <= time);
|
||||
}
|
||||
};
|
||||
|
||||
AegiVideoFrame const& ThreadedFrameSource::ProcFrame(int frameNum, double time, bool raw) {
|
||||
AegiVideoFrame *frame;
|
||||
{
|
||||
wxMutexLocker locker(providerMutex);
|
||||
frame = frameBuffer + frameBufferIdx;
|
||||
frameBufferIdx = !frameBufferIdx;
|
||||
try {
|
||||
frame->CopyFrom(videoProvider->GetFrame(frameNum));
|
||||
}
|
||||
catch (const wchar_t *err) { throw VideoProviderErrorEvent(err); }
|
||||
catch (wxString const& err) { throw VideoProviderErrorEvent(err); }
|
||||
}
|
||||
|
||||
// This deliberately results in a call to LoadSubtitles while a render
|
||||
// is pending making the queued render use the new file
|
||||
if (!raw) {
|
||||
try {
|
||||
wxMutexLocker locker(fileMutex);
|
||||
if (subs.get() && singleFrame != frameNum) {
|
||||
// Generally edits and seeks come in groups; if the last thing done
|
||||
// was seek it is more likely that the user will seek again and
|
||||
// vice versa. As such, if this is the first frame requested after
|
||||
// an edit, only export the currently visible lines (because the
|
||||
// other lines will probably not be viewed before the file changes
|
||||
// again), and if it's a different frame, export the entire file.
|
||||
if (singleFrame == -1) {
|
||||
singleFrame = frameNum;
|
||||
// Copying a nontrivially sized AssFile is fairly slow, so
|
||||
// instead muck around with its innards to just temporarily
|
||||
// remove the non-visible lines without deleting them
|
||||
std::list<AssEntry*> visible;
|
||||
std::remove_copy_if(subs->Line.begin(), subs->Line.end(),
|
||||
std::back_inserter(visible),
|
||||
invisible_line(time));
|
||||
try {
|
||||
std::swap(subs->Line, visible);
|
||||
provider->LoadSubtitles(subs.get());
|
||||
}
|
||||
catch(...) {
|
||||
std::swap(subs->Line, visible);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
else {
|
||||
provider->LoadSubtitles(subs.get());
|
||||
subs.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const wchar_t *err) { throw SubtitlesProviderErrorEvent(err); }
|
||||
catch (wxString const& err) { throw SubtitlesProviderErrorEvent(err); }
|
||||
|
||||
provider->DrawSubtitles(*frame, time);
|
||||
}
|
||||
return *frame;
|
||||
}
|
||||
|
||||
void *ThreadedFrameSource::Entry() {
|
||||
while (!TestDestroy() && run) {
|
||||
jobMutex.Lock();
|
||||
if (nextSubs.get()) {
|
||||
wxMutexLocker locker(fileMutex);
|
||||
subs = nextSubs;
|
||||
singleFrame = -1;
|
||||
}
|
||||
if (nextTime == -1.) {
|
||||
jobReady.Wait();
|
||||
continue;
|
||||
}
|
||||
|
||||
double time = nextTime;
|
||||
int frameNum = nextFrame;
|
||||
nextTime = -1.;
|
||||
jobMutex.Unlock();
|
||||
|
||||
try {
|
||||
AegiVideoFrame const& frame = ProcFrame(frameNum, time);
|
||||
|
||||
std::tr1::shared_ptr<wxMutexLocker> evtLock(new wxMutexLocker(evtMutex));
|
||||
FrameReadyEvent *evt = new FrameReadyEvent(&frame, time, evtLock);
|
||||
evt->SetEventType(EVT_FRAME_READY);
|
||||
parent->QueueEvent(evt);
|
||||
}
|
||||
catch (wxEvent const& err) {
|
||||
// Pass error back to parent thread
|
||||
parent->QueueEvent(err.Clone());
|
||||
}
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
ThreadedFrameSource::ThreadedFrameSource(wxString videoFileName, wxEvtHandler *parent)
|
||||
: wxThread()
|
||||
, provider(SubtitlesProviderFactoryManager::GetProvider())
|
||||
, videoProvider(VideoProviderFactoryManager::GetProvider(videoFileName))
|
||||
, parent(parent)
|
||||
, nextTime(-1.)
|
||||
, jobReady(jobMutex)
|
||||
, frameBufferIdx(0)
|
||||
, run(true)
|
||||
{
|
||||
Create();
|
||||
Run();
|
||||
}
|
||||
|
||||
void ThreadedFrameSource::LoadSubtitles(AssFile *subs) {
|
||||
AssExporter exporter(subs);
|
||||
exporter.AddAutoFilters();
|
||||
AssFile *exported = exporter.ExportTransform();
|
||||
wxMutexLocker locker(jobMutex);
|
||||
// Set nextSubs and let the worker thread move it to subs so that we don't
|
||||
// have to lock fileMutex on the GUI thread, as that can be locked for
|
||||
// extended periods of time with slow-rendering subtitles
|
||||
nextSubs.reset(exported);
|
||||
}
|
||||
|
||||
void ThreadedFrameSource::RequestFrame(int frame, double time) {
|
||||
wxMutexLocker locker(jobMutex);
|
||||
nextTime = time;
|
||||
nextFrame = frame;
|
||||
jobReady.Signal();
|
||||
}
|
||||
|
||||
AegiVideoFrame const& ThreadedFrameSource::GetFrame(int frame, double time, bool raw) {
|
||||
return ProcFrame(frame, time, raw);
|
||||
}
|
||||
|
||||
void ThreadedFrameSource::End() {
|
||||
run = false;
|
||||
jobReady.Signal();
|
||||
}
|
||||
|
||||
ThreadedFrameSource::~ThreadedFrameSource() {
|
||||
frameBuffer[0].Clear();
|
||||
frameBuffer[1].Clear();
|
||||
}
|
||||
|
||||
wxDEFINE_EVENT(EVT_FRAME_READY, FrameReadyEvent);
|
||||
wxDEFINE_EVENT(EVT_VIDEO_ERROR, VideoProviderErrorEvent);
|
||||
wxDEFINE_EVENT(EVT_SUBTITLES_ERROR, SubtitlesProviderErrorEvent);
|
||||
|
||||
VideoProviderErrorEvent::VideoProviderErrorEvent(wxString msg)
|
||||
: agi::Exception(STD_STR(msg), NULL)
|
||||
{
|
||||
SetEventType(EVT_VIDEO_ERROR);
|
||||
}
|
||||
SubtitlesProviderErrorEvent::SubtitlesProviderErrorEvent(wxString msg)
|
||||
: agi::Exception(STD_STR(msg), NULL)
|
||||
{
|
||||
SetEventType(EVT_SUBTITLES_ERROR);
|
||||
}
|
171
aegisub/src/threaded_frame_source.h
Normal file
171
aegisub/src/threaded_frame_source.h
Normal file
|
@ -0,0 +1,171 @@
|
|||
// Copyright (c) 2010, Thomas Goyne <plorkyeran@aegisub.org>
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright notice,
|
||||
// this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above copyright notice,
|
||||
// this list of conditions and the following disclaimer in the documentation
|
||||
// and/or other materials provided with the distribution.
|
||||
// * Neither the name of the Aegisub Group nor the names of its contributors
|
||||
// may be used to endorse or promote products derived from this software
|
||||
// without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
// POSSIBILITY OF SUCH DAMAGE.
|
||||
//
|
||||
// Aegisub Project http://www.aegisub.org/
|
||||
//
|
||||
// $Id$
|
||||
|
||||
/// @file threaded_frame_source.h
|
||||
/// @see threaded_frame_source.cpp
|
||||
/// @ingroup video
|
||||
///
|
||||
|
||||
#ifndef AGI_PRE
|
||||
#include <tr1/memory>
|
||||
|
||||
#include <wx/event.h>
|
||||
#include <wx/thread.h>
|
||||
#endif
|
||||
|
||||
#include <libaegisub/exception.h>
|
||||
#include "video_frame.h"
|
||||
|
||||
class AssFile;
|
||||
class SubtitlesProvider;
|
||||
class VideoProvider;
|
||||
|
||||
/// @class ThreadedFrameSource
|
||||
/// @brief An asynchronous video decoding and subtitle rendering wrapper
|
||||
class ThreadedFrameSource : public wxThread {
|
||||
/// Subtitles provider
|
||||
std::auto_ptr<SubtitlesProvider> provider;
|
||||
/// Video provider
|
||||
std::tr1::shared_ptr<VideoProvider> videoProvider;
|
||||
/// Event handler to send FrameReady events to
|
||||
wxEvtHandler *parent;
|
||||
|
||||
int nextFrame; ///< Next queued frame, or -1 for none
|
||||
double nextTime; ///< Next queued time
|
||||
std::auto_ptr<AssFile> nextSubs; ///< Next queued AssFile
|
||||
|
||||
/// Subtitles to be loaded the next time a frame is requested
|
||||
std::auto_ptr<AssFile> subs;
|
||||
/// If subs is set and this is not -1, frame which subs was limited to when
|
||||
/// it was last sent to the subtitle provider
|
||||
int singleFrame;
|
||||
|
||||
wxMutex fileMutex; ///< Mutex for subtitle file usage
|
||||
wxMutex jobMutex; ///< Mutex for nextFrame/nextTime
|
||||
wxMutex providerMutex; ///< Mutex for video provider
|
||||
wxMutex evtMutex; ///< Mutex for FrameReadyEvents associated with this
|
||||
|
||||
wxCondition jobReady; ///< Signal for indicating that a frame has be requested
|
||||
|
||||
/// Frame buffers to render subtitles into
|
||||
/// Two are needed so that a frame can be decoded/rendered while the GUI is
|
||||
/// doing stuff with the other
|
||||
AegiVideoFrame frameBuffer[2];
|
||||
/// Next frame buffer to use
|
||||
int frameBufferIdx;
|
||||
|
||||
bool run; ///< Should the thread continue to run
|
||||
|
||||
void *Entry();
|
||||
AegiVideoFrame const& ProcFrame(int frameNum, double time, bool raw = false);
|
||||
public:
|
||||
/// @brief Load the passed subtitle file
|
||||
/// @param subs File to load
|
||||
///
|
||||
/// This function blocks until is it is safe for the calling thread to
|
||||
/// modify subs
|
||||
void LoadSubtitles(AssFile *subs) throw();
|
||||
|
||||
/// @brief Queue a request for a frame
|
||||
/// @brief frame Frame number
|
||||
/// @brief time Exact start time of the frame in seconds
|
||||
///
|
||||
/// This merely queues up a request and deletes any pending requests; there
|
||||
/// is no guarantee that the requested frame will ever actually be produced
|
||||
void RequestFrame(int frame, double time) throw();
|
||||
|
||||
/// @brief Synchronously get a frame
|
||||
/// @brief frame Frame number
|
||||
/// @brief time Exact start time of the frame in seconds
|
||||
/// @brief raw Get raw frame without subtitles
|
||||
AegiVideoFrame const& GetFrame(int frame, double time, bool raw = false);
|
||||
|
||||
/// @brief Non-blocking Delete
|
||||
///
|
||||
/// Needed due to that calling Delete while the thread is waiting on
|
||||
/// jobReady results in a deadlock; as such this is the only safe way to
|
||||
/// destroy a ThreadedFrameSource
|
||||
void End();
|
||||
|
||||
std::tr1::shared_ptr<VideoProvider> GetVideoProvider() const { return videoProvider; }
|
||||
|
||||
/// @brief Constructor
|
||||
/// @param videoFileName File to open
|
||||
/// @param parent Event handler to send FrameReady events to
|
||||
ThreadedFrameSource(wxString videoFileName, wxEvtHandler *parent);
|
||||
~ThreadedFrameSource();
|
||||
};
|
||||
|
||||
/// @class FrameReadyEvent
|
||||
/// @brief Event which signals that a requested frame is ready
|
||||
class FrameReadyEvent : public wxEvent {
|
||||
/// Externally passed mutex that is kept locked as long as this or a copy
|
||||
/// of this exists. Used to ensure that the next FrameReadyEvent is not
|
||||
/// announced until this one is fully processed.
|
||||
///
|
||||
/// Although tr1 does not require that shared_ptr be thread safe (due to
|
||||
/// the standard having no concept of threads), all implementations have
|
||||
/// at least a thread safe reference counter, which is all we happen to
|
||||
/// need here.
|
||||
std::tr1::shared_ptr<wxMutexLocker> mutex;
|
||||
public:
|
||||
/// Frame which is ready; only guaranteed to be valid as long as this
|
||||
/// event object exists
|
||||
const AegiVideoFrame *frame;
|
||||
/// Time which was used for subtitle rendering
|
||||
double time;
|
||||
wxEvent *Clone() const { return new FrameReadyEvent(*this); };
|
||||
FrameReadyEvent(const AegiVideoFrame *frame, double time, std::tr1::shared_ptr<wxMutexLocker> mutex)
|
||||
: mutex(mutex), frame(frame), time(time) {
|
||||
}
|
||||
};
|
||||
|
||||
// These exceptions are wxEvents so that they can be passed directly back to
|
||||
// the parent thread as events
|
||||
class VideoProviderErrorEvent : public wxEvent, public agi::Exception {
|
||||
public:
|
||||
const char * GetName() const { return "video/error"; }
|
||||
wxEvent *Clone() const { return new VideoProviderErrorEvent(*this); };
|
||||
agi::Exception *Copy() const { return new VideoProviderErrorEvent(*this); };
|
||||
VideoProviderErrorEvent(wxString msg);
|
||||
};
|
||||
|
||||
class SubtitlesProviderErrorEvent : public wxEvent, public agi::Exception {
|
||||
public:
|
||||
const char * GetName() const { return "subtitles/error"; }
|
||||
wxEvent *Clone() const { return new SubtitlesProviderErrorEvent(*this); };
|
||||
agi::Exception *Copy() const { return new SubtitlesProviderErrorEvent(*this); };
|
||||
SubtitlesProviderErrorEvent(wxString msg);
|
||||
};
|
||||
|
||||
wxDECLARE_EVENT(EVT_FRAME_READY, FrameReadyEvent)
|
||||
wxDECLARE_EVENT(EVT_VIDEO_ERROR, VideoProviderErrorEvent)
|
||||
wxDECLARE_EVENT(EVT_SUBTITLES_ERROR, SubtitlesProviderErrorEvent)
|
|
@ -54,13 +54,12 @@
|
|||
#endif
|
||||
|
||||
#include "ass_dialogue.h"
|
||||
#include "ass_exporter.h"
|
||||
#include "ass_file.h"
|
||||
#include "ass_style.h"
|
||||
#include "ass_time.h"
|
||||
#include "audio_display.h"
|
||||
#include "compat.h"
|
||||
#include "export_visible_lines.h"
|
||||
#include "include/aegisub/video_provider.h"
|
||||
#include "keyframe.h"
|
||||
#include <libaegisub/access.h>
|
||||
#include "main.h"
|
||||
|
@ -68,12 +67,11 @@
|
|||
#include "standard_paths.h"
|
||||
#include "subs_edit_box.h"
|
||||
#include "subs_grid.h"
|
||||
#include "subtitles_provider_manager.h"
|
||||
#include "threaded_frame_source.h"
|
||||
#include "utils.h"
|
||||
#include "video_box.h"
|
||||
#include "video_context.h"
|
||||
#include "video_display.h"
|
||||
#include "video_provider_manager.h"
|
||||
|
||||
/// IDs
|
||||
enum {
|
||||
|
@ -105,6 +103,8 @@ VideoContext::VideoContext()
|
|||
, VFR_Input(videoFPS)
|
||||
, VFR_Output(ovrFPS)
|
||||
{
|
||||
Bind(EVT_VIDEO_ERROR, &VideoContext::OnVideoError, this);
|
||||
Bind(EVT_SUBTITLES_ERROR, &VideoContext::OnSubtitlesError, this);
|
||||
}
|
||||
|
||||
VideoContext::~VideoContext () {
|
||||
|
@ -112,7 +112,6 @@ VideoContext::~VideoContext () {
|
|||
delete audio->provider;
|
||||
delete audio->player;
|
||||
}
|
||||
tempFrame.Clear();
|
||||
}
|
||||
|
||||
VideoContext *VideoContext::Get() {
|
||||
|
@ -143,11 +142,10 @@ void VideoContext::Reset() {
|
|||
|
||||
// Clean up video data
|
||||
videoName.clear();
|
||||
tempFrame.Clear();
|
||||
|
||||
// Remove provider
|
||||
videoProvider.reset();
|
||||
provider.reset();
|
||||
subsProvider.reset();
|
||||
}
|
||||
|
||||
void VideoContext::SetVideo(const wxString &filename) {
|
||||
|
@ -158,20 +156,17 @@ void VideoContext::SetVideo(const wxString &filename) {
|
|||
try {
|
||||
grid->CommitChanges(true);
|
||||
|
||||
// Choose a provider
|
||||
provider.reset(VideoProviderFactoryManager::GetProvider(filename));
|
||||
|
||||
// Get subtitles provider
|
||||
try {
|
||||
subsProvider.reset(SubtitlesProviderFactoryManager::GetProvider());
|
||||
provider.reset(new ThreadedFrameSource(filename, this), std::mem_fun(&ThreadedFrameSource::End));
|
||||
videoProvider = provider->GetVideoProvider();
|
||||
}
|
||||
catch (wxString err) { wxMessageBox(_T("Error while loading subtitles provider: ") + err,_T("Subtitles provider")); }
|
||||
catch (const wchar_t *err) { wxMessageBox(_T("Error while loading subtitles provider: ") + wxString(err),_T("Subtitles provider")); }
|
||||
catch (wxString err) { wxMessageBox(L"Error while loading video: " + err, L"Video Error"); }
|
||||
catch (const wchar_t *err) { wxMessageBox(L"Error while loading video: " + wxString(err), L"Video Error"); }
|
||||
|
||||
keyFrames = provider->GetKeyFrames();
|
||||
keyFrames = videoProvider->GetKeyFrames();
|
||||
|
||||
// Set frame rate
|
||||
videoFPS = provider->GetFPS();
|
||||
videoFPS = videoProvider->GetFPS();
|
||||
if (ovrFPS.IsLoaded()) {
|
||||
int ovr = wxMessageBox(_("You already have timecodes loaded. Would you like to replace them with timecodes from the video file?"), _("Replace timecodes?"), wxYES_NO | wxICON_QUESTION);
|
||||
if (ovr == wxYES) {
|
||||
|
@ -181,7 +176,7 @@ void VideoContext::SetVideo(const wxString &filename) {
|
|||
}
|
||||
|
||||
// Gather video parameters
|
||||
length = provider->GetFrameCount();
|
||||
length = videoProvider->GetFrameCount();
|
||||
|
||||
// Set filename
|
||||
videoName = filename;
|
||||
|
@ -193,7 +188,7 @@ void VideoContext::SetVideo(const wxString &filename) {
|
|||
frame_n = 0;
|
||||
|
||||
// Show warning
|
||||
wxString warning = provider->GetWarning();
|
||||
wxString warning = videoProvider->GetWarning();
|
||||
if (!warning.empty()) wxMessageBox(warning,_T("Warning"),wxICON_WARNING | wxOK);
|
||||
|
||||
hasSubtitles = false;
|
||||
|
@ -204,16 +199,14 @@ void VideoContext::SetVideo(const wxString &filename) {
|
|||
UpdateDisplays(true);
|
||||
}
|
||||
|
||||
catch (wxString &e) {
|
||||
catch (const wxString &e) {
|
||||
wxMessageBox(e,_T("Error setting video"),wxICON_ERROR | wxOK);
|
||||
}
|
||||
}
|
||||
|
||||
void VideoContext::AddDisplay(VideoDisplay *display) {
|
||||
for (std::list<VideoDisplay*>::iterator cur=displayList.begin();cur!=displayList.end();cur++) {
|
||||
if ((*cur) == display) return;
|
||||
}
|
||||
displayList.push_back(display);
|
||||
if (std::find(displayList.begin(), displayList.end(), display) == displayList.end())
|
||||
displayList.push_back(display);
|
||||
}
|
||||
|
||||
void VideoContext::RemoveDisplay(VideoDisplay *display) {
|
||||
|
@ -247,26 +240,10 @@ void VideoContext::UpdateDisplays(bool full, bool seek) {
|
|||
}
|
||||
}
|
||||
|
||||
void VideoContext::Refresh(bool full) {
|
||||
if (subsProvider.get()) {
|
||||
if (full) {
|
||||
AssLimitToVisibleFilter::SetFrame(-1);
|
||||
singleFrame = false;
|
||||
}
|
||||
else {
|
||||
AssLimitToVisibleFilter::SetFrame(frame_n);
|
||||
singleFrame = true;
|
||||
}
|
||||
void VideoContext::Refresh() {
|
||||
if (!IsLoaded()) return;
|
||||
|
||||
AssExporter exporter(grid->ass);
|
||||
exporter.AddAutoFilters();
|
||||
try {
|
||||
std::auto_ptr<AssFile> exported(exporter.ExportTransform());
|
||||
subsProvider->LoadSubtitles(exported.get());
|
||||
}
|
||||
catch (wxString err) { wxMessageBox(_T("Error while invoking subtitles provider: ") + err,_T("Subtitles provider")); }
|
||||
catch (const wchar_t *err) { wxMessageBox(_T("Error while invoking subtitles provider: ") + wxString(err),_T("Subtitles provider")); }
|
||||
}
|
||||
provider->LoadSubtitles(grid->ass);
|
||||
UpdateDisplays(false);
|
||||
}
|
||||
|
||||
|
@ -278,10 +255,6 @@ void VideoContext::JumpToFrame(int n) {
|
|||
|
||||
frame_n = n;
|
||||
|
||||
if (singleFrame) {
|
||||
Refresh(true);
|
||||
}
|
||||
|
||||
UpdateDisplays(false, true);
|
||||
|
||||
static agi::OptionValue* highlight = OPT_GET("Subtitle/Grid/Highlight Subtitles in Frame");
|
||||
|
@ -292,33 +265,19 @@ void VideoContext::JumpToTime(int ms, agi::vfr::Time end) {
|
|||
JumpToFrame(FrameAtTime(ms, end));
|
||||
}
|
||||
|
||||
AegiVideoFrame VideoContext::GetFrame(int n,bool raw) {
|
||||
// Current frame if -1
|
||||
if (n == -1) n = frame_n;
|
||||
void VideoContext::GetFrameAsync(int n) {
|
||||
provider->RequestFrame(n,videoFPS.TimeAtFrame(n)/1000.0);
|
||||
}
|
||||
|
||||
AegiVideoFrame frame = provider->GetFrame(n);
|
||||
|
||||
// Raster subtitles if available/necessary
|
||||
if (!raw && subsProvider.get()) {
|
||||
tempFrame.CopyFrom(frame);
|
||||
try {
|
||||
subsProvider->DrawSubtitles(tempFrame,videoFPS.TimeAtFrame(n)/1000.0);
|
||||
}
|
||||
catch (...) {
|
||||
wxLogError(L"Subtitle rendering for the current frame failed.\n");
|
||||
}
|
||||
return tempFrame;
|
||||
}
|
||||
|
||||
// Return pure frame
|
||||
else return frame;
|
||||
AegiVideoFrame const& VideoContext::GetFrame(int n, bool raw) {
|
||||
return provider->GetFrame(n, videoFPS.TimeAtFrame(n)/1000.0, raw);
|
||||
}
|
||||
|
||||
int VideoContext::GetWidth() const {
|
||||
return provider->GetWidth();
|
||||
return videoProvider->GetWidth();
|
||||
}
|
||||
int VideoContext::GetHeight() const {
|
||||
return provider->GetHeight();
|
||||
return videoProvider->GetHeight();
|
||||
}
|
||||
|
||||
void VideoContext::SaveSnapshot(bool raw) {
|
||||
|
@ -520,8 +479,8 @@ void VideoContext::SaveKeyframes(wxString filename) {
|
|||
|
||||
void VideoContext::CloseKeyframes() {
|
||||
keyFramesFilename.clear();
|
||||
if (provider.get()) {
|
||||
keyFrames = provider->GetKeyFrames();
|
||||
if (videoProvider.get()) {
|
||||
keyFrames = videoProvider->GetKeyFrames();
|
||||
}
|
||||
else {
|
||||
keyFrames.clear();
|
||||
|
@ -571,3 +530,15 @@ int VideoContext::FrameAtTime(int time, agi::vfr::Time type) const {
|
|||
}
|
||||
return videoFPS.FrameAtTime(time, type);
|
||||
}
|
||||
|
||||
void VideoContext::OnVideoError(VideoProviderErrorEvent const& err) {
|
||||
wxLogError(
|
||||
L"Failed seeking video. The video file may be corrupt or incomplete.\n"
|
||||
L"Error message reported: %s",
|
||||
lagi_wxString(err.GetMessage()).c_str());
|
||||
}
|
||||
void VideoContext::OnSubtitlesError(SubtitlesProviderErrorEvent const& err) {
|
||||
wxLogError(
|
||||
L"Failed rendering subtitles. Error message reported: %s",
|
||||
lagi_wxString(err.GetMessage()).c_str());
|
||||
}
|
||||
|
|
|
@ -65,8 +65,10 @@ class AudioProvider;
|
|||
class AudioDisplay;
|
||||
class AssDialogue;
|
||||
class KeyFrameFile;
|
||||
class SubtitlesProvider;
|
||||
class SubtitlesProviderErrorEvent;
|
||||
class ThreadedFrameSource;
|
||||
class VideoProvider;
|
||||
class VideoProviderErrorEvent;
|
||||
class VideoDisplay;
|
||||
|
||||
namespace agi {
|
||||
|
@ -87,13 +89,10 @@ private:
|
|||
std::list<VideoDisplay*> displayList;
|
||||
|
||||
/// DOCME
|
||||
AegiVideoFrame tempFrame;
|
||||
std::tr1::shared_ptr<VideoProvider> videoProvider;
|
||||
|
||||
/// DOCME
|
||||
std::auto_ptr<VideoProvider> provider;
|
||||
|
||||
/// DOCME
|
||||
std::auto_ptr<SubtitlesProvider> subsProvider;
|
||||
std::tr1::shared_ptr<ThreadedFrameSource> provider;
|
||||
|
||||
/// DOCME
|
||||
std::vector<int> keyFrames;
|
||||
|
@ -153,6 +152,9 @@ private:
|
|||
|
||||
bool singleFrame;
|
||||
|
||||
void OnVideoError(VideoProviderErrorEvent const& err);
|
||||
void OnSubtitlesError(SubtitlesProviderErrorEvent const& err);
|
||||
|
||||
public:
|
||||
/// DOCME
|
||||
SubtitlesGrid *grid;
|
||||
|
@ -174,15 +176,16 @@ public:
|
|||
|
||||
|
||||
/// @brief Get the video provider used for the currently open video
|
||||
VideoProvider *GetProvider() const { return provider.get(); }
|
||||
AegiVideoFrame GetFrame(int n,bool raw=false);
|
||||
VideoProvider *GetProvider() const { return videoProvider.get(); }
|
||||
AegiVideoFrame const& GetFrame(int n, bool raw = false);
|
||||
void GetFrameAsync(int n);
|
||||
|
||||
/// @brief Save the currently displayed frame as an image
|
||||
/// @param raw Should the frame have subtitles?
|
||||
void SaveSnapshot(bool raw);
|
||||
|
||||
/// @brief Is there a video loaded?
|
||||
bool IsLoaded() const { return !!provider.get(); }
|
||||
bool IsLoaded() const { return !!videoProvider.get(); }
|
||||
|
||||
/// @brief Is the video currently playing?
|
||||
bool IsPlaying() const { return isPlaying; }
|
||||
|
@ -233,9 +236,7 @@ public:
|
|||
void JumpToTime(int ms, agi::vfr::Time end = agi::vfr::START);
|
||||
|
||||
/// @brief Refresh the subtitle provider
|
||||
/// @param full Send the entire subtitle file to the renderer rather than
|
||||
/// just the lines visible on the current frame
|
||||
void Refresh(bool full = false);
|
||||
void Refresh();
|
||||
|
||||
/// @brief Update the video display
|
||||
/// @param full Recalculate size and slider lengths
|
||||
|
|
|
@ -60,6 +60,7 @@
|
|||
#include "hotkeys.h"
|
||||
#include "main.h"
|
||||
#include "subs_grid.h"
|
||||
#include "threaded_frame_source.h"
|
||||
#include "video_out_gl.h"
|
||||
#include "video_box.h"
|
||||
#include "video_context.h"
|
||||
|
@ -141,10 +142,12 @@ VideoDisplay::VideoDisplay(VideoBox *box, VideoSlider *ControlSlider, wxTextCtrl
|
|||
, freeSize(false)
|
||||
{
|
||||
box->Bind(wxEVT_COMMAND_TOOL_CLICKED, &VideoDisplay::OnMode, this, Video_Mode_Standard, Video_Mode_Vector_Clip);
|
||||
VideoContext::Get()->Bind(EVT_FRAME_READY, &VideoDisplay::UploadFrameData, this);
|
||||
SetCursor(wxNullCursor);
|
||||
}
|
||||
|
||||
VideoDisplay::~VideoDisplay () {
|
||||
VideoContext::Get()->Unbind(EVT_FRAME_READY, &VideoDisplay::UploadFrameData, this);
|
||||
VideoContext::Get()->RemoveDisplay(this);
|
||||
}
|
||||
|
||||
|
@ -214,39 +217,23 @@ void VideoDisplay::SetFrame(int frameNumber) {
|
|||
if (context->IsLoaded()) {
|
||||
context->GetScriptSize(scriptW, scriptH);
|
||||
tool->SetFrame(frameNumber);
|
||||
|
||||
UploadFrameData();
|
||||
context->GetFrameAsync(currentFrame);
|
||||
}
|
||||
Render();
|
||||
}
|
||||
|
||||
void VideoDisplay::UploadFrameData() {
|
||||
void VideoDisplay::UploadFrameData(FrameReadyEvent &evt) {
|
||||
if (!InitContext()) return;
|
||||
VideoContext *context = VideoContext::Get();
|
||||
AegiVideoFrame frame;
|
||||
|
||||
try {
|
||||
frame = context->GetFrame(currentFrame);
|
||||
}
|
||||
catch (const wxChar *err) {
|
||||
wxLogError(
|
||||
L"Failed seeking video. The video file may be corrupt or incomplete.\n"
|
||||
L"Error message reported: %s",
|
||||
err);
|
||||
}
|
||||
catch (...) {
|
||||
wxLogError(
|
||||
L"Failed seeking video. The video file may be corrupt or incomplete.\n"
|
||||
L"No further error message given.");
|
||||
}
|
||||
try {
|
||||
videoOut->UploadFrameData(frame);
|
||||
videoOut->UploadFrameData(*evt.frame);
|
||||
}
|
||||
catch (const VideoOutInitException& err) {
|
||||
wxLogError(
|
||||
L"Failed to initialize video display. Closing other running programs and updating your video card drivers may fix this.\n"
|
||||
L"Failed to initialize video display. Closing other running "
|
||||
L"programs and updating your video card drivers may fix this.\n"
|
||||
L"Error message reported: %s",
|
||||
err.GetMessage().c_str());
|
||||
context->Reset();
|
||||
VideoContext::Get()->Reset();
|
||||
}
|
||||
catch (const VideoOutRenderException& err) {
|
||||
wxLogError(
|
||||
|
@ -254,14 +241,14 @@ void VideoDisplay::UploadFrameData() {
|
|||
L"Error message reported: %s",
|
||||
err.GetMessage().c_str());
|
||||
}
|
||||
Render();
|
||||
}
|
||||
|
||||
void VideoDisplay::Refresh() {
|
||||
if (!tool.get()) tool.reset(new VisualToolCross(this, video, toolBar));
|
||||
if (!InitContext()) return;
|
||||
UploadFrameData();
|
||||
VideoContext::Get()->GetFrameAsync(currentFrame);
|
||||
tool->Refresh();
|
||||
Render();
|
||||
}
|
||||
|
||||
void VideoDisplay::SetFrameRange(int from, int to) {
|
||||
|
@ -597,14 +584,14 @@ void VideoDisplay::FromScriptCoords(int *x, int *y) const {
|
|||
|
||||
void VideoDisplay::OnCopyToClipboard(wxCommandEvent &) {
|
||||
if (wxTheClipboard->Open()) {
|
||||
wxTheClipboard->SetData(new wxBitmapDataObject(wxBitmap(VideoContext::Get()->GetFrame(-1).GetImage(),24)));
|
||||
wxTheClipboard->SetData(new wxBitmapDataObject(wxBitmap(VideoContext::Get()->GetFrame(currentFrame).GetImage(),24)));
|
||||
wxTheClipboard->Close();
|
||||
}
|
||||
}
|
||||
|
||||
void VideoDisplay::OnCopyToClipboardRaw(wxCommandEvent &) {
|
||||
if (wxTheClipboard->Open()) {
|
||||
wxTheClipboard->SetData(new wxBitmapDataObject(wxBitmap(VideoContext::Get()->GetFrame(-1,true).GetImage(),24)));
|
||||
wxTheClipboard->SetData(new wxBitmapDataObject(wxBitmap(VideoContext::Get()->GetFrame(currentFrame,true).GetImage(),24)));
|
||||
wxTheClipboard->Close();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
#endif
|
||||
|
||||
// Prototypes
|
||||
class FrameReadyEvent;
|
||||
class VideoSlider;
|
||||
class VideoBox;
|
||||
class VideoOutGL;
|
||||
|
@ -97,7 +98,7 @@ class VideoDisplay : public wxGLCanvas {
|
|||
void DrawOverscanMask(int sizeH, int sizeV, wxColor color, double alpha) const;
|
||||
|
||||
/// Upload the image for the current frame to the video card
|
||||
void UploadFrameData();
|
||||
void UploadFrameData(FrameReadyEvent&);
|
||||
|
||||
/// @brief Paint event
|
||||
void OnPaint(wxPaintEvent& event);
|
||||
|
|
Loading…
Reference in a new issue