{* UltraStar Deluxe - Karaoke Game
*
* UltraStar Deluxe is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* 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; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*
* $URL$
* $Id$
*}
unit UDisplay;
interface
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
{$I switches.inc}
uses
UCommon,
Math,
SDL,
gl,
glu,
SysUtils,
UMenu,
UPath,
UMusic,
UHookableEvent;
type
TDisplay = class
private
ePreDraw: THookableEvent;
eDraw: THookableEvent;
// fade-to-black
BlackScreen: boolean;
FadeEnabled: boolean; // true if fading is enabled
FadeFailed: boolean; // true if fading is possible (enough memory, etc.)
FadeStartTime: cardinal; // time when fading starts, 0 means that the fade texture must be initialized
DoneOnShow: boolean; // true if passed onShow after fading
FadeTex: array[0..1] of GLuint;
TexW, TexH: Cardinal;
FPSCounter: cardinal;
LastFPS: cardinal;
NextFPSSwap: cardinal;
OSD_LastError: string;
{ software cursor data }
Cursor_X: double;
Cursor_Y: double;
Cursor_Pressed: boolean;
// used for cursor fade out when there is no movement
Cursor_Visible: boolean;
Cursor_LastMove: cardinal;
Cursor_Fade: boolean;
procedure DrawDebugInformation;
{ called by MoveCursor and OnMouseButton to update last move and start fade in }
procedure UpdateCursorFade;
public
Cursor_HiddenByScreen: boolean; // hides software cursor and deactivate auto fade in, must be public for access in UMenuButton
NextScreen: PMenu;
CurrentScreen: PMenu;
// popup data
NextScreenWithCheck: Pmenu;
CheckOK: boolean;
// FIXME: Fade is set to 0 in UMain and other files but not used here anymore.
Fade: real;
constructor Create;
destructor Destroy; override;
procedure InitFadeTextures();
procedure SaveScreenShot;
function Draw: boolean;
{ calls ParseInput of cur or next Screen if assigned }
function ParseInput(PressedKey: cardinal; CharCode: UCS4Char; PressedDown : boolean): boolean;
{ sets SDL_ShowCursor depending on options set in Ini }
procedure SetCursor;
{ called when cursor moves, positioning of software cursor }
procedure MoveCursor(X, Y: double);
{ called when left or right mousebutton is pressed or released }
procedure OnMouseButton(Pressed: boolean);
{ fades to specific screen (playing specified sound) }
function FadeTo(Screen: PMenu; const aSound: TAudioPlaybackStream = nil): PMenu;
{ abort fading to the current screen, may be used in OnShow, or during fade process }
procedure AbortScreenChange;
{ draws software cursor }
procedure DrawCursor;
end;
var
Display: TDisplay;
const
{ constants for screen transition
time in milliseconds }
FADE_DURATION = 400;
{ constants for software cursor effects
time in milliseconds }
CURSOR_FADE_IN_TIME = 500; // seconds the fade in effect lasts
CURSOR_FADE_OUT_TIME = 2000; // seconds the fade out effect lasts
CURSOR_AUTOHIDE_TIME = 5000; // seconds until auto fade out starts if there is no mouse movement
implementation
uses
TextGL,
UCommandLine,
UGraphic,
UIni,
UImage,
ULog,
UMain,
UTexture,
UTime,
ULanguage,
UPathUtils;
constructor TDisplay.Create;
begin
inherited Create;
// create events for plugins
ePreDraw := THookableEvent.Create('Display.PreDraw');
eDraw := THookableEvent.Create('Display.Draw');
// init popup
CheckOK := false;
NextScreen := nil;
NextScreenWithCheck := nil;
BlackScreen := false;
// init fade
FadeStartTime := 0;
FadeEnabled := (Ini.ScreenFade = 1);
FadeFailed := false;
DoneOnShow := false;
glGenTextures(2, PGLuint(@FadeTex));
InitFadeTextures();
// set LastError for OSD to No Error
OSD_LastError := 'No Errors';
// software cursor default values
Cursor_LastMove := 0;
Cursor_Visible := false;
Cursor_Pressed := false;
Cursor_X := -1;
Cursor_Y := -1;
Cursor_Fade := false;
Cursor_HiddenByScreen := true;
end;
destructor TDisplay.Destroy;
begin
glDeleteTextures(2, @FadeTex);
inherited Destroy;
end;
procedure TDisplay.InitFadeTextures();
var
i: integer;
begin
TexW := Round(Power(2, Ceil(Log2(ScreenW div Screens))));
TexH := Round(Power(2, Ceil(Log2(ScreenH))));
for i := 0 to 1 do
begin
glBindTexture(GL_TEXTURE_2D, FadeTex[i]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, 3, TexW, TexH, 0, GL_RGB, GL_UNSIGNED_BYTE, nil);
end;
end;
function TDisplay.Draw: boolean;
var
S: integer;
FadeStateSquare: real;
FadeW, FadeH: real;
FadeCopyW, FadeCopyH: integer;
glError: glEnum;
begin
Result := true;
for S := 1 to Screens do
begin
ScreenAct := S;
//if Screens = 1 then ScreenX := 0;
//if (Screens = 2) and (S = 1) then ScreenX := -1;
//if (Screens = 2) and (S = 2) then ScreenX := 1;
ScreenX := 0;
glViewPort((S-1) * ScreenW div Screens, 0, ScreenW div Screens, ScreenH);
// popup check was successful... move on
if CheckOK then
begin
if assigned(NextScreenWithCheck) then
begin
NextScreen := NextScreenWithCheck;
NextScreenWithCheck := nil;
CheckOk := false;
end
else
begin
// on end of game fade to black before exit
BlackScreen := true;
end;
end;
if (not assigned(NextScreen)) and (not BlackScreen) then
begin
ePreDraw.CallHookChain(false);
CurrentScreen.Draw;
// popup
if (ScreenPopupError <> nil) and ScreenPopupError.Visible then
ScreenPopupError.Draw
else if (ScreenPopupInfo <> nil) and ScreenPopupInfo.Visible then
ScreenPopupInfo.Draw
else if (ScreenPopupCheck <> nil) and ScreenPopupCheck.Visible then
ScreenPopupCheck.Draw;
// fade
FadeStartTime := 0;
if ((Ini.ScreenFade = 1) and (not FadeFailed)) then
FadeEnabled := true
else
FadeEnabled := false;
eDraw.CallHookChain(false);
end
else
begin
// disable fading if initialization failed
if (FadeEnabled and FadeFailed) then
begin
FadeEnabled := false;
end;
if (FadeEnabled and not FadeFailed) then
begin
// create fading texture if we're just starting
if FadeStartTime = 0 then
begin
// draw screen that will be faded
ePreDraw.CallHookChain(false);
CurrentScreen.Draw;
eDraw.CallHookChain(false);
// clear OpenGL errors, otherwise fading might be disabled due to some
// older errors in previous OpenGL calls.
glGetError();
FadeCopyW := ScreenW div Screens;
FadeCopyH := ScreenH;
// it is possible that our fade textures are too small after a window
// resize. In that case resize the fade texture to fit the requirements.
if (TexW < FadeCopyW) or (TexH < FadeCopyH) then
InitFadeTextures();
// copy screen to texture
glBindTexture(GL_TEXTURE_2D, FadeTex[S-1]);
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, (S-1) * ScreenW div Screens, 0,
FadeCopyW, FadeCopyH);
glError := glGetError();
if (glError <> GL_NO_ERROR) then
begin
FadeFailed := true;
Log.LogError('Fading disabled: ' + gluErrorString(glError), 'TDisplay.Draw');
end;
if not BlackScreen and (S = 1) and not DoneOnShow then
begin
NextScreen.OnShow;
DoneOnShow := true;
end;
// set fade time once on second screen (or first if screens = 1)
if (Screens = 1) or (S = 2) then
FadeStartTime := SDL_GetTicks;
end; // end texture creation in first fading step
if not BlackScreen then
begin
ePreDraw.CallHookChain(false);
NextScreen.Draw; // draw next screen
eDraw.CallHookChain(false);
end
else if ScreenAct = 1 then
begin
// draw black screen
glClearColor(0, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
end;
// and draw old screen over it... slowly fading out
if (FadeStartTime = 0) then
FadeStateSquare := 0 // for first screen if screens = 2
else
FadeStateSquare := sqr((SDL_GetTicks - FadeStartTime) / FADE_DURATION);
if (FadeStateSquare < 1) then
begin
FadeW := (ScreenW div Screens)/TexW;
FadeH := ScreenH/TexH;
glBindTexture(GL_TEXTURE_2D, FadeTex[S-1]);
// TODO: check if glTexEnvi() gives any speed improvement
//glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
glColor4f(1, 1, 1, 1-FadeStateSquare);
glEnable(GL_TEXTURE_2D);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_BLEND);
glBegin(GL_QUADS);
glTexCoord2f((0+FadeStateSquare/2)*FadeW, (0+FadeStateSquare/2)*FadeH);
glVertex2f(0, RenderH);
glTexCoord2f((0+FadeStateSquare/2)*FadeW, (1-FadeStateSquare/2)*FadeH);
glVertex2f(0, 0);
glTexCoord2f((1-FadeStateSquare/2)*FadeW, (1-FadeStateSquare/2)*FadeH);
glVertex2f(RenderW, 0);
glTexCoord2f((1-FadeStateSquare/2)*FadeW, (0+FadeStateSquare/2)*FadeH);
glVertex2f(RenderW, RenderH);
glEnd;
glDisable(GL_BLEND);
glDisable(GL_TEXTURE_2D);
// reset to default
//glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
end;
end
// there is no need to init next screen if it is a black screen
else if not BlackScreen then
begin
NextScreen.OnShow;
end;
if ((FadeStartTime + FADE_DURATION < SDL_GetTicks) or
(not FadeEnabled) or FadeFailed) and
((Screens = 1) or (S = 2)) then
begin
// fade out complete...
FadeStartTime := 0;
DoneOnShow := false;
CurrentScreen.onHide;
CurrentScreen.ShowFinish := false;
CurrentScreen := NextScreen;
NextScreen := nil;
if not BlackScreen then
begin
CurrentScreen.OnShowFinish;
CurrentScreen.ShowFinish := true;
end
else
begin
Result := false;
Break;
end;
end;
end; // if
// Draw OSD only on first Screen if Debug Mode is enabled
if ((Ini.Debug = 1) or (Params.Debug)) and (S = 1) then
DrawDebugInformation;
if not BlackScreen then
DrawCursor;
end; // for
end;
{ sets SDL_ShowCursor depending on options set in Ini }
procedure TDisplay.SetCursor;
var
Cursor: Integer;
begin
Cursor := 0;
if (CurrentScreen <> @ScreenSing) or (Cursor_HiddenByScreen) then
begin // hide cursor on singscreen
if (Ini.Mouse = 0) and (Ini.FullScreen = 0) then
// show sdl (os) cursor in window mode even when mouse support is off
Cursor := 1
else if (Ini.Mouse = 1) then
// show sdl (os) cursor when hardware cursor is selected
Cursor := 1;
if (Ini.Mouse <> 2) then
Cursor_HiddenByScreen := false;
end
else if (Ini.Mouse <> 2) then
Cursor_HiddenByScreen := true;
SDL_ShowCursor(Cursor);
if (Ini.Mouse = 2) then
begin
if Cursor_HiddenByScreen then
begin
// show software cursor
Cursor_HiddenByScreen := false;
Cursor_Visible := false;
Cursor_Fade := false;
end
else if (CurrentScreen = @ScreenSing) then
begin
// hide software cursor in singscreen
Cursor_HiddenByScreen := true;
Cursor_Visible := false;
Cursor_Fade := false;
end;
end;
end;
{ called by MoveCursor and OnMouseButton to update last move and start fade in }
procedure TDisplay.UpdateCursorFade;
var
Ticks: cardinal;
begin
Ticks := SDL_GetTicks;
{ fade in on movement (or button press) if not first movement }
if (not Cursor_Visible) and (Cursor_LastMove <> 0) then
begin
if Cursor_Fade then // we use a trick here to consider progress of fade out
Cursor_LastMove := Ticks - round(CURSOR_FADE_IN_TIME * (1 - (Ticks - Cursor_LastMove)/CURSOR_FADE_OUT_TIME))
else
Cursor_LastMove := Ticks;
Cursor_Visible := true;
Cursor_Fade := true;
end
else if not Cursor_Fade then
begin
Cursor_LastMove := Ticks;
end;
end;
{ called when cursor moves, positioning of software cursor }
procedure TDisplay.MoveCursor(X, Y: double);
begin
if (Ini.Mouse = 2) and
((X <> Cursor_X) or (Y <> Cursor_Y)) then
begin
Cursor_X := X;
Cursor_Y := Y;
UpdateCursorFade;
end;
end;
{ called when left or right mousebutton is pressed or released }
procedure TDisplay.OnMouseButton(Pressed: boolean);
begin
if (Ini.Mouse = 2) then
begin
Cursor_Pressed := Pressed;
UpdateCursorFade;
end;
end;
{ draws software cursor }
procedure TDisplay.DrawCursor;
var
Alpha: single;
Ticks: cardinal;
DrawX: double;
begin
if (Ini.Mouse = 2) and ((Screens = 1) or ((ScreenAct - 1) = (Round(Cursor_X+16) div RenderW))) then
begin // draw software cursor
Ticks := SDL_GetTicks;
if (Cursor_Visible) and (Cursor_LastMove + CURSOR_AUTOHIDE_TIME <= Ticks) then
begin // start fade out after 5 secs w/o activity
Cursor_Visible := false;
Cursor_LastMove := Ticks;
Cursor_Fade := true;
end;
// fading
if Cursor_Fade then
begin
if Cursor_Visible then
begin // fade in
if (Cursor_LastMove + CURSOR_FADE_IN_TIME <= Ticks) then
Cursor_Fade := false
else
Alpha := sin((Ticks - Cursor_LastMove) * 0.5 * pi / CURSOR_FADE_IN_TIME) * 0.7;
end
else
begin //fade out
if (Cursor_LastMove + CURSOR_FADE_OUT_TIME <= Ticks) then
Cursor_Fade := false
else
Alpha := cos((Ticks - Cursor_LastMove) * 0.5 * pi / CURSOR_FADE_OUT_TIME) * 0.7;
end;
end;
// no else if here because we may turn off fade in if block
if not Cursor_Fade then
begin
if Cursor_Visible then
Alpha := 0.7 // alpha when cursor visible and not fading
else
Alpha := 0; // alpha when cursor is hidden
end;
if (Alpha > 0) and (not Cursor_HiddenByScreen) then
begin
DrawX := Cursor_X;
if (ScreenAct = 2) then
DrawX := DrawX - RenderW;
glColor4f(1, 1, 1, Alpha);
glEnable(GL_TEXTURE_2D);
glEnable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
if (Cursor_Pressed) and (Tex_Cursor_Pressed.TexNum > 0) then
glBindTexture(GL_TEXTURE_2D, Tex_Cursor_Pressed.TexNum)
else
glBindTexture(GL_TEXTURE_2D, Tex_Cursor_Unpressed.TexNum);
glBegin(GL_QUADS);
glTexCoord2f(0, 0);
glVertex2f(DrawX, Cursor_Y);
glTexCoord2f(0, 1);
glVertex2f(DrawX, Cursor_Y + 32);
glTexCoord2f(1, 1);
glVertex2f(DrawX + 32, Cursor_Y + 32);
glTexCoord2f(1, 0);
glVertex2f(DrawX + 32, Cursor_Y);
glEnd;
glDisable(GL_BLEND);
glDisable(GL_TEXTURE_2D);
end;
end;
end;
function TDisplay.ParseInput(PressedKey: cardinal; CharCode: UCS4Char; PressedDown : boolean): boolean;
begin
if (assigned(NextScreen)) then
Result := NextScreen^.ParseInput(PressedKey, CharCode, PressedDown)
else if (assigned(CurrentScreen)) then
Result := CurrentScreen^.ParseInput(PressedKey, CharCode, PressedDown)
else
Result := True;
end;
{ abort fading to the next screen, may be used in OnShow, or during fade process }
procedure TDisplay.AbortScreenChange;
var
Temp: PMenu;
begin
// this is some kind of "hack" it is based on the
// code that is used to change the screens in TDisplay.Draw
// we should rewrite this whole behaviour, as it is not well
// structured and not well extendable. Also we should offer
// a possibility to change screens to plugins
// change this code when restructuring is done
if (assigned(NextScreen)) then
begin
// we have to swap the screens
Temp := CurrentScreen;
CurrentScreen := NextScreen;
NextScreen := Temp;
// and call the OnShow procedure of the previous screen
// because it was already called by default fade procedure
NextScreen.OnShow;
end;
end;
{ fades to specific screen (playing specified sound)
returns old screen }
function TDisplay.FadeTo(Screen: PMenu; const aSound: TAudioPlaybackStream = nil): PMenu;
begin
Result := CurrentScreen;
if (Result <> nil) then
begin
if (aSound <> nil) then
Result.FadeTo(Screen, aSound)
else
Result.FadeTo(Screen);
end;
end;
procedure TDisplay.SaveScreenShot;
var
Num: integer;
FileName: IPath;
Prefix: UTF8String;
ScreenData: PChar;
Surface: PSDL_Surface;
Success: boolean;
Align: integer;
RowSize: integer;
begin
// Exit if Screenshot-path does not exist or read-only
if (ScreenshotsPath.IsUnset) then
Exit;
for Num := 1 to 9999 do
begin
// fill prefix to 4 digits with leading '0', e.g. '0001'
Prefix := Format('screenshot%.4d', [Num]);
FileName := ScreenshotsPath.Append(Prefix + '.png');
if not FileName.Exists() then
break;
end;
// we must take the row-alignment (4byte by default) into account
glGetIntegerv(GL_PACK_ALIGNMENT, @Align);
// calc aligned row-size
RowSize := ((ScreenW*3 + (Align-1)) div Align) * Align;
GetMem(ScreenData, RowSize * ScreenH);
glReadPixels(0, 0, ScreenW, ScreenH, GL_RGB, GL_UNSIGNED_BYTE, ScreenData);
// on big endian machines (powerpc) this may need to be changed to
// Needs to be tests. KaMiSchi Sept 2008
// in this case one may have to add " glext, " to the list of used units
// glReadPixels(0, 0, ScreenW, ScreenH, GL_BGR, GL_UNSIGNED_BYTE, ScreenData);
Surface := SDL_CreateRGBSurfaceFrom(
ScreenData, ScreenW, ScreenH, 24, RowSize,
$0000FF, $00FF00, $FF0000, 0);
// Success := WriteJPGImage(FileName, Surface, 95);
// Success := WriteBMPImage(FileName, Surface);
Success := WritePNGImage(FileName, Surface);
if Success then
ScreenPopupInfo.ShowPopup(Format(Language.Translate('SCREENSHOT_SAVED'), [FileName.GetName.ToUTF8()]))
else
ScreenPopupError.ShowPopup(Language.Translate('SCREENSHOT_FAILED'));
SDL_FreeSurface(Surface);
FreeMem(ScreenData);
end;
//------------
// DrawDebugInformation - procedure draw fps and some other informations on screen
//------------
procedure TDisplay.DrawDebugInformation;
var
Ticks: cardinal;
begin
// Some White Background for information
glEnable(GL_BLEND);
glDisable(GL_TEXTURE_2D);
glColor4f(1, 1, 1, 0.5);
glBegin(GL_QUADS);
glVertex2f(690, 44);
glVertex2f(690, 0);
glVertex2f(800, 0);
glVertex2f(800, 44);
glEnd;
glDisable(GL_BLEND);
// set font specs
SetFontStyle(ftNormal);
SetFontSize(21);
SetFontItalic(false);
glColor4f(0, 0, 0, 1);
// calculate fps
Ticks := SDL_GetTicks();
if (Ticks >= NextFPSSwap) then
begin
LastFPS := FPSCounter * 4;
FPSCounter := 0;
NextFPSSwap := Ticks + 250;
end;
Inc(FPSCounter);
// draw text
// fps
SetFontPos(695, 0);
glPrint ('FPS: ' + InttoStr(LastFPS));
// rspeed
SetFontPos(695, 13);
glPrint ('RSpeed: ' + InttoStr(Round(1000 * TimeMid)));
// lasterror
SetFontPos(695, 26);
glColor4f(1, 0, 0, 1);
glPrint (OSD_LastError);
glColor4f(1, 1, 1, 1);
end;
end.