Update for N-Gage - Audio is now double buffered (#15516)

[N-Gage] Audio is now double buffered to avoid stuttering and glitches. Some audio platform specific variables were exposed through SDL_Hints. Same method was used to display FPS. N-gage functions to obtain the current buffer and screen pitch were added to the render.

Adds hints:

SDL_AUDIO_NGAGE_LATENCY
SDL_AUDIO_NGAGE_SCHEDULER_TICK
SDL_AUDIO_NGAGE_PROCESS_TICK
SDL_AUDIO_NGAGE_PROCESS_PRIORITY
SDL_RENDER_SHOW_FPS

Adds functions to get current buffer address and pitch:

void *NGAGE_GetBackbufferAddress(void);
int NGAGE_GetBackbufferPitch(void);

---------
Co-authored-by: Michael Fitzmayer <mail@michael-fitzmayer.de>
Co-authored-by: Eddy Jansson <eloj@users.noreply.github.com>
This commit is contained in:
misscelan
2026-05-06 16:39:23 +02:00
committed by GitHub
parent 1ac0ae9224
commit 37089cf0a8
7 changed files with 323 additions and 215 deletions

View File

@@ -42,18 +42,20 @@ static bool NGAGEAUDIO_OpenDevice(SDL_AudioDevice *device)
}
device->hidden = phdata;
phdata->buffer = SDL_calloc(1, device->buffer_size);
if (!phdata->buffer) {
SDL_OutOfMemory();
phdata->buffer[0] = SDL_calloc(1, device->buffer_size);
phdata->buffer[1] = SDL_calloc(1, device->buffer_size);
if (!phdata->buffer[0] || !phdata->buffer[1])
{
SDL_Log("Error: Failed to allocate audio buffers");
SDL_free(phdata->buffer[0]);
SDL_free(phdata->buffer[1]);
SDL_free(phdata);
return false;
}
devptr = device;
// Since the phone can change the sample rate during a phone call,
// we set the sample rate to 8KHz to be safe. Even though it
// might be possible to adjust the sample rate dynamically, it's
// not supported by the current implementation.
phdata->fill_index = 0;
devptr = device;
device->spec.format = SDL_AUDIO_S16LE;
device->spec.channels = 1;
@@ -64,6 +66,13 @@ static bool NGAGEAUDIO_OpenDevice(SDL_AudioDevice *device)
return true;
}
/*********************************************
NGAGEAUDIO_GetDeviceBuf -
Return the buffer that is currently being filled by SDL
**********************************************/
static Uint8 *NGAGEAUDIO_GetDeviceBuf(SDL_AudioDevice *device, int *buffer_size)
{
SDL_PrivateAudioData *phdata = (SDL_PrivateAudioData *)device->hidden;
@@ -71,19 +80,24 @@ static Uint8 *NGAGEAUDIO_GetDeviceBuf(SDL_AudioDevice *device, int *buffer_size)
*buffer_size = 0;
return 0;
}
*buffer_size = device->buffer_size;
return phdata->buffer;
return phdata->buffer[phdata->fill_index];
}
static void NGAGEAUDIO_CloseDevice(SDL_AudioDevice *device)
{
if (device->hidden) {
SDL_free(device->hidden->buffer);
SDL_free(device->hidden);
}
SDL_PrivateAudioData *phdata = (SDL_PrivateAudioData *)device->hidden;
return;
SDL_free(phdata->buffer[0]);
SDL_free(phdata->buffer[1]);
SDL_free(phdata);
device->hidden = NULL;
}
}
static bool NGAGEAUDIO_Init(SDL_AudioDriverImpl *impl)

View File

