ft2_video.c (29433B)
1 // for finding memory leaks in debug mode with Visual Studio 2 #if defined _DEBUG && defined _MSC_VER 3 #include <crtdbg.h> 4 #endif 5 6 #include <stdio.h> 7 #include <stdint.h> 8 #include <stdbool.h> 9 #include <math.h> 10 #ifdef _WIN32 11 #define WIN32_MEAN_AND_LEAN 12 #include <windows.h> 13 #include <SDL2/SDL_syswm.h> 14 #else 15 #include <unistd.h> // usleep() 16 #endif 17 #include "ft2_header.h" 18 #include "ft2_config.h" 19 #include "ft2_gui.h" 20 #include "ft2_video.h" 21 #include "ft2_events.h" 22 #include "ft2_mouse.h" 23 #include "scopes/ft2_scopes.h" 24 #include "ft2_pattern_ed.h" 25 #include "ft2_pattern_draw.h" 26 #include "ft2_sample_ed.h" 27 #include "ft2_nibbles.h" 28 #include "ft2_inst_ed.h" 29 #include "ft2_diskop.h" 30 #include "ft2_about.h" 31 #include "ft2_trim.h" 32 #include "ft2_sampling.h" 33 #include "ft2_module_loader.h" 34 #include "ft2_midi.h" 35 #include "ft2_bmp.h" 36 #include "ft2_structs.h" 37 38 static const uint8_t textCursorData[12] = 39 { 40 PAL_FORGRND, PAL_FORGRND, PAL_FORGRND, 41 PAL_FORGRND, PAL_FORGRND, PAL_FORGRND, 42 PAL_FORGRND, PAL_FORGRND, PAL_FORGRND, 43 PAL_FORGRND, PAL_FORGRND, PAL_FORGRND 44 }; 45 46 video_t video; // globalized 47 48 static bool songIsModified; 49 static char wndTitle[256]; 50 static sprite_t sprites[SPRITE_NUM]; 51 52 // for FPS counter 53 #define FPS_LINES 15 54 #define FPS_SCAN_FRAMES 60 55 #define FPS_RENDER_W 285 56 #define FPS_RENDER_H (((FONT1_CHAR_H + 1) * FPS_LINES) + 1) 57 #define FPS_RENDER_X 2 58 #define FPS_RENDER_Y 2 59 60 static char fpsTextBuf[1024]; 61 static uint64_t frameStartTime; 62 static double dRunningFrameDuration, dAvgFPS; 63 // ------------------ 64 65 static void drawReplayerData(void); 66 67 void resetFPSCounter(void) 68 { 69 editor.framesPassed = 0; 70 fpsTextBuf[0] = '\0'; 71 dRunningFrameDuration = 1000.0 / VBLANK_HZ; 72 } 73 74 void beginFPSCounter(void) 75 { 76 if (video.showFPSCounter) 77 frameStartTime = SDL_GetPerformanceCounter(); 78 } 79 80 static void drawFPSCounter(void) 81 { 82 SDL_version SDLVer; 83 84 SDL_GetVersion(&SDLVer); 85 86 if (editor.framesPassed >= FPS_SCAN_FRAMES && (editor.framesPassed % FPS_SCAN_FRAMES) == 0) 87 { 88 dAvgFPS = 1000.0 / (dRunningFrameDuration / FPS_SCAN_FRAMES); 89 if (dAvgFPS < 0.0 || dAvgFPS > 99999999.9999) 90 dAvgFPS = 99999999.9999; // prevent number from overflowing text box 91 92 dRunningFrameDuration = 0.0; 93 } 94 95 clearRect(FPS_RENDER_X+2, FPS_RENDER_Y+2, FPS_RENDER_W, FPS_RENDER_H); 96 vLineDouble(FPS_RENDER_X, FPS_RENDER_Y+1, FPS_RENDER_H+2, PAL_FORGRND); 97 vLineDouble(FPS_RENDER_X+FPS_RENDER_W, FPS_RENDER_Y+1, FPS_RENDER_H+2, PAL_FORGRND); 98 hLineDouble(FPS_RENDER_X+1, FPS_RENDER_Y, FPS_RENDER_W, PAL_FORGRND); 99 hLineDouble(FPS_RENDER_X+1, FPS_RENDER_Y+FPS_RENDER_H+2, FPS_RENDER_W, PAL_FORGRND); 100 101 // if enough frame data isn't collected yet, show a message 102 if (editor.framesPassed < FPS_SCAN_FRAMES) 103 { 104 const char *text = "Gathering frame information..."; 105 const uint16_t textW = textWidth(text); 106 textOut(FPS_RENDER_X+((FPS_RENDER_W/2)-(textW/2)), FPS_RENDER_Y+((FPS_RENDER_H/2)-(FONT1_CHAR_H/2)), PAL_FORGRND, text); 107 return; 108 } 109 110 double dRefreshRate = video.dMonitorRefreshRate; 111 if (dRefreshRate < 0.0 || dRefreshRate > 9999.9) 112 dRefreshRate = 9999.9; // prevent number from overflowing text box 113 114 sprintf(fpsTextBuf, 115 "SDL version: %u.%u.%u\n" \ 116 "Frames per second: %.3f\n" \ 117 "Monitor refresh rate: %.1fHz (+/-)\n" \ 118 "59..61Hz GPU VSync used: %s\n" \ 119 "HPC frequency (timer): %.4fMHz\n" \ 120 "Audio frequency: %.1fkHz (expected %.1fkHz)\n" \ 121 "Audio buffer samples: %d (expected %d)\n" \ 122 "Render size: %dx%d (offset %d,%d)\n" \ 123 "Disp. size: %dx%d (window: %dx%d)\n" \ 124 "Render scaling: x=%.4f, y=%.4f\n" \ 125 "DPI zoom factors: x=%.4f, y=%.4f\n" \ 126 "Mouse pixel-space muls: x=%.4f, y=%.4f\n" \ 127 "Relative mouse coords: %d,%d\n" \ 128 "Absolute mouse coords: %d,%d\n" \ 129 "Press CTRL+SHIFT+F to close this box.\n", 130 SDLVer.major, SDLVer.minor, SDLVer.patch, 131 dAvgFPS, 132 dRefreshRate, 133 video.vsync60HzPresent ? "yes" : "no", 134 hpcFreq.freq64 / (1000.0 * 1000.0), 135 audio.haveFreq / 1000.0, audio.wantFreq / 1000.0, 136 audio.haveSamples, audio.wantSamples, 137 video.renderW, video.renderH, video.renderX, video.renderY, 138 video.displayW, video.displayH, video.windowW, video.windowH, 139 (double)video.renderW / SCREEN_W, (double)video.renderH / SCREEN_H, 140 video.dDpiZoomFactorX, video.dDpiZoomFactorY, 141 video.dMouseXMul, video.dMouseYMul, 142 mouse.x, mouse.y, 143 mouse.absX, mouse.absY); 144 145 // draw text 146 147 uint16_t xPos = FPS_RENDER_X+3; 148 uint16_t yPos = FPS_RENDER_Y+3; 149 150 char *textPtr = fpsTextBuf; 151 while (*textPtr != '\0') 152 { 153 const char ch = *textPtr++; 154 if (ch == '\n') 155 { 156 yPos += FONT1_CHAR_H+1; 157 xPos = FPS_RENDER_X+3; 158 continue; 159 } 160 161 charOut(xPos, yPos, PAL_FORGRND, ch); 162 xPos += charWidth(ch); 163 } 164 165 // draw framerate tester symbol 166 167 const uint16_t symbolEnd = 115; 168 169 // ping-pong movement 170 uint16_t x = editor.framesPassed % (symbolEnd * 2); 171 if (x >= symbolEnd) 172 x = (symbolEnd * 2) - x; 173 174 charOut(164 + x, 16, PAL_FORGRND, '*'); 175 } 176 177 void endFPSCounter(void) 178 { 179 if (video.showFPSCounter && frameStartTime > 0) 180 { 181 uint64_t frameTimeDiff64 = SDL_GetPerformanceCounter() - frameStartTime; 182 if (frameTimeDiff64 > INT32_MAX) 183 frameTimeDiff64 = INT32_MAX; 184 185 dRunningFrameDuration += (int32_t)frameTimeDiff64 * hpcFreq.dFreqMulMs; 186 } 187 } 188 189 void flipFrame(void) 190 { 191 const uint32_t windowFlags = SDL_GetWindowFlags(video.window); 192 bool minimized = (windowFlags & SDL_WINDOW_MINIMIZED) ? true : false; 193 194 renderSprites(); 195 196 if (video.showFPSCounter) 197 drawFPSCounter(); 198 199 SDL_UpdateTexture(video.texture, NULL, video.frameBuffer, SCREEN_W * sizeof (int32_t)); 200 201 // SDL 2.0.14 bug on Windows (?): This function consumes ever-increasing memory if the program is minimized 202 if (!minimized) 203 SDL_RenderClear(video.renderer); 204 205 if (video.useCustomRenderRect) 206 SDL_RenderCopy(video.renderer, video.texture, NULL, &video.renderRect); 207 else 208 SDL_RenderCopy(video.renderer, video.texture, NULL, NULL); 209 210 SDL_RenderPresent(video.renderer); 211 212 eraseSprites(); 213 214 if (!video.vsync60HzPresent) 215 { 216 // we have no VSync, do crude thread sleeping to sync to ~60Hz 217 hpc_Wait(&video.vblankHpc); 218 } 219 else 220 { 221 /* We have VSync, but it can unexpectedly get inactive in certain scenarios. 222 ** We have to force thread sleeping (to ~60Hz) if so. 223 */ 224 #ifdef __APPLE__ 225 // macOS: VSync gets disabled if the window is 100% covered by another window. Let's add a (crude) fix: 226 if (minimized || !(windowFlags & SDL_WINDOW_INPUT_FOCUS)) 227 hpc_Wait(&video.vblankHpc); 228 #elif __unix__ 229 /* *NIX: VSync can get disabled in fullscreen mode in some distros/systems. Let's add a fix. 230 ** 231 ** TODO/XXX: This is probably a BAD hack and can cause a poor fullscreen experience if VSync did 232 ** in fact work in fullscreen mode... 233 */ 234 if (minimized || video.fullscreen) 235 hpc_Wait(&video.vblankHpc); 236 #else 237 if (minimized) 238 hpc_Wait(&video.vblankHpc); 239 #endif 240 } 241 242 editor.framesPassed++; 243 244 /* Reset audio/video sync timestamp every half an hour to prevent 245 ** possible sync drifting after hours of playing a song without 246 ** a single song stop (resets timestamp) in-between. 247 */ 248 if (editor.framesPassed >= VBLANK_HZ*60*30) 249 audio.resetSyncTickTimeFlag = true; 250 } 251 252 void showErrorMsgBox(const char *fmt, ...) 253 { 254 char strBuf[512+1]; 255 va_list args; 256 257 // format the text string 258 va_start(args, fmt); 259 vsnprintf(strBuf, sizeof (strBuf)-1, fmt, args); 260 va_end(args); 261 262 // SDL message boxes can be very buggy on Windows XP, use MessageBoxA() instead 263 #ifdef _WIN32 264 MessageBoxA(NULL, strBuf, "Error", MB_OK | MB_ICONERROR); 265 #else 266 SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", strBuf, NULL); 267 #endif 268 } 269 270 static void updateRenderSizeVars(void) 271 { 272 int32_t widthInPixels, heightInPixels; 273 SDL_DisplayMode dm; 274 275 int32_t di = SDL_GetWindowDisplayIndex(video.window); 276 if (di < 0) 277 di = 0; // return display index 0 (default) on error 278 279 SDL_GetDesktopDisplayMode(di, &dm); 280 video.displayW = dm.w; 281 video.displayH = dm.h; 282 283 SDL_GetWindowSize(video.window, &video.windowW, &video.windowH); 284 video.renderX = 0; 285 video.renderY = 0; 286 287 video.useCustomRenderRect = false; 288 289 if (video.fullscreen) 290 { 291 if (config.specialFlags2 & STRETCH_IMAGE) 292 { 293 // "streched out" windowed fullscreen 294 295 video.renderW = video.windowW; 296 video.renderH = video.windowH; 297 298 // get DPI zoom factors (Macs with Retina, etc... returns 1.0 if no zoom) 299 SDL_GL_GetDrawableSize(video.window, &widthInPixels, &heightInPixels); 300 video.dDpiZoomFactorX = (double)widthInPixels / video.windowW; 301 video.dDpiZoomFactorY = (double)heightInPixels / video.windowH; 302 } 303 else 304 { 305 // centered windowed fullscreen, with pixel-perfect integer upscaling 306 307 const int32_t maxUpscaleFactor = MIN(video.windowW / SCREEN_W, video.windowH / SCREEN_H); 308 video.renderW = SCREEN_W * maxUpscaleFactor; 309 video.renderH = SCREEN_H * maxUpscaleFactor; 310 video.renderX = (video.windowW - video.renderW) / 2; 311 video.renderY = (video.windowH - video.renderH) / 2; 312 313 // get DPI zoom factors (Macs with Retina, etc... returns 1.0 if no zoom) 314 SDL_GL_GetDrawableSize(video.window, &widthInPixels, &heightInPixels); 315 video.dDpiZoomFactorX = (double)widthInPixels / video.windowW; 316 video.dDpiZoomFactorY = (double)heightInPixels / video.windowH; 317 318 video.renderRect.x = (int32_t)floor(video.renderX * video.dDpiZoomFactorX); 319 video.renderRect.y = (int32_t)floor(video.renderY * video.dDpiZoomFactorY); 320 video.renderRect.w = (int32_t)floor(video.renderW * video.dDpiZoomFactorX); 321 video.renderRect.h = (int32_t)floor(video.renderH * video.dDpiZoomFactorY); 322 video.useCustomRenderRect = true; // use the destination coordinates above in SDL_RenderCopy() 323 } 324 } 325 else 326 { 327 // windowed mode 328 329 SDL_GetWindowSize(video.window, &video.renderW, &video.renderH); 330 331 // get DPI zoom factors (Macs with Retina, etc... returns 1.0 if no zoom) 332 SDL_GL_GetDrawableSize(video.window, &widthInPixels, &heightInPixels); 333 video.dDpiZoomFactorX = (double)widthInPixels / video.windowW; 334 video.dDpiZoomFactorY = (double)heightInPixels / video.windowH; 335 } 336 337 // "hardware mouse" calculations 338 video.mouseCursorUpscaleFactor = MIN(video.renderW / SCREEN_W, video.renderH / SCREEN_H); 339 createMouseCursors(); 340 } 341 342 void enterFullscreen(void) 343 { 344 SDL_SetWindowFullscreen(video.window, SDL_WINDOW_FULLSCREEN_DESKTOP); 345 SDL_Delay(15); // fixes possible issues 346 347 updateRenderSizeVars(); 348 updateMouseScaling(); 349 setMousePosToCenter(); 350 } 351 352 void leaveFullscreen(void) 353 { 354 SDL_SetWindowFullscreen(video.window, 0); 355 SDL_Delay(15); // fixes possible issues 356 357 setWindowSizeFromConfig(false); // false = do not change actual window size, only update variables 358 SDL_SetWindowSize(video.window, SCREEN_W * video.windowModeUpscaleFactor, SCREEN_H * video.windowModeUpscaleFactor); 359 360 updateRenderSizeVars(); 361 updateMouseScaling(); 362 setMousePosToCenter(); 363 364 #ifdef __unix__ // can be required on Linux... (or else the window keeps moving down every time you leave fullscreen) 365 SDL_SetWindowPosition(video.window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED); 366 #endif 367 } 368 369 void toggleFullscreen(void) 370 { 371 video.fullscreen ^= 1; 372 373 if (video.fullscreen) 374 enterFullscreen(); 375 else 376 leaveFullscreen(); 377 } 378 379 bool setupSprites(void) 380 { 381 sprite_t *s; 382 383 memset(sprites, 0, sizeof (sprites)); 384 385 // hide sprites 386 s = sprites; 387 for (int32_t i = 0; i < SPRITE_NUM; i++, s++) 388 s->x = s->y = INT16_MAX; 389 390 s = &sprites[SPRITE_MOUSE_POINTER]; 391 s->data = bmp.mouseCursors; 392 s->w = MOUSE_CURSOR_W; 393 s->h = MOUSE_CURSOR_H; 394 395 s = &sprites[SPRITE_LEFT_LOOP_PIN]; 396 s->data = &bmp.loopPins[0*(154*16)]; 397 s->w = 16; 398 s->h = SAMPLE_AREA_HEIGHT; 399 400 s = &sprites[SPRITE_RIGHT_LOOP_PIN]; 401 s->data = &bmp.loopPins[2*(154*16)]; 402 s->w = 16; 403 s->h = SAMPLE_AREA_HEIGHT; 404 405 s = &sprites[SPRITE_TEXT_CURSOR]; 406 s->data = textCursorData; 407 s->w = 1; 408 s->h = 12; 409 410 hideSprite(SPRITE_MOUSE_POINTER); 411 hideSprite(SPRITE_LEFT_LOOP_PIN); 412 hideSprite(SPRITE_RIGHT_LOOP_PIN); 413 hideSprite(SPRITE_TEXT_CURSOR); 414 415 // setup refresh buffer (used to clear sprites after each frame) 416 s = sprites; 417 for (uint32_t i = 0; i < SPRITE_NUM; i++, s++) 418 { 419 s->refreshBuffer = (uint32_t *)malloc(s->w * s->h * sizeof (int32_t)); 420 if (s->refreshBuffer == NULL) 421 return false; 422 } 423 424 return true; 425 } 426 427 void changeSpriteData(int32_t sprite, const uint8_t *data) 428 { 429 sprites[sprite].data = data; 430 memset(sprites[sprite].refreshBuffer, 0, sprites[sprite].w * sprites[sprite].h * sizeof (int32_t)); 431 } 432 433 void freeSprites(void) 434 { 435 sprite_t *s = sprites; 436 for (int32_t i = 0; i < SPRITE_NUM; i++, s++) 437 { 438 if (s->refreshBuffer != NULL) 439 { 440 free(s->refreshBuffer); 441 s->refreshBuffer = NULL; 442 } 443 } 444 } 445 446 void setLeftLoopPinState(bool clicked) 447 { 448 changeSpriteData(SPRITE_LEFT_LOOP_PIN, clicked ? &bmp.loopPins[1*(154*16)] : &bmp.loopPins[0*(154*16)]); 449 } 450 451 void setRightLoopPinState(bool clicked) 452 { 453 changeSpriteData(SPRITE_RIGHT_LOOP_PIN, clicked ? &bmp.loopPins[3*(154*16)] : &bmp.loopPins[2*(154*16)]); 454 } 455 456 int32_t getSpritePosX(int32_t sprite) 457 { 458 return sprites[sprite].x; 459 } 460 461 void setSpritePos(int32_t sprite, int32_t x, int32_t y) 462 { 463 sprites[sprite].newX = (int16_t)x; 464 sprites[sprite].newY = (int16_t)y; 465 } 466 467 void hideSprite(int32_t sprite) 468 { 469 sprites[sprite].newX = SCREEN_W; 470 } 471 472 void eraseSprites(void) 473 { 474 sprite_t *s = &sprites[SPRITE_NUM-1]; 475 for (int32_t i = SPRITE_NUM-1; i >= 0; i--, s--) // erasing must be done in reverse order 476 { 477 if (s->x >= SCREEN_W || s->y >= SCREEN_H) // sprite is hidden, don't draw nor fill clear buffer 478 continue; 479 480 assert(s->refreshBuffer != NULL); 481 482 int32_t sw = s->w; 483 int32_t sh = s->h; 484 int32_t sx = s->x; 485 int32_t sy = s->y; 486 487 // if x is negative, adjust variables 488 if (sx < 0) 489 { 490 sw += sx; // subtraction 491 sx = 0; 492 } 493 494 // if y is negative, adjust variables 495 if (sy < 0) 496 { 497 sh += sy; // subtraction 498 sy = 0; 499 } 500 501 const uint32_t *src32 = s->refreshBuffer; 502 uint32_t *dst32 = &video.frameBuffer[(sy * SCREEN_W) + sx]; 503 504 // handle x/y clipping 505 if (sx+sw >= SCREEN_W) sw = SCREEN_W - sx; 506 if (sy+sh >= SCREEN_H) sh = SCREEN_H - sy; 507 508 const int32_t srcPitch = s->w - sw; 509 const int32_t dstPitch = SCREEN_W - sw; 510 511 for (int32_t y = 0; y < sh; y++) 512 { 513 for (int32_t x = 0; x < sw; x++) 514 *dst32++ = *src32++; 515 516 src32 += srcPitch; 517 dst32 += dstPitch; 518 } 519 } 520 } 521 522 void renderSprites(void) 523 { 524 sprite_t *s = sprites; 525 for (int32_t i = 0; i < SPRITE_NUM; i++, s++) 526 { 527 if (i == SPRITE_LEFT_LOOP_PIN || i == SPRITE_RIGHT_LOOP_PIN) 528 continue; // these need special drawing (done elsewhere) 529 530 // don't render the text edit cursor if window is inactive 531 if (i == SPRITE_TEXT_CURSOR) 532 { 533 assert(video.window != NULL); 534 const uint32_t windowFlags = SDL_GetWindowFlags(video.window); 535 if (!(windowFlags & SDL_WINDOW_INPUT_FOCUS)) 536 continue; 537 } 538 539 // set new sprite position 540 s->x = s->newX; 541 s->y = s->newY; 542 543 if (s->x >= SCREEN_W || s->y >= SCREEN_H) // sprite is hidden, don't draw nor fill clear buffer 544 continue; 545 546 assert(s->data != NULL && s->refreshBuffer != NULL); 547 548 int32_t sw = s->w; 549 int32_t sh = s->h; 550 int32_t sx = s->x; 551 int32_t sy = s->y; 552 const uint8_t *src8 = s->data; 553 554 // if x is negative, adjust variables 555 if (sx < 0) 556 { 557 sw += sx; // subtraction 558 src8 -= sx; // addition 559 sx = 0; 560 } 561 562 // if y is negative, adjust variables 563 if (sy < 0) 564 { 565 sh += sy; // subtraction 566 src8 += (-sy * s->w); // addition 567 sy = 0; 568 } 569 570 if (sw <= 0 || sh <= 0) // sprite is hidden, don't draw nor fill clear buffer 571 continue; 572 573 uint32_t *dst32 = &video.frameBuffer[(sy * SCREEN_W) + sx]; 574 uint32_t *clr32 = s->refreshBuffer; 575 576 // handle x/y clipping 577 if (sx+sw >= SCREEN_W) sw = SCREEN_W - sx; 578 if (sy+sh >= SCREEN_H) sh = SCREEN_H - sy; 579 580 const int32_t srcPitch = s->w - sw; 581 const int32_t dstPitch = SCREEN_W - sw; 582 583 if (mouse.mouseOverTextBox && i == SPRITE_MOUSE_POINTER) 584 { 585 // text edit mouse pointer (has color changing depending on content under it) 586 for (int32_t y = 0; y < sh; y++) 587 { 588 for (int32_t x = 0; x < sw; x++) 589 { 590 *clr32++ = *dst32; // fill clear buffer 591 592 if (*src8 != PAL_TRANSPR) 593 { 594 if (!(*dst32 & 0xFFFFFF) || *dst32 == video.palette[PAL_TEXTMRK]) 595 *dst32 = 0xB3DBF6; 596 else 597 *dst32 = 0x004ECE; 598 } 599 600 dst32++; 601 src8++; 602 } 603 604 clr32 += srcPitch; 605 src8 += srcPitch; 606 dst32 += dstPitch; 607 } 608 } 609 else 610 { 611 // normal sprites 612 for (int32_t y = 0; y < sh; y++) 613 { 614 for (int32_t x = 0; x < sw; x++) 615 { 616 *clr32++ = *dst32; // fill clear buffer 617 618 if (*src8 != PAL_TRANSPR) 619 { 620 assert(*src8 < PAL_NUM); 621 *dst32 = video.palette[*src8]; 622 } 623 624 dst32++; 625 src8++; 626 } 627 628 clr32 += srcPitch; 629 src8 += srcPitch; 630 dst32 += dstPitch; 631 } 632 } 633 } 634 } 635 636 void renderLoopPins(void) 637 { 638 const uint8_t *src8; 639 int32_t sx, x, y, sw, sh, srcPitch, dstPitch; 640 uint32_t *clr32, *dst32; 641 642 // left loop pin 643 644 sprite_t *s = &sprites[SPRITE_LEFT_LOOP_PIN]; 645 assert(s->data != NULL && s->refreshBuffer != NULL); 646 647 // set new sprite position 648 s->x = s->newX; 649 s->y = s->newY; 650 651 if (s->x < SCREEN_W) // loop pin shown? 652 { 653 sw = s->w; 654 sh = s->h; 655 sx = s->x; 656 657 src8 = s->data; 658 clr32 = s->refreshBuffer; 659 660 // if x is negative, adjust variables 661 if (sx < 0) 662 { 663 sw += sx; // subtraction 664 src8 -= sx; // addition 665 sx = 0; 666 } 667 668 dst32 = &video.frameBuffer[(s->y * SCREEN_W) + sx]; 669 670 // handle x clipping 671 if (sx+sw >= SCREEN_W) sw = SCREEN_W - sx; 672 673 srcPitch = s->w - sw; 674 dstPitch = SCREEN_W - sw; 675 676 for (y = 0; y < sh; y++) 677 { 678 for (x = 0; x < sw; x++) 679 { 680 *clr32++ = *dst32; // fill clear buffer 681 682 if (*src8 != PAL_TRANSPR) 683 { 684 assert(*src8 < PAL_NUM); 685 *dst32 = video.palette[*src8]; 686 } 687 688 dst32++; 689 src8++; 690 } 691 692 src8 += srcPitch; 693 clr32 += srcPitch; 694 dst32 += dstPitch; 695 } 696 } 697 698 // right loop pin 699 700 s = &sprites[SPRITE_RIGHT_LOOP_PIN]; 701 assert(s->data != NULL && s->refreshBuffer != NULL); 702 703 // set new sprite position 704 s->x = s->newX; 705 s->y = s->newY; 706 707 if (s->x < SCREEN_W) // loop pin shown? 708 { 709 s->x = s->newX; 710 s->y = s->newY; 711 712 sw = s->w; 713 sh = s->h; 714 sx = s->x; 715 716 src8 = s->data; 717 clr32 = s->refreshBuffer; 718 719 // if x is negative, adjust variables 720 if (sx < 0) 721 { 722 sw += sx; // subtraction 723 src8 -= sx; // addition 724 sx = 0; 725 } 726 727 dst32 = &video.frameBuffer[(s->y * SCREEN_W) + sx]; 728 729 // handle x clipping 730 if (sx+sw >= SCREEN_W) sw = SCREEN_W - sx; 731 732 srcPitch = s->w - sw; 733 dstPitch = SCREEN_W - sw; 734 735 for (y = 0; y < sh; y++) 736 { 737 for (x = 0; x < sw; x++) 738 { 739 *clr32++ = *dst32; 740 741 if (*src8 != PAL_TRANSPR) 742 { 743 assert(*src8 < PAL_NUM); 744 if (y < 9 && *src8 == PAL_LOOPPIN) 745 { 746 // don't draw marker line on top of left loop pin's thumb graphics 747 const uint8_t pal = *dst32 >> 24; 748 if (pal != PAL_DESKTOP && pal != PAL_DSKTOP1 && pal != PAL_DSKTOP2) 749 *dst32 = video.palette[*src8]; 750 } 751 else 752 { 753 *dst32 = video.palette[*src8]; 754 } 755 } 756 757 dst32++; 758 src8++; 759 } 760 761 src8 += srcPitch; 762 clr32 += srcPitch; 763 dst32 += dstPitch; 764 } 765 } 766 } 767 768 void closeVideo(void) 769 { 770 if (video.texture != NULL) 771 { 772 SDL_DestroyTexture(video.texture); 773 video.texture = NULL; 774 } 775 776 if (video.renderer != NULL) 777 { 778 SDL_DestroyRenderer(video.renderer); 779 video.renderer = NULL; 780 } 781 782 if (video.window != NULL) 783 { 784 SDL_DestroyWindow(video.window); 785 video.window = NULL; 786 } 787 788 if (video.frameBuffer != NULL) 789 { 790 free(video.frameBuffer); 791 video.frameBuffer = NULL; 792 } 793 } 794 795 void setWindowSizeFromConfig(bool updateRenderer) 796 { 797 #define MAX_UPSCALE_FACTOR 16 // 10112x6400 - ought to be good enough for many years to come 798 799 uint8_t i; 800 SDL_DisplayMode dm; 801 802 uint8_t oldUpscaleFactor = video.windowModeUpscaleFactor; 803 if (config.windowFlags & WINSIZE_AUTO) 804 { 805 int32_t di = SDL_GetWindowDisplayIndex(video.window); 806 if (di < 0) 807 di = 0; // return display index 0 (default) on error 808 809 // find out which upscaling factor is the biggest to fit on screen 810 if (SDL_GetDesktopDisplayMode(di, &dm) == 0) 811 { 812 for (i = MAX_UPSCALE_FACTOR; i >= 1; i--) 813 { 814 // height test is slightly taller because of window title, window borders and taskbar/menu/dock 815 if (dm.w >= SCREEN_W*i && dm.h >= (SCREEN_H+64)*i) 816 { 817 video.windowModeUpscaleFactor = i; 818 break; 819 } 820 } 821 822 if (i == 0) 823 video.windowModeUpscaleFactor = 1; // 1x is not going to fit, but use 1x anyways... 824 } 825 else 826 { 827 // couldn't get screen resolution, set to 1x 828 video.windowModeUpscaleFactor = 1; 829 } 830 } 831 else if (config.windowFlags & WINSIZE_1X) video.windowModeUpscaleFactor = 1; 832 else if (config.windowFlags & WINSIZE_2X) video.windowModeUpscaleFactor = 2; 833 else if (config.windowFlags & WINSIZE_3X) video.windowModeUpscaleFactor = 3; 834 else if (config.windowFlags & WINSIZE_4X) video.windowModeUpscaleFactor = 4; 835 836 if (updateRenderer) 837 { 838 SDL_SetWindowSize(video.window, SCREEN_W * video.windowModeUpscaleFactor, SCREEN_H * video.windowModeUpscaleFactor); 839 840 if (oldUpscaleFactor != video.windowModeUpscaleFactor) 841 SDL_SetWindowPosition(video.window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED); 842 843 updateRenderSizeVars(); 844 updateMouseScaling(); 845 setMousePosToCenter(); 846 } 847 } 848 849 void updateWindowTitle(bool forceUpdate) 850 { 851 if (!forceUpdate && songIsModified == song.isModified) 852 return; // window title is already set to the same 853 854 char *songTitle = getCurrSongFilename(); 855 if (songTitle != NULL) 856 { 857 char songTitleTrunc[128]; 858 strncpy(songTitleTrunc, songTitle, sizeof (songTitleTrunc)-1); 859 songTitleTrunc[sizeof (songTitleTrunc)-1] = '\0'; 860 861 if (song.isModified) 862 sprintf(wndTitle, "Fasttracker II clone v%s - \"%s\" (unsaved)", PROG_VER_STR, songTitleTrunc); 863 else 864 sprintf(wndTitle, "Fasttracker II clone v%s - \"%s\"", PROG_VER_STR, songTitleTrunc); 865 } 866 else 867 { 868 if (song.isModified) 869 sprintf(wndTitle, "Fasttracker II clone v%s - \"untitled\" (unsaved)", PROG_VER_STR); 870 else 871 sprintf(wndTitle, "Fasttracker II clone v%s - \"untitled\"", PROG_VER_STR); 872 } 873 874 SDL_SetWindowTitle(video.window, wndTitle); 875 songIsModified = song.isModified; 876 } 877 878 bool recreateTexture(void) 879 { 880 if (video.texture != NULL) 881 { 882 SDL_DestroyTexture(video.texture); 883 video.texture = NULL; 884 } 885 886 if (config.windowFlags & PIXEL_FILTER) 887 SDL_SetHint("SDL_RENDER_SCALE_QUALITY", "best"); 888 else 889 SDL_SetHint("SDL_RENDER_SCALE_QUALITY", "nearest"); 890 891 video.texture = SDL_CreateTexture(video.renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, SCREEN_W, SCREEN_H); 892 if (video.texture == NULL) 893 { 894 showErrorMsgBox("Couldn't create a %dx%d GPU texture:\n\"%s\"\n\nIs your GPU (+ driver) too old?", SCREEN_W, SCREEN_H, SDL_GetError()); 895 return false; 896 } 897 898 SDL_SetTextureBlendMode(video.texture, SDL_BLENDMODE_NONE); 899 return true; 900 } 901 902 bool setupWindow(void) 903 { 904 SDL_DisplayMode dm; 905 906 video.vsync60HzPresent = false; 907 908 uint32_t windowFlags = SDL_WINDOW_ALLOW_HIGHDPI; 909 #if defined (__APPLE__) || defined (_WIN32) // yet another quirk! 910 windowFlags |= SDL_WINDOW_HIDDEN; 911 #endif 912 913 setWindowSizeFromConfig(false); 914 915 int32_t di = SDL_GetWindowDisplayIndex(video.window); 916 if (di < 0) 917 di = 0; // return display index 0 (default) on error 918 919 SDL_GetDesktopDisplayMode(di, &dm); 920 video.dMonitorRefreshRate = (double)dm.refresh_rate; 921 922 if (dm.refresh_rate >= 59 && dm.refresh_rate <= 61) 923 video.vsync60HzPresent = true; 924 925 if (config.windowFlags & FORCE_VSYNC_OFF) 926 video.vsync60HzPresent = false; 927 928 video.window = SDL_CreateWindow("", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 929 SCREEN_W * video.windowModeUpscaleFactor, SCREEN_H * video.windowModeUpscaleFactor, 930 windowFlags); 931 932 if (video.window == NULL) 933 { 934 showErrorMsgBox("Couldn't create SDL window:\n%s", SDL_GetError()); 935 return false; 936 } 937 938 #ifdef __APPLE__ // for macOS we need to do this here for reasons I have forgotten 939 SDL_PumpEvents(); 940 SDL_ShowWindow(video.window); 941 #endif 942 943 updateWindowTitle(true); 944 return true; 945 } 946 947 bool setupRenderer(void) 948 { 949 uint32_t rendererFlags = 0; 950 if (video.vsync60HzPresent) 951 rendererFlags |= SDL_RENDERER_PRESENTVSYNC; 952 953 video.renderer = SDL_CreateRenderer(video.window, -1, rendererFlags); 954 if (video.renderer == NULL) 955 { 956 if (video.vsync60HzPresent) 957 { 958 // try again without vsync flag 959 video.vsync60HzPresent = false; 960 961 rendererFlags &= ~SDL_RENDERER_PRESENTVSYNC; 962 video.renderer = SDL_CreateRenderer(video.window, -1, rendererFlags); 963 } 964 965 if (video.renderer == NULL) 966 { 967 showErrorMsgBox("Couldn't create SDL renderer:\n\"%s\"\n\nIs your GPU (+ driver) too old?", 968 SDL_GetError()); 969 return false; 970 } 971 } 972 973 SDL_SetRenderDrawBlendMode(video.renderer, SDL_BLENDMODE_NONE); 974 975 if (!recreateTexture()) 976 { 977 showErrorMsgBox("Couldn't create a %dx%d GPU texture:\n\"%s\"\n\nIs your GPU (+ driver) too old?", 978 SCREEN_W, SCREEN_H, SDL_GetError()); 979 return false; 980 } 981 982 // framebuffer used by SDL (for texture) 983 video.frameBuffer = (uint32_t *)malloc(SCREEN_W * SCREEN_H * sizeof (uint32_t)); 984 if (video.frameBuffer == NULL) 985 { 986 showErrorMsgBox("Not enough memory!"); 987 return false; 988 } 989 990 if (!setupSprites()) 991 return false; 992 993 updateRenderSizeVars(); 994 updateMouseScaling(); 995 996 if (config.specialFlags2 & HARDWARE_MOUSE) 997 SDL_ShowCursor(SDL_TRUE); 998 else 999 SDL_ShowCursor(SDL_FALSE); 1000 1001 SDL_SetRenderDrawColor(video.renderer, 0, 0, 0, SDL_ALPHA_OPAQUE); 1002 return true; 1003 } 1004 1005 void handleRedrawing(void) 1006 { 1007 if (!ui.configScreenShown && !ui.helpScreenShown) 1008 { 1009 if (ui.aboutScreenShown) 1010 { 1011 renderAboutScreenFrame(); 1012 } 1013 else if (ui.nibblesShown) 1014 { 1015 if (editor.NI_Play) 1016 moveNibblesPlayers(); 1017 } 1018 else 1019 { 1020 if (ui.updatePosSections) 1021 { 1022 ui.updatePosSections = false; 1023 1024 if (!ui.diskOpShown) 1025 { 1026 drawSongLoopStart(); 1027 drawSongLength(); 1028 drawPosEdNums(editor.songPos); 1029 drawEditPattern(editor.editPattern); 1030 drawPatternLength(editor.editPattern); 1031 drawSongBPM(editor.BPM); 1032 drawSongSpeed(editor.speed); 1033 drawGlobalVol(editor.globalVolume); 1034 1035 if (!songPlaying || editor.wavIsRendering) 1036 setScrollBarPos(SB_POS_ED, editor.songPos, false); 1037 1038 // draw current mode text 1039 1040 const char *str = NULL; 1041 if (playMode == PLAYMODE_PATT) str = "> Play ptn. <"; 1042 else if (playMode == PLAYMODE_EDIT) str = "> Editing <"; 1043 else if (playMode == PLAYMODE_RECSONG) str = "> Rec. sng. <"; 1044 else if (playMode == PLAYMODE_RECPATT) str = "> Rec. ptn. <"; 1045 1046 uint16_t areaWidth = 102; 1047 uint16_t maxStrWidth = 76; // wide enough 1048 uint16_t x = 101; 1049 uint16_t y = 80; 1050 1051 if (ui.extendedPatternEditor) 1052 { 1053 y = 56; 1054 areaWidth = 443; 1055 } 1056 1057 // clear area 1058 uint16_t clrX = x + ((areaWidth - maxStrWidth) / 2); 1059 fillRect(clrX, y, maxStrWidth, FONT1_CHAR_H+1, PAL_DESKTOP); 1060 1061 // draw text (if needed) 1062 if (str != NULL) 1063 { 1064 x += (areaWidth - textWidth(str)) / 2; 1065 textOut(x, y, PAL_FORGRND, str); 1066 } 1067 } 1068 } 1069 1070 if (ui.updatePosEdScrollBar) 1071 { 1072 ui.updatePosEdScrollBar = false; 1073 setScrollBarPos(SB_POS_ED, song.songPos, false); 1074 setScrollBarEnd(SB_POS_ED, (song.songLength - 1) + 5); 1075 } 1076 1077 if (!ui.diskOpShown) 1078 drawPlaybackTime(); 1079 1080 if (!ui.extendedPatternEditor) 1081 { 1082 if (ui.sampleEditorExtShown) 1083 handleSampleEditorExtRedrawing(); 1084 else if (ui.scopesShown) 1085 drawScopes(); 1086 } 1087 } 1088 } 1089 1090 drawReplayerData(); 1091 1092 if (ui.instEditorShown) 1093 handleInstEditorRedrawing(); 1094 else if (ui.sampleEditorShown) 1095 handleSamplerRedrawing(); 1096 1097 // blink text edit cursor 1098 if (editor.editTextFlag && mouse.lastEditBox != -1) 1099 { 1100 assert(mouse.lastEditBox >= 0 && mouse.lastEditBox < NUM_TEXTBOXES); 1101 1102 textBox_t *txt = &textBoxes[mouse.lastEditBox]; 1103 if (editor.textCursorBlinkCounter < 256/2 && !textIsMarked() && !(mouse.leftButtonPressed | mouse.rightButtonPressed)) 1104 setSpritePos(SPRITE_TEXT_CURSOR, getTextCursorX(txt), getTextCursorY(txt) - 1); // show text cursor 1105 else 1106 hideSprite(SPRITE_TEXT_CURSOR); // hide text cursor 1107 1108 editor.textCursorBlinkCounter += TEXT_CURSOR_BLINK_RATE; 1109 } 1110 1111 if (editor.busy) 1112 animateBusyMouse(); 1113 1114 renderLoopPins(); 1115 } 1116 1117 static void drawReplayerData(void) 1118 { 1119 if (songPlaying) 1120 { 1121 if (ui.drawReplayerPianoFlag) 1122 { 1123 ui.drawReplayerPianoFlag = false; 1124 if (ui.instEditorShown && chSyncEntry != NULL) 1125 drawPiano(chSyncEntry); 1126 } 1127 1128 bool drawPosText = true; 1129 if (ui.configScreenShown || ui.nibblesShown || 1130 ui.helpScreenShown || ui.aboutScreenShown || 1131 ui.diskOpShown) 1132 { 1133 drawPosText = false; 1134 } 1135 1136 if (drawPosText) 1137 { 1138 if (ui.drawBPMFlag) 1139 { 1140 ui.drawBPMFlag = false; 1141 drawSongBPM(editor.BPM); 1142 } 1143 1144 if (ui.drawSpeedFlag) 1145 { 1146 ui.drawSpeedFlag = false; 1147 drawSongSpeed(editor.speed); 1148 } 1149 1150 if (ui.drawGlobVolFlag) 1151 { 1152 ui.drawGlobVolFlag = false; 1153 drawGlobalVol(editor.globalVolume); 1154 } 1155 1156 if (ui.drawPosEdFlag) 1157 { 1158 ui.drawPosEdFlag = false; 1159 drawPosEdNums(editor.songPos); 1160 setScrollBarPos(SB_POS_ED, editor.songPos, false); 1161 } 1162 1163 if (ui.drawPattNumLenFlag) 1164 { 1165 ui.drawPattNumLenFlag = false; 1166 drawEditPattern(editor.editPattern); 1167 drawPatternLength(editor.editPattern); 1168 } 1169 } 1170 } 1171 else if (ui.instEditorShown) 1172 { 1173 drawPiano(NULL); 1174 } 1175 1176 // handle pattern data updates 1177 if (ui.updatePatternEditor) 1178 { 1179 ui.updatePatternEditor = false; 1180 if (ui.patternEditorShown) 1181 writePattern(editor.row, editor.editPattern); 1182 } 1183 }