/* * 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 "port.h" #include "libs/threadlib.h" #include "libs/graphics/drawcmd.h" #include "libs/graphics/drawable.h" #include "libs/graphics/context.h" #include "libs/graphics/dcqueue.h" #include "libs/graphics/gfx_common.h" #include "libs/graphics/bbox.h" #include "libs/timelib.h" #include "libs/log.h" #include "libs/misc.h" // for TFB_DEBUG_HALT static RecursiveMutex DCQ_Mutex; CondVar RenderingCond; TFB_DrawCommand DCQ[DCQ_MAX]; TFB_DrawCommandQueue DrawCommandQueue; #define FPS_PERIOD (ONE_SECOND / 100) int RenderedFrames = 0; // Wait for the queue to be emptied. static void TFB_WaitForSpace (int requested_slots) { int old_depth, i; log_add (log_Debug, "DCQ overload (Size = %d, FullSize = %d, " "Requested = %d). Sleeping until renderer is done.", DrawCommandQueue.Size, DrawCommandQueue.FullSize, requested_slots); // Restore the DCQ locking level. I *think* this is // always 1, but... TFB_BatchReset (); old_depth = GetRecursiveMutexDepth (DCQ_Mutex); for (i = 0; i < old_depth; i++) UnlockRecursiveMutex (DCQ_Mutex); WaitCondVar (RenderingCond); for (i = 0; i < old_depth; i++) LockRecursiveMutex (DCQ_Mutex); log_add (log_Debug, "DCQ clear (Size = %d, FullSize = %d). Continuing.", DrawCommandQueue.Size, DrawCommandQueue.FullSize); } void Lock_DCQ (int slots) { LockRecursiveMutex (DCQ_Mutex); while (DrawCommandQueue.FullSize >= DCQ_MAX - slots) { TFB_WaitForSpace (slots); } } void Unlock_DCQ (void) { UnlockRecursiveMutex (DCQ_Mutex); } // Always have the DCQ locked when calling this. static void Synchronize_DCQ (void) { if (!DrawCommandQueue.Batching) { int front = DrawCommandQueue.Front; int back = DrawCommandQueue.InsertionPoint; DrawCommandQueue.Back = DrawCommandQueue.InsertionPoint; if (front <= back) { DrawCommandQueue.Size = (back - front); } else { DrawCommandQueue.Size = (back + DCQ_MAX - front); } DrawCommandQueue.FullSize = DrawCommandQueue.Size; } } void TFB_BatchGraphics (void) { LockRecursiveMutex (DCQ_Mutex); DrawCommandQueue.Batching++; UnlockRecursiveMutex (DCQ_Mutex); } void TFB_UnbatchGraphics (void) { LockRecursiveMutex (DCQ_Mutex); if (DrawCommandQueue.Batching) { DrawCommandQueue.Batching--; } Synchronize_DCQ (); UnlockRecursiveMutex (DCQ_Mutex); } // Cancel all pending batch operations, making them unbatched. This will // cause a small amount of flicker when invoked, but prevents // batching problems from freezing the game. void TFB_BatchReset (void) { LockRecursiveMutex (DCQ_Mutex); DrawCommandQueue.Batching = 0; Synchronize_DCQ (); UnlockRecursiveMutex (DCQ_Mutex); } // Draw Command Queue Stuff void Init_DrawCommandQueue (void) { DrawCommandQueue.Back = 0; DrawCommandQueue.Front = 0; DrawCommandQueue.InsertionPoint = 0; DrawCommandQueue.Batching = 0; DrawCommandQueue.FullSize = 0; DrawCommandQueue.Size = 0; TFB_BBox_Init (ScreenWidth, ScreenHeight); DCQ_Mutex = CreateRecursiveMutex ("DCQ", SYNC_CLASS_TOPLEVEL | SYNC_CLASS_VIDEO); RenderingCond = CreateCondVar ("DCQ empty", SYNC_CLASS_TOPLEVEL | SYNC_CLASS_VIDEO); } void Uninit_DrawCommandQueue (void) { if (RenderingCond) { DestroyCondVar (RenderingCond); RenderingCond = 0; } if (DCQ_Mutex) { DestroyRecursiveMutex (DCQ_Mutex); DCQ_Mutex = 0; } } void TFB_DrawCommandQueue_Push (TFB_DrawCommand* Command) { Lock_DCQ (1); DCQ[DrawCommandQueue.InsertionPoint] = *Command; DrawCommandQueue.InsertionPoint = (DrawCommandQueue.InsertionPoint + 1) % DCQ_MAX; DrawCommandQueue.FullSize++; Synchronize_DCQ (); Unlock_DCQ (); } int TFB_DrawCommandQueue_Pop (TFB_DrawCommand *target) { LockRecursiveMutex (DCQ_Mutex); if (DrawCommandQueue.Size == 0) { Unlock_DCQ (); return (0); } if (DrawCommandQueue.Front == DrawCommandQueue.Back && DrawCommandQueue.Size != DCQ_MAX) { log_add (log_Debug, "Augh! Assertion failure in DCQ! " "Front == Back, Size != DCQ_MAX"); DrawCommandQueue.Size = 0; Unlock_DCQ (); return (0); } *target = DCQ[DrawCommandQueue.Front]; DrawCommandQueue.Front = (DrawCommandQueue.Front + 1) % DCQ_MAX; DrawCommandQueue.Size--; DrawCommandQueue.FullSize--; UnlockRecursiveMutex (DCQ_Mutex); return 1; } void TFB_DrawCommandQueue_Clear () { LockRecursiveMutex (DCQ_Mutex); DrawCommandQueue.Size = 0; DrawCommandQueue.Front = 0; DrawCommandQueue.Back = 0; DrawCommandQueue.Batching = 0; DrawCommandQueue.FullSize = 0; DrawCommandQueue.InsertionPoint = 0; UnlockRecursiveMutex (DCQ_Mutex); } static void checkExclusiveThread (TFB_DrawCommand* DrawCommand) { #ifdef DEBUG_DCQ_THREADS static uint32 exclusiveThreadId; extern uint32 SDL_ThreadID(void); // Only one thread is currently allowed to enqueue commands // This is not a technical limitation but rather a semantical one atm. if (DrawCommand->Type == TFB_DRAWCOMMANDTYPE_REINITVIDEO) { // TFB_DRAWCOMMANDTYPE_REINITVIDEO is an exception // It is queued from the main() thread, which is safe to do return; } if (!exclusiveThreadId) exclusiveThreadId = SDL_ThreadID(); else assert (SDL_ThreadID() == exclusiveThreadId); #else (void) DrawCommand; // suppress unused warning #endif } void TFB_EnqueueDrawCommand (TFB_DrawCommand* DrawCommand) { if (TFB_DEBUG_HALT) { return; } checkExclusiveThread (DrawCommand); if (DrawCommand->Type <= TFB_DRAWCOMMANDTYPE_COPYTOIMAGE && _CurFramePtr->Type == SCREEN_DRAWABLE) { static RECT scissor_rect; // Set the clipping region. // We allow drawing with no current context set, so the whole screen if ((_pCurContext && !rectsEqual (scissor_rect, _pCurContext->ClipRect)) || (!_pCurContext && scissor_rect.extent.width != 0)) { // Enqueue command to set the glScissor spec TFB_DrawCommand DC; if (_pCurContext) scissor_rect = _pCurContext->ClipRect; else scissor_rect.extent.width = 0; if (scissor_rect.extent.width) { DC.Type = TFB_DRAWCOMMANDTYPE_SCISSORENABLE; DC.data.scissor.rect = scissor_rect; } else { DC.Type = TFB_DRAWCOMMANDTYPE_SCISSORDISABLE; } TFB_EnqueueDrawCommand(&DC); } } TFB_DrawCommandQueue_Push (DrawCommand); } static void computeFPS (void) { static TimeCount last_time; static TimePeriod fps_counter; TimeCount current_time; TimePeriod delta_time; current_time = GetTimeCounter (); delta_time = current_time - last_time; last_time = current_time; fps_counter += delta_time; if (fps_counter > FPS_PERIOD) { log_add (log_User, "fps %.2f, effective %.2f", (float)ONE_SECOND / delta_time, (float)ONE_SECOND * RenderedFrames / fps_counter); fps_counter = 0; RenderedFrames = 0; } } // Only call from main() thread!! void TFB_FlushGraphics (void) { int commands_handled; BOOLEAN livelock_deterrence; // This is technically a locking violation on DrawCommandQueue.Size, // but it is likely to not be very destructive. if (DrawCommandQueue.Size == 0) { static int last_fade = 255; static int last_transition = 255; int current_fade = GetFadeAmount (); int current_transition = TransitionAmount; if ((current_fade != 255 && current_fade != last_fade) || (current_transition != 255 && current_transition != last_transition) || (current_fade == 255 && last_fade != 255) || (current_transition == 255 && last_transition != 255)) { TFB_SwapBuffers (TFB_REDRAW_FADING); // if fading, redraw every frame } else { TaskSwitch (); } last_fade = current_fade; last_transition = current_transition; BroadcastCondVar (RenderingCond); return; } if (GfxFlags & TFB_GFXFLAGS_SHOWFPS) computeFPS (); commands_handled = 0; livelock_deterrence = FALSE; if (DrawCommandQueue.FullSize > DCQ_FORCE_BREAK_SIZE) { TFB_BatchReset (); } if (DrawCommandQueue.Size > DCQ_FORCE_SLOWDOWN_SIZE) { Lock_DCQ (-1); livelock_deterrence = TRUE; } TFB_BBox_Reset (); for (;;) { TFB_DrawCommand DC; if (!TFB_DrawCommandQueue_Pop (&DC)) { // the Queue is now empty. break; } ++commands_handled; if (!livelock_deterrence && commands_handled + DrawCommandQueue.Size > DCQ_LIVELOCK_MAX) { // log_add (log_Debug, "Initiating livelock deterrence!"); livelock_deterrence = TRUE; Lock_DCQ (-1); } switch (DC.Type) { case TFB_DRAWCOMMANDTYPE_SETMIPMAP: { TFB_DrawCommand_SetMipmap *cmd = &DC.data.setmipmap; TFB_DrawImage_SetMipmap (cmd->image, cmd->mipmap, cmd->hotx, cmd->hoty); break; } case TFB_DRAWCOMMANDTYPE_IMAGE: { TFB_DrawCommand_Image *cmd = &DC.data.image; TFB_Image *DC_image = cmd->image; const int x = cmd->x; const int y = cmd->y; TFB_DrawCanvas_Image (DC_image, x, y, cmd->scale, cmd->scaleMode, cmd->colormap, cmd->drawMode, TFB_GetScreenCanvas (cmd->destBuffer)); if (cmd->destBuffer == TFB_SCREEN_MAIN) { LockMutex (DC_image->mutex); if (cmd->scale) TFB_BBox_RegisterCanvas (DC_image->ScaledImg, x - DC_image->last_scale_hs.x, y - DC_image->last_scale_hs.y); else TFB_BBox_RegisterCanvas (DC_image->NormalImg, x - DC_image->NormalHs.x, y - DC_image->NormalHs.y); UnlockMutex (DC_image->mutex); } break; } case TFB_DRAWCOMMANDTYPE_FILLEDIMAGE: { TFB_DrawCommand_FilledImage *cmd = &DC.data.filledimage; TFB_Image *DC_image = cmd->image; const int x = cmd->x; const int y = cmd->y; TFB_DrawCanvas_FilledImage (DC_image, x, y, cmd->scale, cmd->scaleMode, cmd->color, cmd->drawMode, TFB_GetScreenCanvas (cmd->destBuffer)); if (cmd->destBuffer == TFB_SCREEN_MAIN) { LockMutex (DC_image->mutex); if (cmd->scale) TFB_BBox_RegisterCanvas (DC_image->ScaledImg, x - DC_image->last_scale_hs.x, y - DC_image->last_scale_hs.y); else TFB_BBox_RegisterCanvas (DC_image->NormalImg, x - DC_image->NormalHs.x, y - DC_image->NormalHs.y); UnlockMutex (DC_image->mutex); } break; } case TFB_DRAWCOMMANDTYPE_FONTCHAR: { TFB_DrawCommand_FontChar *cmd = &DC.data.fontchar; TFB_Char *DC_char = cmd->fontchar; const int x = cmd->x; const int y = cmd->y; TFB_DrawCanvas_FontChar (DC_char, cmd->backing, x, y, cmd->drawMode, TFB_GetScreenCanvas (cmd->destBuffer)); if (cmd->destBuffer == TFB_SCREEN_MAIN) { RECT r; r.corner.x = x - DC_char->HotSpot.x; r.corner.y = y - DC_char->HotSpot.y; r.extent.width = DC_char->extent.width; r.extent.height = DC_char->extent.height; TFB_BBox_RegisterRect (&r); } break; } case TFB_DRAWCOMMANDTYPE_LINE: { TFB_DrawCommand_Line *cmd = &DC.data.line; if (cmd->destBuffer == TFB_SCREEN_MAIN) { TFB_BBox_RegisterPoint (cmd->x1, cmd->y1); TFB_BBox_RegisterPoint (cmd->x2, cmd->y2); } TFB_DrawCanvas_Line (cmd->x1, cmd->y1, cmd->x2, cmd->y2, cmd->color, cmd->drawMode, TFB_GetScreenCanvas (cmd->destBuffer)); break; } case TFB_DRAWCOMMANDTYPE_RECTANGLE: { TFB_DrawCommand_Rect *cmd = &DC.data.rect; if (cmd->destBuffer == TFB_SCREEN_MAIN) TFB_BBox_RegisterRect (&cmd->rect); TFB_DrawCanvas_Rect (&cmd->rect, cmd->color, cmd->drawMode, TFB_GetScreenCanvas (cmd->destBuffer)); break; } case TFB_DRAWCOMMANDTYPE_SCISSORENABLE: { TFB_DrawCommand_Scissor *cmd = &DC.data.scissor; TFB_DrawCanvas_SetClipRect ( TFB_GetScreenCanvas (TFB_SCREEN_MAIN), &cmd->rect); TFB_BBox_SetClipRect (&DC.data.scissor.rect); break; } case TFB_DRAWCOMMANDTYPE_SCISSORDISABLE: TFB_DrawCanvas_SetClipRect ( TFB_GetScreenCanvas (TFB_SCREEN_MAIN), NULL); TFB_BBox_SetClipRect (NULL); break; case TFB_DRAWCOMMANDTYPE_COPYTOIMAGE: { TFB_DrawCommand_CopyToImage *cmd = &DC.data.copytoimage; TFB_Image *DC_image = cmd->image; const POINT dstPt = {0, 0}; if (DC_image == 0) { log_add (log_Debug, "DCQ ERROR: COPYTOIMAGE passed null " "image ptr"); break; } LockMutex (DC_image->mutex); TFB_DrawCanvas_CopyRect ( TFB_GetScreenCanvas (cmd->srcBuffer), &cmd->rect, DC_image->NormalImg, dstPt); UnlockMutex (DC_image->mutex); break; } case TFB_DRAWCOMMANDTYPE_COPY: { TFB_DrawCommand_Copy *cmd = &DC.data.copy; const RECT r = cmd->rect; if (cmd->destBuffer == TFB_SCREEN_MAIN) TFB_BBox_RegisterRect (&cmd->rect); TFB_DrawCanvas_CopyRect ( TFB_GetScreenCanvas (cmd->srcBuffer), &r, TFB_GetScreenCanvas (cmd->destBuffer), r.corner); break; } case TFB_DRAWCOMMANDTYPE_DELETEIMAGE: { TFB_Image *DC_image = DC.data.deleteimage.image; TFB_DrawImage_Delete (DC_image); break; } case TFB_DRAWCOMMANDTYPE_DELETEDATA: { void *data = DC.data.deletedata.data; HFree (data); break; } case TFB_DRAWCOMMANDTYPE_SENDSIGNAL: ClearSemaphore (DC.data.sendsignal.sem); break; case TFB_DRAWCOMMANDTYPE_REINITVIDEO: { TFB_DrawCommand_ReinitVideo *cmd = &DC.data.reinitvideo; int oldDriver = GraphicsDriver; int oldFlags = GfxFlags; int oldWidth = ScreenWidthActual; int oldHeight = ScreenHeightActual; if (TFB_ReInitGraphics (cmd->driver, cmd->flags, cmd->width, cmd->height)) { log_add (log_Error, "Could not provide requested mode: " "reverting to last known driver."); // We don't know what exactly failed, so roll it all back if (TFB_ReInitGraphics (oldDriver, oldFlags, oldWidth, oldHeight)) { log_add (log_Fatal, "Couldn't reinit at that point either. " "Your video has been somehow tied in knots."); exit (EXIT_FAILURE); } } TFB_SwapBuffers (TFB_REDRAW_YES); break; } case TFB_DRAWCOMMANDTYPE_CALLBACK: { DC.data.callback.callback (DC.data.callback.arg); break; } } } if (livelock_deterrence) Unlock_DCQ (); TFB_SwapBuffers (TFB_REDRAW_NO); RenderedFrames++; BroadcastCondVar (RenderingCond); } void TFB_PurgeDanglingGraphics (void) { Lock_DCQ (-1); for (;;) { TFB_DrawCommand DC; if (!TFB_DrawCommandQueue_Pop (&DC)) { // the Queue is now empty. break; } switch (DC.Type) { case TFB_DRAWCOMMANDTYPE_DELETEIMAGE: { TFB_Image *DC_image = DC.data.deleteimage.image; TFB_DrawImage_Delete (DC_image); break; } case TFB_DRAWCOMMANDTYPE_DELETEDATA: { void *data = DC.data.deletedata.data; HFree (data); break; } case TFB_DRAWCOMMANDTYPE_IMAGE: { TFB_ColorMap *cmap = DC.data.image.colormap; if (cmap) TFB_ReturnColorMap (cmap); break; } case TFB_DRAWCOMMANDTYPE_SENDSIGNAL: { ClearSemaphore (DC.data.sendsignal.sem); break; } } } Unlock_DCQ (); }