@@ -98,89 +98,12 @@ void CAudio::Start()
}
}
// Feeds more processed data to the audio stream.
void CAudio::Feed()
{
// If a WriteL is already in progress, or we aren't even playing;
// do nothing!
if ((iState != EStateWriting) && (iState != EStatePlaying)) {
return;
}
// Figure out the number of samples that really have been played
// through the output.
TTimeIntervalMicroSeconds pos = iStream->Position();
TInt played = 8 * (pos.Int64() / TInt64(1000)).GetTInt(); // 8kHz.
played += iBaseSamplesPlayed;
// Determine the difference between the number of samples written to
// CMdaAudioOutputStream and the number of samples it has played.
// The difference is the amount of data in the buffers.
if (played < 0) {
played = 0;
}
TInt buffered = iSamplesWritten - played;
if (buffered < 0) {
buffered = 0;
}
if (iState == EStateWriting) {
return;
}
// The trick for low latency: Do not let the buffers fill up beyond the
// latency desired! We write as many samples as the difference between
// the latency target (in samples) and the amount of data buffered.
TInt samplesToWrite = iLatencySamples - buffered;
// Do not write very small blocks. This should improve efficiency, since
// writes to the streaming API are likely to be expensive.
if (samplesToWrite < iMinWrite) {
// Not enough data to write, set up a timer to fire after a while.
// Try againwhen it expired.
if (iTimerActive) {
return;
}
iTimerActive = ETrue;
SetActive();
iTimer.After(iStatus, (1000 * iLatency) / 8);
return;
}
// Do not write more than the set number of samples at once.
int numSamples = samplesToWrite;
if (numSamples > iMaxWrite) {
numSamples = iMaxWrite;
}
SDL_AudioDevice *device = NGAGE_GetAudioDeviceAddr();
if (device) {
SDL_PrivateAudioData *phdata = (SDL_PrivateAudioData *)device->hidden;
iBufDes.Set(phdata->buffer, 2 * numSamples, 2 * numSamples);
iStream->WriteL(iBufDes);
iState = EStateWriting;
// Keep track of the number of samples written (for latency calculations).
iSamplesWritten += numSamples;
} else {
// Output device not ready yet. Let's go for another round.
if (iTimerActive) {
return;
}
iTimerActive = ETrue;
SetActive();
iTimer.After(iStatus, (1000 * iLatency) / 8);
}
}
void CAudio::RunL()
{
iTimerActive = EFalse;
Feed();
}
void CAudio::DoCancel()
@@ -194,9 +117,21 @@ void CAudio::StartThread()
TInt heapMinSize = 8192; // 8 KB initial heap size.
TInt heapMaxSize = 1024 * 1024; // 1 MB maximum heap size.
TInt err = iProcess.Create(_L("ProcessThread"), ProcessThreadCB, KDefaultStackSize * 2, heapMinSize, heapMaxSize, this);
if (err == KErrNone) {
iProcess.SetPriority(EPriorityLess);
if (err == KErrNone)
{
TThreadPriority prio = EPriorityLess;
const char *prioHint = SDL_GetHint(SDL_HINT_AUDIO_NGAGE_PROCESS_PRIORITY);
if (prioHint) {
// Symbian priorities: 10 (MuchLess), 20 (Less), 30 (Normal), 40 (More)
prio = (TThreadPriority)SDL_atoi(prioHint);
RThread().SetPriority(prio);
}
iProcess.SetPriority(prio);
iProcess.Resume();
} else {
SDL_Log("Error: Failed to create audio processing thread: %d", err);
@@ -212,138 +147,240 @@ void CAudio::StopThread()
}
}
/***************************************************
* ProcessThreadCB -
*
* This thread calls the SDL mixer when the buffer is ready and self->iState == EStatePlaying (basically other than initial stated, when not writing)
*
* It only mixes, never calls WriteL
****************************************************/
TInt CAudio::ProcessThreadCB(TAny *aPtr)
{
CTrapCleanup *cleanup = CTrapCleanup::New();
if (!cleanup)
return KErrNoMemory;
CAudio *self = static_cast<CAudio *>(aPtr);
SDL_AudioDevice *device = NGAGE_GetAudioDeviceAddr();
while (self->iStreamStarted) {
if (device) {
SDL_PlaybackAudioThreadIterate(device);
} else {
device = NGAGE_GetAudioDeviceAddr();
}
User::After(100000); // 100ms.
TInt processTick = 40000; // Default 40ms
const char *tickHint = SDL_GetHint(SDL_HINT_AUDIO_NGAGE_PROCESS_TICK);
if (tickHint)
{
processTick = SDL_atoi(tickHint) * 1000;
}
while (self->iStreamStarted)
{
if (self->iState == EStatePlaying && !self->iBufferReady)
{
/* Ask SDL to mix audio into buffer[fill_index]*/
SDL_PlaybackAudioThreadIterate(device);
/* Signal AudioThreadCB to write it*/
self->iBufferReady = ETrue;
}
else
{
/*if we are not ready to obtain the mix data we sleep a bit this thread*/
User::After(processTick);
}
}
delete cleanup;
return KErrNone;
}
void CAudio::MaoscOpenComplete(TInt aError)
{
if (aError == KErrNone) {
iStream->SetVolume(1);
iStreamStarted = ETrue;
StartThread();
} else {
SDL_Log("Error: Failed to open audio stream: %d", aError);
}
}
void CAudio::MaoscBufferCopied(TInt aError, const TDesC8 & /*aBuffer*/)
{
if (aError == KErrNone) {
iState = EStatePlaying;
Feed();
} else if (aError == KErrAbort) {
// The stream has been stopped.
iState = EStateDone;
} else {
SDL_Log("Error: Failed to copy audio buffer: %d", aError);
}
}
void CAudio::MaoscPlayComplete(TInt aError)
{
// If we finish due to an underflow, we'll need to restart playback.
// Normally KErrUnderlow is raised at stream end, but in our case the API
// should never see the stream end -- we are continuously feeding it more
// data! Many underflow errors mean that the latency target is too low.
if (aError == KErrUnderflow) {
// The number of samples played gets reset to zero when we restart
// playback after underflow.
iBaseSamplesPlayed = iSamplesWritten;
iStream->Stop();
Cancel();
iStream->SetAudioPropertiesL(TMdaAudioDataSettings::ESampleRate8000Hz, TMdaAudioDataSettings::EChannelsMono);
iState = EStatePlaying;
Feed();
return;
} else if (aError != KErrNone) {
// Handle error.
}
// We shouldn't get here.
SDL_Log("%s: %d", SDL_FUNCTION, aError);
}
static TBool gAudioRunning;
TBool AudioIsReady()
{
return gAudioRunning;
}
/***************************************************
* AudioThreadCB -
*
* This thread owns the scheduler and calls WriteL, wich queues the assigned sound buffer to be played
****************************************************/
TInt AudioThreadCB(TAny *aParams)
{
CTrapCleanup *cleanup = CTrapCleanup::New();
if (!cleanup) {
return KErrNoMemory;
}
CActiveScheduler *scheduler = new CActiveScheduler();
if (!scheduler) {
delete cleanup;
return KErrNoMemory;
}
CActiveScheduler::Install(scheduler);
TRAPD(err, {
TInt latency = *(TInt *)aParams;
CAudio *audio = CAudio::NewL(latency);
CleanupStack::PushL(audio);
TRAPD(err,
{
TInt latency = *(TInt *)aParams;
CAudio *audio = CAudio::NewL(latency);
CleanupStack::PushL(audio);
audio->iBufferReady = EFalse;
gAudioRunning = ETrue;
audio->Start();
TBool once = EFalse;
gAudioRunning = ETrue;
audio->Start();
while (gAudioRunning) {
// Allow active scheduler to process any events.
TInt error;
CActiveScheduler::RunIfReady(error, CActive::EPriorityIdle);
TInt processTick = 5000; // Default 5ms
const char *tickHint = SDL_GetHint(SDL_HINT_AUDIO_NGAGE_PROCESS_TICK);
if (tickHint) {
processTick = SDL_atoi(tickHint) * 1000;
}
if (!once) {
SDL_AudioDevice *device = NGAGE_GetAudioDeviceAddr();
if (device) {
// Stream ready; start feeding audio data.
// After feeding it once, the callbacks will take over.
audio->iState = CAudio::EStatePlaying;
audio->Feed();
once = ETrue;
}
}
User::After(100000); // 100ms.
}
while (gAudioRunning)
{
TInt error;
CActiveScheduler::RunIfReady(error, CActive::EPriorityIdle);
/*there is some mix data sound ready*/
if (audio->iBufferReady)
{
audio->iBufferReady = EFalse;
CleanupStack::PopAndDestroy(audio);
});
SDL_AudioDevice *device = NGAGE_GetAudioDeviceAddr();
if (device && device->hidden)
{
SDL_PrivateAudioData *phdata = (SDL_PrivateAudioData *)device->hidden;
audio->iState = EStateWriting;
/*sends the chuck mixed to the queue*/
audio->iBufDes.Set(phdata->buffer[phdata->fill_index], device->buffer_size, device->buffer_size);
TRAPD(werr, audio->iStream->WriteL(audio->iBufDes));
if (werr != KErrNone)
{
/*asks ProcessThreadCB to bring another mix chunk*/
audio->iState = EStatePlaying;
}
else
{
/*swap buffers so while this buffer is being played we can get the mix of the next one if we can*/
phdata->fill_index = 1 - phdata->fill_index;
}
}
}
/*sleep a bit this thread not to hog the CPU*/
User::After(processTick);
}
CleanupStack::PopAndDestroy(audio);
});
delete scheduler;
delete cleanup;
return err;
}
/***************************************************
* MaoscOpenComplete -
*
* Opens the audiostream
*
* *******************************************************/
void CAudio::MaoscOpenComplete(TInt aError)
{
if (aError == KErrNone)
{
/*setting the volume to max, users can change the volume later of their channels individually in code*/
iStream->SetVolume(iStream->MaxVolume());
iStreamStarted = ETrue;
/* Wait until SDL has set devptr and hidden data*/
SDL_AudioDevice *device = NULL;
while (!device || !device->hidden) {
User::After(10000); // 10ms poll
device = NGAGE_GetAudioDeviceAddr();
}
/* Now start the ProcessThreadCB thread*/
StartThread();
/* Kickstart: device is guaranteed valid now*/
this->iState = EStatePlaying;
}
else
{
SDL_Log("Error: Failed to open audio stream: %d", aError);
}
}
/***************************************************
* MaoscOpenComplete -
*
* This signals the mixed data has been finally copied to the designated audio buffer
*
* *******************************************************/
void CAudio::MaoscBufferCopied(TInt aError, const TDesC8 & /*aBuffer*/)
{
if (aError == KErrNone)
{
iState = EStatePlaying;
}
else if (aError == KErrAbort)
{
/* The stream has been stopped.*/
iState = EStateDone;
}
else
{
SDL_Log("Error: Failed to copy audio buffer: %d", aError);
}
}
/***************************************************
* MaoscPlayComplete -
*
* The result after playing the mixed chunk
*
* *******************************************************/
void CAudio::MaoscPlayComplete(TInt aError)
{
/* If we finish due to an underflow, we'll need to restart playback.
Normally KErrUnderlow is raised at stream end, but in our case the API
should never see the stream end -- we are continuously feeding it more
data! Many underflow errors mean that the latency target is too low.*/
if (aError == KErrUnderflow)
{
/* Restart the stream hardware */
iStream->Stop();
TInt ignoredError;
TRAP(ignoredError, iStream->SetAudioPropertiesL(TMdaAudioDataSettings::ESampleRate8000Hz, TMdaAudioDataSettings::EChannelsMono));
/* This wakes up ProcessThreadCB so it can call SDL_PlaybackAudioThreadIterate*/
iState = EStatePlaying;
return;
} else if (aError != KErrNone) {
}
/* We shouldn't get here.*/
SDL_Log("%s: %d", SDL_FUNCTION, aError);
}
TBool AudioIsReady()
{
return gAudioRunning;
}
RThread audioThread;
void InitAudio(TInt *aLatency)
{
// Check if the user has provided a custom latency value via a hint
const char *hint = SDL_GetHint(SDL_HINT_AUDIO_NGAGE_LATENCY);
if (hint) {
*aLatency = (TInt)SDL_atoi(hint);
}
_LIT(KAudioThreadName, "AudioThread");
TInt err = audioThread.Create(KAudioThreadName, AudioThreadCB, KDefaultStackSize, 0, aLatency);

View File

@@ -23,9 +23,27 @@
#ifndef SDL_ngageaudio_h
#define SDL_ngageaudio_h
#ifndef SDL_HINT_AUDIO_NGAGE_LATENCY
#define SDL_HINT_AUDIO_NGAGE_LATENCY "SDL_AUDIO_NGAGE_LATENCY"
#endif
#ifndef SDL_HINT_AUDIO_NGAGE_SCHEDULER_TICK
#define SDL_HINT_AUDIO_NGAGE_SCHEDULER_TICK "SDL_AUDIO_NGAGE_SCHEDULER_TICK"
#endif
#ifndef SDL_HINT_AUDIO_NGAGE_PROCESS_TICK
#define SDL_HINT_AUDIO_NGAGE_PROCESS_TICK "SDL_AUDIO_NGAGE_PROCESS_TICK"
#endif
#ifndef SDL_HINT_AUDIO_NGAGE_PROCESS_PRIORITY
#define SDL_HINT_AUDIO_NGAGE_PROCESS_PRIORITY "SDL_AUDIO_NGAGE_PROCESS_PRIORITY"
#endif
typedef struct SDL_PrivateAudioData
{
Uint8 *buffer;
Uint8 *buffer[2];
int fill_index; /* Which buffer SDL is currently filling */
int play_index; /* Which buffer the hardware is currently using*/
int buffer_size;
} SDL_PrivateAudioData;

View File

@@ -42,6 +42,16 @@ TBool AudioIsReady();
void InitAudio(TInt *aLatency);
void DeinitAudio();
enum TAudioState
{
EStateNone = 0,
EStateOpening,
EStatePlaying,
EStateWriting,
EStateDone
};
class CAudio : public CActive, public MMdaAudioOutputStreamCallback
{
public:
@@ -50,49 +60,42 @@ class CAudio : public CActive, public MMdaAudioOutputStreamCallback
void ConstructL(TInt aLatency);
void Start();
void Feed();
void RunL();
void DoCancel();
static TInt ProcessThreadCB(TAny * /*aPtr*/);
// From MMdaAudioOutputStreamCallback
void MaoscOpenComplete(TInt aError);
void MaoscBufferCopied(TInt aError, const TDesC8 &aBuffer);
void MaoscPlayComplete(TInt aError);
enum
{
EStateNone = 0,
EStateOpening,
EStatePlaying,
EStateWriting,
EStateDone
} iState;
TAudioState iState;
CMdaAudioOutputStream *iStream; /*CMdaAudioOutputStream handler*/
TPtr8 iBufDes; /* Descriptor for the buffer.*/
TBool iStreamStarted; /* have we initialized the audio stream?*/
RThread iProcess; /* thread handler */
TBool iBufferReady; /* Signal AudioThreadCB the buffer is ready*/
private:
CAudio();
void StartThread();
void StopThread();
CMdaAudioOutputStream *iStream;
TMdaAudioDataSettings iStreamSettings;
TBool iStreamStarted;
TPtr8 iBufDes; // Descriptor for the buffer.
TInt iLatency; // Latency target in ms
TInt iLatencySamples; // Latency target in samples.
TInt iMinWrite; // Min number of samples to write per turn.
TInt iMaxWrite; // Max number of samples to write per turn.
TInt iBaseSamplesPlayed; // amples played before last restart.
TInt iSamplesWritten; // Number of samples written so far.
RTimer iTimer;
TBool iTimerCreated;
TBool iTimerActive;
RThread iProcess;
};
#endif // SDL_ngageaudio_hpp

View File

@@ -149,6 +149,23 @@ void NGAGE_SetRenderTargetInternal(NGAGE_TextureData *target)
}
}
static void SDLCALL NGAGE_ShowFPSChanged(void *userdata, const char *name, const char *oldValue, const char *newValue)
{
CRenderer *renderer = (CRenderer *)userdata;
renderer->SetShowFPS(SDL_GetStringBoolean(newValue, false));
}
void *NGAGE_GetBackbufferAddress(void)
{
return gRenderer->GetCurrentBitmap()->DataAddress();
}
int NGAGE_GetBackbufferPitch(void)
{
return CFbsBitmap::ScanLineLength(NGAGE_SCREEN_WIDTH, EColor4K) / 2;
}
#ifdef __cplusplus
}
#endif
@@ -166,6 +183,8 @@ CRenderer::CRenderer() : iRenderer(0), iDirectScreen(0), iScreenGc(0), iWsSessio
CRenderer::~CRenderer()
{
SDL_RemoveHintCallback(SDL_HINT_RENDER_NGAGE_SHOW_FPS, NGAGE_ShowFPSChanged, this);
delete iRenderer;
iRenderer = 0;
@@ -266,6 +285,8 @@ void CRenderer::ConstructL()
}
iDirectScreen->ScreenDevice()->SetAutoUpdate(ETrue);
}
SDL_AddHintCallback(SDL_HINT_RENDER_NGAGE_SHOW_FPS, NGAGE_ShowFPSChanged, this);
}
void CRenderer::Restart(RDirectScreenAccess::TTerminationReasons aReason)
@@ -336,6 +357,8 @@ bool CRenderer::Copy(SDL_Renderer *renderer, SDL_Texture *texture, const SDL_Rec
return false;
}
NGAGE_TextureData *phdata = (NGAGE_TextureData *)texture->internal;
if (!phdata || !phdata->bitmap) {
return false;
@@ -346,7 +369,7 @@ bool CRenderer::Copy(SDL_Renderer *renderer, SDL_Texture *texture, const SDL_Rec
int sw = srcrect->w;
int sh = srcrect->h;
// Fast path: render target texture with no color mod.
// BitBlt directly from its bitmap — DataAddress() is unreliable
// for bitmaps that have been drawn into via a CFbsBitGc.
@@ -359,7 +382,8 @@ bool CRenderer::Copy(SDL_Renderer *renderer, SDL_Texture *texture, const SDL_Rec
SDL_GetTextureBlendMode(texture, &blend);
bool no_color_key = (blend != SDL_BLENDMODE_BLEND);
if (phdata->gc && no_color_mod && no_scale && no_color_key) {
if (phdata->gc && no_color_mod && no_scale && no_color_key)
{
CFbsBitGc *gc = GetCurrentGc();
if (gc) {
TRect aSource(TPoint(srcrect->x, srcrect->y), TSize(sw, sh));
@@ -369,6 +393,7 @@ bool CRenderer::Copy(SDL_Renderer *renderer, SDL_Texture *texture, const SDL_Rec
return true;
}
// Fast path: color-key with no color mod and no scale.
// Blit directly from the source bitmap into the destination, skipping transparent pixels.
if (no_color_mod && no_scale && !no_color_key && phdata->has_color_key) {
@@ -414,6 +439,7 @@ bool CRenderer::Copy(SDL_Renderer *renderer, SDL_Texture *texture, const SDL_Rec
return false;
}
}
TSize scratch_size = iScratchBitmap->SizeInPixels();
if (scratch_size.iWidth < sw || scratch_size.iHeight < sh) {
iScratchBitmap->Reset();
@@ -438,6 +464,7 @@ bool CRenderer::Copy(SDL_Renderer *renderer, SDL_Texture *texture, const SDL_Rec
void *source = iPixelBufferA;
void *dest = iPixelBufferB;
if (!no_color_mod) {
ApplyColorMod(dest, source, src_pitch, sw, sh, texture->color);
void *tmp = source;
@@ -934,13 +961,15 @@ void CRenderer::HandleEvent(const TWsEvent &aWsEvent)
timestamp = SDL_GetPerformanceCounter();
SDL_SendKeyboardKey(timestamp, 1, aWsEvent.Key()->iCode, ConvertScancode(aWsEvent.Key()->iScanCode), true);
/*
commented out so it works with hints
if (aWsEvent.Key()->iScanCode == EStdKeyHash) {
if (iShowFPS) {
iShowFPS = EFalse;
} else {
iShowFPS = ETrue;
}
}
}*/
break;
case EEventKeyUp: /* Key events */

View File

@@ -25,6 +25,10 @@
#define NGAGE_SCREEN_WIDTH 176
#define NGAGE_SCREEN_HEIGHT 208
#ifndef SDL_HINT_RENDER_NGAGE_SHOW_FPS
#define SDL_HINT_RENDER_NGAGE_SHOW_FPS "SDL_RENDER_NGAGE_SHOW_FPS"
#endif
#ifdef __cplusplus
extern "C" {
#endif
@@ -108,6 +112,8 @@ void NGAGE_SetDrawColor(const Uint32 color);
void NGAGE_PumpEventsInternal(void);
void NGAGE_SuspendScreenSaverInternal(bool suspend);
void NGAGE_SetRenderTargetInternal(NGAGE_TextureData *target);
void *NGAGE_GetBackbufferAddress(void);
int NGAGE_GetBackbufferPitch(void);
#ifdef __cplusplus
}

View File

@@ -46,6 +46,7 @@ class CRenderer : public MDirectScreenAccess
void SetClipRect(TInt aX, TInt aY, TInt aWidth, TInt aHeight);
void UpdateFPS();
void SuspendScreenSaver(TBool aSuspend);
void SetShowFPS(TBool aShow) { iShowFPS = aShow; }
// Render target management.
void SetRenderTarget(NGAGE_TextureData *aTarget);