-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathA2VideoManager.cpp
More file actions
1995 lines (1814 loc) · 76.2 KB
/
A2VideoManager.cpp
File metadata and controls
1995 lines (1814 loc) · 76.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#include "A2VideoManager.h"
#include <cstring>
#include <zlib.h>
#include <iostream>
#include <fstream>
#include <sstream>
#include <cstring> // for std::memcmp
#include <string>
#include <algorithm>
#include <system_error>
#include <map>
#include "SDL.h"
#include <SDL_opengl.h>
#ifdef _DEBUGTIMINGS
#include <chrono>
#endif
#include <iostream>
#include <iomanip>
#include "OpenGLHelper.h"
#include "MemoryManager.h"
#include "extras/MemoryLoader.h"
#include "SoundManager.h"
#include "MockingboardManager.h"
#include "LogTextManager.h"
#include "EventRecorder.h"
#include "GRAddr2XY.h"
#include "imgui.h"
#include "SDL_rect.h"
#include "BasicQuad.h"
// In main.cpp
extern uint32_t Main_GetFPSLimit();
// Aspect constraint callback to enforce a fixed aspect ratio for ImGui windows.
// The aspect ratio is provided via the UserData pointer
static inline void AspectConstraintCallback(ImGuiSizeCallbackData* data)
{
float aspect_ratio = *(float*)data->UserData;
data->DesiredSize.y = data->DesiredSize.x / aspect_ratio;
}
// from AppleWin
static glm::vec4 borderPaletteRGB[] =
{
glm::vec4(0.0f, 0.0f, 0.0f, 1.0f), //(0x00,0x00,0x00) BLACK,
glm::vec4(0.67451f, 0.07059f, 0.29804f, 1.0f), //(0xAC,0x12,0x4C) DEEP_RED,
glm::vec4(0.0f, 0.02745f, 0.51373f, 1.0f), //(0x00,0x07,0x83) DARK_BLUE,
glm::vec4(0.66667f, 0.10196f, 0.81961f, 1.0f), //(0xAA,0x1A,0xD1) MAGENTA,
glm::vec4(0.0f, 0.51373f, 0.18431f, 1.0f), //(0x00,0x83,0x2F) DARK_GREEN,
glm::vec4(0.62353f, 0.59216f, 0.49412f, 1.0f), //(0x9F,0x97,0x7E) DARK_GRAY,
glm::vec4(0.0f, 0.54118f, 0.70980f, 1.0f), //(0x00,0x8A,0xB5) BLUE,
glm::vec4(0.62353f, 0.61961f, 1.0f, 1.0f), //(0x9F,0x9E,0xFF) LIGHT_BLUE,
glm::vec4(0.47843f, 0.37255f, 0.0f, 1.0f), //(0x7A,0x5F,0x00) BROWN,
glm::vec4(1.0f, 0.44706f, 0.27843f, 1.0f), //(0xFF,0x72,0x47) ORANGE,
glm::vec4(0.47059f, 0.40784f, 0.49804f, 1.0f), //(0x78,0x68,0x7F) LIGHT_GRAY,
glm::vec4(1.0f, 0.47843f, 0.81176f, 1.0f), //(0xFF,0x7A,0xCF) PINK,
glm::vec4(0.43529f, 0.90196f, 0.17255f, 1.0f), //(0x6F,0xE6,0x2C) GREEN,
glm::vec4(1.0f, 0.96471f, 0.48235f, 1.0f), //(0xFF,0xF6,0x7B) YELLOW,
glm::vec4(0.42353f, 0.93333f, 0.69804f, 1.0f), //(0x6C,0xEE,0xB2) AQUA,
glm::vec4(1.0f, 1.0f, 1.0f, 1.0f), //(0xFF,0xFF,0xFF) WHITE,
};
// Memory offsets for TEXT/D/LGR and D/HGR modes
// The rows aren't contiguous in Apple 2 RAM.
// They're interlaced because WOZ chip optimization.
// Apple 2 TEXT row offset interlacing in RAM
static uint16_t g_RAM_TEXTOffsets[] =
{
0x0000, 0x0080, 0x0100, 0x0180, 0x0200, 0x0280, 0x0300, 0x0380,
0x0028, 0x00A8, 0x0128, 0x01A8, 0x0228, 0x02A8, 0x0328, 0x03A8,
0x0050, 0x00D0, 0x0150, 0x01D0, 0x0250, 0x02D0, 0x0350, 0x03D0
};
// Apple 2 HGR row offsets interlacing in RAM
static uint16_t g_RAM_HGROffsets[] = {
0x0000, 0x0400, 0x0800, 0x0C00, 0x1000, 0x1400, 0x1800, 0x1C00,
0x0080, 0x0480, 0x0880, 0x0C80, 0x1080, 0x1480, 0x1880, 0x1C80,
0x0100, 0x0500, 0x0900, 0x0D00, 0x1100, 0x1500, 0x1900, 0x1D00,
0x0180, 0x0580, 0x0980, 0x0D80, 0x1180, 0x1580, 0x1980, 0x1D80,
0x0200, 0x0600, 0x0A00, 0x0E00, 0x1200, 0x1600, 0x1A00, 0x1E00,
0x0280, 0x0680, 0x0A80, 0x0E80, 0x1280, 0x1680, 0x1A80, 0x1E80,
0x0300, 0x0700, 0x0B00, 0x0F00, 0x1300, 0x1700, 0x1B00, 0x1F00,
0x0380, 0x0780, 0x0B80, 0x0F80, 0x1380, 0x1780, 0x1B80, 0x1F80,
0x0028, 0x0428, 0x0828, 0x0C28, 0x1028, 0x1428, 0x1828, 0x1C28,
0x00A8, 0x04A8, 0x08A8, 0x0CA8, 0x10A8, 0x14A8, 0x18A8, 0x1CA8,
0x0128, 0x0528, 0x0928, 0x0D28, 0x1128, 0x1528, 0x1928, 0x1D28,
0x01A8, 0x05A8, 0x09A8, 0x0DA8, 0x11A8, 0x15A8, 0x19A8, 0x1DA8,
0x0228, 0x0628, 0x0A28, 0x0E28, 0x1228, 0x1628, 0x1A28, 0x1E28,
0x02A8, 0x06A8, 0x0AA8, 0x0EA8, 0x12A8, 0x16A8, 0x1AA8, 0x1EA8,
0x0328, 0x0728, 0x0B28, 0x0F28, 0x1328, 0x1728, 0x1B28, 0x1F28,
0x03A8, 0x07A8, 0x0BA8, 0x0FA8, 0x13A8, 0x17A8, 0x1BA8, 0x1FA8,
0x0050, 0x0450, 0x0850, 0x0C50, 0x1050, 0x1450, 0x1850, 0x1C50,
0x00D0, 0x04D0, 0x08D0, 0x0CD0, 0x10D0, 0x14D0, 0x18D0, 0x1CD0,
0x0150, 0x0550, 0x0950, 0x0D50, 0x1150, 0x1550, 0x1950, 0x1D50,
0x01D0, 0x05D0, 0x09D0, 0x0DD0, 0x11D0, 0x15D0, 0x19D0, 0x1DD0,
0x0250, 0x0650, 0x0A50, 0x0E50, 0x1250, 0x1650, 0x1A50, 0x1E50,
0x02D0, 0x06D0, 0x0AD0, 0x0ED0, 0x12D0, 0x16D0, 0x1AD0, 0x1ED0,
0x0350, 0x0750, 0x0B50, 0x0F50, 0x1350, 0x1750, 0x1B50, 0x1F50,
0x03D0, 0x07D0, 0x0BD0, 0x0FD0, 0x13D0, 0x17D0, 0x1BD0, 0x1FD0
};
static std::string fontpath = "assets/Apple2eFont14x16";
// below because "The declaration of a static data member in its class definition is not a definition"
A2VideoManager* A2VideoManager::s_instance;
//////////////////////////////////////////////////////////////////////////
// Overlay String Methods
//////////////////////////////////////////////////////////////////////////
void A2VideoManager::DrawOverlayString(const std::string& text, uint8_t colors, uint32_t x, uint32_t y)
{
for (uint8_t i = 0; i < text.length(); ++i)
{
overlay_text[_OVERLAY_CHAR_WIDTH * y + x + i] = text.c_str()[i] + 0x80;
overlay_colors[_OVERLAY_CHAR_WIDTH * y + x + i] = colors;
}
overlay_lines[y] = 1;
}
void A2VideoManager::DrawOverlayString(const char* text, uint8_t len, uint8_t colors, uint32_t x, uint32_t y)
{
(void)len;
uint8_t i = 0;
while (text[i] != '\0')
{
overlay_text[_OVERLAY_CHAR_WIDTH * y + x + i] = text[i] + 0x80;
overlay_colors[_OVERLAY_CHAR_WIDTH * y+ + x + i] = colors;
++i;
}
overlay_lines[y] = 1;
}
void A2VideoManager::DrawOverlayCharacter(const char c, uint8_t colors, uint32_t x, uint32_t y)
{
overlay_text[_OVERLAY_CHAR_WIDTH * y + x] = c + 0x80;
overlay_colors[_OVERLAY_CHAR_WIDTH * y + x] = colors;
overlay_lines[y] = 1;
}
void A2VideoManager::EraseOverlayRange(uint8_t len, uint32_t x, uint32_t y)
{
memset(overlay_text + (_OVERLAY_CHAR_WIDTH * y + x), 0, len);
UpdateOverlayLine(y);
}
void A2VideoManager::EraseOverlayCharacter(uint32_t x, uint32_t y)
{
overlay_text[_OVERLAY_CHAR_WIDTH * y + x] = 0;
UpdateOverlayLine(y);
}
inline void A2VideoManager::UpdateOverlayLine(uint32_t y)
{
for (uint8_t i=0; i< _OVERLAY_CHAR_WIDTH; ++i) {
if (overlay_text[_OVERLAY_CHAR_WIDTH * y + i] > 0)
{
overlay_lines[y] = 1;
return;
}
}
overlay_lines[y] = 0;
}
//////////////////////////////////////////////////////////////////////////
// Manager Methods
//////////////////////////////////////////////////////////////////////////
A2VideoManager::~A2VideoManager()
{
for (int i = 0; i < 2; i++)
{
if (vrams_array[i].vram_legacy != nullptr)
delete[] vrams_array[i].vram_legacy;
if (vrams_array[i].vram_shr != nullptr)
delete[] vrams_array[i].vram_shr;
if (vrams_array[i].vram_pal256 != nullptr)
delete[] vrams_array[i].vram_pal256;
if (vrams_array[i].offset_buffer != nullptr)
delete[] vrams_array[i].offset_buffer;
}
delete[] vrams_array;
ResetGLData();
if (APPLE2MEMORYTEX != UINT_MAX)
glDeleteTextures(1, &APPLE2MEMORYTEX);
}
void A2VideoManager::Initialize()
{
// Here do not reinitialize bBeamIsActive. It could still be active from earlier.
// The initialization process can be triggered from a ctrl-reset on the Apple 2.
bIsReady = false;
v_debug_rgb_windows.reserve(_MAX_DEBUG_RGB_WINDOWS);
border_w_slider_val = (int)this->GetBordersWidthCycles();
border_h_slider_val = (int)this->GetBordersHeightScanlines() / 8;
SDL_memset(&event_newframe, 0, sizeof(event_newframe));
event_newframe.type = SDL_USEREVENT;
event_newframe.user.code = SDLUSEREVENT_A2NEWFRAME;
event_newframe.user.data1 = nullptr;
event_newframe.user.data2 = nullptr;
ResetGLData();
auto oglHelper = OpenGLHelper::GetInstance();
for (int i = 0; i < 2; i++)
{
vrams_array[i].id = i;
vrams_array[i].frame_idx = current_frame_idx + i;
vrams_array[i].bWasRendered = true; // otherwise it won't render the first frame
vrams_array[i].mode = A2Mode_e::NONE;
if (vrams_array[i].vram_legacy != nullptr)
{
delete[] vrams_array[i].vram_legacy;
vrams_array[i].vram_legacy = nullptr;
}
if (vrams_array[i].vram_shr != nullptr)
{
delete[] vrams_array[i].vram_shr;
vrams_array[i].vram_shr = nullptr;
}
if (vrams_array[i].vram_pal256 != nullptr)
{
delete[] vrams_array[i].vram_pal256;
vrams_array[i].vram_pal256 = nullptr;
}
if (vrams_array[i].offset_buffer != nullptr)
{
delete[] vrams_array[i].offset_buffer;
vrams_array[i].offset_buffer = nullptr;
}
vrams_array[i].vram_legacy = new uint8_t[GetVramSizeLegacy()];
vrams_array[i].vram_shr = new uint8_t[GetVramSizeSHR()];
vrams_array[i].vram_pal256 = new uint8_t[_A2VIDEO_SHR_BYTES_PER_LINE*2*_A2VIDEO_SHR_SCANLINES*_INTERLACE_MULTIPLIER];
vrams_array[i].offset_buffer = new GLfloat[GetVramHeightSHR()];
memset(vrams_array[i].vram_legacy, 0, GetVramSizeLegacy());
memset(vrams_array[i].vram_shr, 0, GetVramSizeSHR());
memset(vrams_array[i].vram_pal256, 0, _A2VIDEO_SHR_BYTES_PER_LINE*2*_A2VIDEO_SHR_SCANLINES*_INTERLACE_MULTIPLIER);
memset(vrams_array[i].offset_buffer, 0, GetVramHeightSHR() * sizeof(GLfloat));
}
vrams_write = &vrams_array[0];
vrams_read = &vrams_array[1];
// Set up the image assets (textures)
// Assign them their respective GPU texture id
*image_assets = {};
for (uint8_t i = 0; i < (sizeof(image_assets) / sizeof(OpenGLHelper::ImageAsset)); i++)
{
image_assets[i].tex_id = oglHelper->get_texture_id_at_slot(i);
}
// Get the font roms
try {
font_roms_array.clear();
for (const auto & entry : std::filesystem::directory_iterator(fontpath)) {
if (entry.is_regular_file()) {
if ((entry.path().extension() == ".png") || (entry.path().extension() == ".PNG"))
font_roms_array.push_back(entry.path().filename().string());
}
}
if (font_roms_array.empty()) {
throw std::filesystem::filesystem_error(
"No Font ROM textures found!",
std::make_error_code(std::errc::no_such_file_or_directory)
);
}
std::sort(font_roms_array.begin(), font_roms_array.end());
} catch (const std::filesystem::filesystem_error& e) {
std::cerr << "Error accessing directory: " << e.what() << std::endl;
exit(1);
}
// Initialize windows
windowsbeam[A2VIDEOBEAM_LEGACY] = std::make_unique<A2WindowBeam>(A2VIDEOBEAM_LEGACY, _SHADER_A2_VERTEX_DEFAULT, _SHADER_BEAM_LEGACY_FRAGMENT);
windowsbeam[A2VIDEOBEAM_SHR] = std::make_unique<A2WindowBeam>(A2VIDEOBEAM_SHR, _SHADER_A2_VERTEX_DEFAULT, _SHADER_BEAM_SHR_FRAGMENT);
windowsbeam[A2VIDEOBEAM_LEGACY]->SetBorder(borders_w_cycles, borders_h_scanlines);
windowsbeam[A2VIDEOBEAM_SHR]->SetBorder(borders_w_cycles, borders_h_scanlines);
vidhdWindowBeam = std::make_unique<VidHdWindowBeam>(VIDHDMODE_NONE);
legacyNTSCQuad = std::make_unique<BasicQuad>(_SHADER_VERTEX_BASIC, "shaders/ntsc_sik.frag");
// The framebuffer width. That will change depending on the layers that are rendered:
// If a VidHD text modes > 80x24 is active, then 1920x1080
// Otherwise if SHR is active in any scanline, then SHR size
// Otherwise, Legacy size
fb_width = windowsbeam[A2VIDEOBEAM_LEGACY]->GetWidth();
fb_height = windowsbeam[A2VIDEOBEAM_LEGACY]->GetHeight();
beamState = BeamState_e::NBVBLANK;
merge_last_change_mode = A2Mode_e::NONE;
merge_last_change_y = UINT_MAX;
offsetTextureExists = false;
mem_edit_vram_legacy.Open = false;
mem_edit_vram_shr.Open = false;
mem_edit_offset_buffer.Open = false;
// tell the next Render() call to run initialization routines
bShouldInitializeRender = true;
// clear the text overlay
std::memset(overlay_text, 0, sizeof(overlay_text));
std::memset(overlay_colors, 0, sizeof(overlay_colors));
std::memset(overlay_lines, 0, sizeof(overlay_lines));
// Set default border color
MemoryManager::GetInstance()->switch_c034 = 13;
bIsReady = true;
}
void A2VideoManager::ResetGLData() {
if (OFFSETTEX != UINT_MAX)
glDeleteTextures(1, &OFFSETTEX);
if (FBO_A2Video != UINT_MAX)
{
glDeleteFramebuffers(1, &FBO_A2Video);
glDeleteTextures(1, &a2video_texture_id);
glDeleteFramebuffers(1, &FBO_NTSC);
glDeleteTextures(1, &ntsc_texture_id);
}
FBO_A2Video = UINT_MAX;
FBO_NTSC = UINT_MAX;
OFFSETTEX = UINT_MAX;
}
void A2VideoManager::ResetComputer()
{
if (bIsRebooting == true)
return;
bIsRebooting = true;
Initialize();
MemoryManager::GetInstance()->Initialize();
SoundManager::GetInstance()->Initialize();
MockingboardManager::GetInstance()->Initialize();
bIsRebooting = false;
}
bool A2VideoManager::IsReady()
{
return bIsReady;
}
void A2VideoManager::ToggleA2Video(bool value)
{
// If true, the A2 Video reinitializes fully
// Only call this method when reinit is necessary,
// like changing mode from SDHR to A2 Video
bA2VideoEnabled = value;
if (bA2VideoEnabled)
bShouldInitializeRender = true;
}
void A2VideoManager::CheckSetBordersWithReinit()
{
if (border_w_slider_val > _BORDER_WIDTH_MAX_CYCLES)
border_w_slider_val = _BORDER_WIDTH_MAX_CYCLES;
if (border_h_slider_val > _BORDER_WIDTH_MAX_CYCLES)
border_h_slider_val = _BORDER_WIDTH_MAX_CYCLES;
if ((border_w_slider_val == borders_w_cycles)
&& (border_h_slider_val == (borders_h_scanlines / 8)))
return;
bIsReady = false;
borders_w_cycles = border_w_slider_val;
borders_h_scanlines = border_h_slider_val * 8; // Must be multiple of 8s
auto _mms = MemoryManager::GetInstance()->SerializeSwitches();
this->Initialize();
MemoryManager::GetInstance()->DeserializeSwitches(_mms);
this->ForceBeamFullScreenRender();
}
// The input is x,y,width,height where x,y are top left origin. The output is SDL style inverted Y
// The full screen quad output would be { -1.f, 1.f, 2.f, -2.f }
SDL_FRect A2VideoManager::NormalizePixelQuad(const SDL_FRect& pixelQuad)
{
SDL_FRect normalized;
normalized.x = (2.0f * pixelQuad.x) / fb_width - 1.0f;
normalized.y = 1.0f - (2.0f * pixelQuad.y) / fb_height;
normalized.w = (2.0f * pixelQuad.w) / fb_width;
normalized.h = (-2.f * pixelQuad.h) / fb_height;
return normalized;
}
SDL_FRect A2VideoManager::CenteredQuadInFramebuffer(const SDL_FRect& quad)
{
SDL_FRect centeredQuad;
centeredQuad.w = quad.w;
centeredQuad.h = quad.h;
centeredQuad.x = (fb_width - quad.w) / 2.0f;
centeredQuad.y = (fb_height - quad.h) / 2.0f;
return centeredQuad;
}
SDL_FRect A2VideoManager::CenteredQuadInFramebufferWithOffset(const SDL_FRect& quad, const SDL_FPoint& offset)
{
auto resQuad = CenteredQuadInFramebuffer(quad);
resQuad.x += offset.x;
resQuad.y += offset.y;
return resQuad;
}
void A2VideoManager::StartNextFrame()
{
// start the next frame
// set the frame index for the buffer we'll move to reading
vrams_write->frame_idx = ++current_frame_idx;
// std::cerr << "starting next frame at current index: " << current_frame_idx << std::endl;
// Flip the double buffers only if the read buffer was rendered
// Otherwise it means the renderer is too slow and hasn't finished rendering
// We just overwrite the current buffer
if (vrams_read->bWasRendered == true)
{
vrams_read->bWasRendered = false;
auto _vtmp = vrams_write;
vrams_write = vrams_read;
vrams_read = _vtmp;
}
// memset(vrams_write->vram_legacy, 0, GetVramSizeLegacy());
// memset(vrams_write->vram_shr, 0, GetVramSizeSHR());
// memset(vrams_write->offset_buffer, 0, GetVramHeightSHR() * sizeof(GLfloat));
// At each vblank reset the mode
vrams_write->mode = A2Mode_e::NONE;
// Update the current region info
current_region = CycleCounter::GetInstance()->GetVideoRegion();
region_scanlines = (current_region == VideoRegion_e::NTSC ? SC_TOTAL_NTSC : SC_TOTAL_PAL);
// For the merged mode
merge_last_change_y = UINT_MAX;
// Additional frame data resets
vrams_write->frameSHRModes = 0;
vrams_write->pagedMode = 0;
// And finally send an event to the main loop saying that the frame was updated
// This is necessary when synching to the Apple 2 VSYNC. Don't create a new event if there are 4
// events already in the queue.
int _numEvents = SDL_PeepEvents(user_events_active, MAX_USEREVENTS_IN_QUEUE,
SDL_PEEKEVENT, SDL_USEREVENT, SDL_USEREVENT);
if (_numEvents < 4)
{
auto _everr = SDL_PushEvent(&event_newframe);
if (_everr == 0)
std::cerr << "Event queue is full, could not push event_newframe!" << std::endl;
else if (_everr < 0) {
std::cerr << "event_newframe error: " << SDL_GetError() << std::endl;
}
}
#ifdef DEBUG
else {
std::cerr << "Max # of A2 frames already in event queue!" << std::endl;
}
#endif
}
void A2VideoManager::BeamIsAtPosition(uint32_t _x, uint32_t _y)
{
/*
@: Frame flip and start of next frame
&: Start next frame scanlines
||H| |H||----------------------------------------------------------------------------|
||B| |B|| |
||o| |o|| |
||r| HBLANK |r|| Content |
||d| |d|| CYCLES_SC_CONTENT x mode_scanlines |
||e| |e|| |
||r| |r|| |
| |---------------------- Vertical border -----------------------|
| |--------------------------- (borders_h_scanlines) --------------------------|
|@ |............................. vertical blanking ............................|
| |............................. vertical blanking ............................|
| |............................. vertical blanking ............................|
| |............................. vertical blanking ............................|
| |............................. vertical blanking ............................|
| |............................. vertical blanking ............................|
| |............................. vertical blanking ............................|
|& |----------------- Vertical border (next frame) -----------------|
| |--------------------------- (borders_h_scanlines) --------------------------|
In order to achieve a "correct" top, left, right, bottom border around the content, with
the origin being at the start of the top border, we translate each border area's x & w
with the below #defines for each of Border L, R, T, B.
*/
#define _TR_ANY_X ((_x + borders_w_cycles + CYCLES_SC_CONTENT) % CYCLES_SC_TOTAL)
#define _TR_ANY_Y ((_y + borders_h_scanlines) % region_scanlines)
if (!bIsReady || bIsRebooting)
return;
auto memMgr = MemoryManager::GetInstance();
uint32_t mode_scanlines = (memMgr->IsSoftSwitch(A2SS_SHR) ? 200 : 192);
// The Apple 2gs drawing is shifted 6 scanlines down
// Let's realign it to match the 2e
if (memMgr->is2gs)
{
_y = (_y + region_scanlines - 6) % region_scanlines;
}
// Do not bother with the beam state until we get a frame start
// Then we can start doing work
if (_y == _SCANLINE_START_FRAME && _x == 0) // frame start
{
beamState = BeamState_e::NBVBLANK;
}
// Now determine the actual beam state
// And flip the frame when switching from BORDER_BOTTOM to NBVBLANK
// keep updating the beam state until it reaches steady state
auto _oldBeamState = beamState;
while (true)
{
switch (beamState)
{
case BeamState_e::UNKNOWN:
break;
case BeamState_e::NBHBLANK:
if (_x == (CYCLES_SC_HBL - borders_w_cycles))
beamState = BeamState_e::BORDER_LEFT;
break;
case BeamState_e::NBVBLANK:
// if there are no vertical borders then _y never gets to region_scanlines
// and we need to handle this special case
if (_y == (region_scanlines - borders_h_scanlines))
beamState = BeamState_e::BORDER_TOP;
else if (_y == 0 && borders_h_scanlines == 0)
beamState = BeamState_e::BORDER_RIGHT;
if (_y == _SCANLINE_START_FRAME && _x == 0)
{
// Start of NBVBLANK at which we flip the double buffering
StartNextFrame();
}
break;
case BeamState_e::BORDER_LEFT:
if (_x == CYCLES_SC_HBL)
beamState = BeamState_e::CONTENT;
break;
case BeamState_e::BORDER_RIGHT:
if (_x == borders_w_cycles)
beamState = BeamState_e::NBHBLANK;
break;
case BeamState_e::BORDER_TOP:
if (_y == 0)
beamState = BeamState_e::BORDER_RIGHT;
break;
case BeamState_e::BORDER_BOTTOM:
if (_y == (mode_scanlines + borders_h_scanlines))
{
beamState = BeamState_e::NBVBLANK;
}
break;
case BeamState_e::CONTENT:
if (_x == 0)
{
if (_y == mode_scanlines)
beamState = BeamState_e::BORDER_BOTTOM;
else
beamState = BeamState_e::BORDER_RIGHT;
}
break;
default:
break;
break;
}
if (_oldBeamState == beamState)
break;
// std::cerr << "switched " << BeamStateToString(_oldBeamState) << " --> " << BeamStateToString(beamState) << std::endl;
_oldBeamState = beamState;
}
// Check for text overlay in this position
if ((_y < COUNT_SC_CONTENT) && (overlay_lines[_y / 8] == 1))
{
if (_x == 0)
{
bWasSHRBeforeOverlay = memMgr->IsSoftSwitch(A2SS_SHR);
memMgr->SetSoftSwitch(A2SS_SHR, false);
} else if (_x == (CYCLES_SC_TOTAL - 1))
{
memMgr->SetSoftSwitch(A2SS_SHR, bWasSHRBeforeOverlay);
}
if (beamState == BeamState_e::CONTENT)
{
if (_x < CYCLES_SC_HBL || _y >= mode_scanlines) // bounds check if mode changes midway
return;
uint32_t _toff = _OVERLAY_CHAR_WIDTH * (_y/8) + (_x - CYCLES_SC_HBL);
// Override when the byte is an overlay
if (overlay_text[_toff] > 0)
{
if (vrams_write->mode == A2Mode_e::SHR)
vrams_write->mode = A2Mode_e::MERGED;
for (uint8_t ii=0; ii<8; ++ii) {
uint8_t* byteStartPtr = vrams_write->vram_legacy + (GetVramWidthLegacy() * (_TR_ANY_Y + ii) + _TR_ANY_X) * 4;
byteStartPtr[0] = overlay_text[_toff]; // main
byteStartPtr[2] = 0b1000;
byteStartPtr[3] = overlay_colors[_toff];
}
return;
}
}
}
auto vramSHRInterlaceOffset = GetVramSizeSHR() / _INTERLACE_MULTIPLIER; // Offset to 2nd half of the vram
// Always at the start of the row, set the SHR SCB to 0x10
// Because we check bit 4 of the SCB to know if that line is drawn as SHR
// The 2gs will always set bit 4 to 0 when sending it over
// Also check if the mode has switched in the middle of the frame
if (_x == 0)
{
if (_TR_ANY_Y < (_A2VIDEO_SHR_SCANLINES + 2 * borders_h_scanlines))
{
// Set both regular and interlaced areas SCBs
vrams_write->vram_shr[GetVramWidthSHR() * _TR_ANY_Y] = 0x10;
vrams_write->vram_shr[vramSHRInterlaceOffset + GetVramWidthSHR() * _TR_ANY_Y] = 0x10;
if (vrams_write->mode == A2Mode_e::MERGED)
{
// Merge mode calculations
// determine the mode switch and update merge_last_change_mode and merge_last_change_y
auto _curr_mode = (memMgr->IsSoftSwitch(A2SS_SHR) ? A2Mode_e::SHR : A2Mode_e::LEGACY);
if ((merge_last_change_mode == A2Mode_e::LEGACY) && (_curr_mode == A2Mode_e::SHR))
{
// 14 -> 16MHz
merge_last_change_y = _TR_ANY_Y;
// std::cerr << "merge to 16 " << merge_last_change_y << std::endl;
}
else if ((merge_last_change_mode == A2Mode_e::SHR) && (_curr_mode == A2Mode_e::LEGACY))
{
// 16 -> 14MHz
merge_last_change_y = _TR_ANY_Y;
// std::cerr << "merge to 14 " << merge_last_change_y << std::endl;
}
merge_last_change_mode = _curr_mode;
// Finally set the offset
// NOTE: We add 10.f to the offset so that the shader can know which mode to apply
// If it's negative, it's Legacy. Positive, SHR.
if (bNoMergedModeWobble)
{
vrams_write->offset_buffer[_TR_ANY_Y] = (_curr_mode == A2Mode_e::LEGACY ? -10.f : 10.f);
}
else
{
if (_TR_ANY_Y < merge_last_change_y // the switch happened last frame
|| (_TR_ANY_Y - merge_last_change_y) > 15) // the switch has been recovered
{
vrams_write->offset_buffer[_TR_ANY_Y] = (_curr_mode == A2Mode_e::LEGACY ? -10.f : 10.f);
}
else
{
// If the change is to 28 MHz, shift negative (left). Otherwise, shift positive (right)
float pixelShift = (GLfloat)glm::pow(glm::exp(merge_last_change_y - _TR_ANY_Y + 15), bWobblePower) - 1.0;
if (_curr_mode == A2Mode_e::LEGACY)
vrams_write->offset_buffer[_TR_ANY_Y] = -(10.f + pixelShift);
else
vrams_write->offset_buffer[_TR_ANY_Y] = 10.f + pixelShift;
}
// std::cerr << "Offset: " << vrams_write->offset_buffer[_TR_ANY_Y] << " y: " << _TR_ANY_Y << std::endl;
}
}
}
}
// Now we get rid of all the non-border BLANK areas to avoid an overflow on the vertical border areas.
// We never want to process vertical borders that are in non-border HBLANK
// Otherwise we'd need the vertical border states to know when they're in HBLANK as well,
// complicating the state machine.
if (_x >= borders_w_cycles && _x < (CYCLES_SC_HBL - borders_w_cycles))
return;
if (_y >= (mode_scanlines + borders_h_scanlines) && _y < (region_scanlines - borders_h_scanlines))
return;
// Now generate the VRAMs themselves
if (memMgr->IsSoftSwitch(A2SS_SHR))
{
// at least 1 byte in this vblank cycle is in SHR
switch (vrams_write->mode)
{
case A2Mode_e::NONE:
vrams_write->mode = A2Mode_e::SHR;
break;
case A2Mode_e::LEGACY:
SwitchToMergedMode(_y);
break;
default:
break;
}
auto bHasDoneDouble = false; // flag to ensure we don't repeat the double/interlace work
auto memPtr = memMgr->GetApple2MemAuxPtr();
uint8_t* lineStartPtr = vrams_write->vram_shr + GetVramWidthSHR() * _TR_ANY_Y;
DRAW_VRAM:
// get the SCB and palettes if we're starting a line
// and it's part of the content area. The top & bottom border areas don't care about SCB
// We may or may not have a border, so at this point the beamstate is either BORDER_LEFT or CONTENT
if ((_TR_ANY_X == 0) && (_y < mode_scanlines))
{
lineStartPtr[0] = memPtr[_A2VIDEO_SHR_SCB_START + _y];
// Get the palette (might be overwritten if it's a SHR3200 image
memcpy(lineStartPtr + 1, // palette starts at byte 1 in our a2shr_vram
memPtr + _A2VIDEO_SHR_PALETTE_START + ((uint32_t)(lineStartPtr[0] & 0xFu) * 32),
32); // palette length is 32 bytes
// Also here check all the palette reserved nibble values (high nibble of byte 2) to see
// what SHR4 modes are used in this line, if SHR4 is enabled via the magic bytes
uint32_t magicBytes = reinterpret_cast<uint32_t*>(memPtr + _A2VIDEO_SHR_MAGIC_BYTES)[0];
if ((magicBytes == _A2VIDEO_SHR4_MAGIC_STRING) || ((overrideSHRMode & A2SM_SHR4SHR) != 0)) // SHR4 mode is enabled
{
// Modes are 0,1,2,3 on the high nibble of the 2-byte palette. We need to switch to bits as per A2SHRSpecialMode_e
scanlineSHR4Modes = A2SM_SHR4SHR; // Default SHR enabled for SHR4
for (uint8_t i = 0; i < 16; ++i) {
scanlineSHR4Modes |= (1 << ((lineStartPtr[2 + 2*i] >> 4) + 4)); // second byte of each palette color (skip SCB byte 1)
// But if we're overriding the mode, let's change the palette in real time to match the overridden mode
// This way the shader (and WindowBeam) doesn't need to know anything about overrides,
// it's just given the correcly overridden data. The original modes remain in scanlineSHR4Modes so we can show that
// to the user in the UI
if ((overrideSHRMode & A2SM_SHR4SHR) != 0) {
auto _lowNibble = (lineStartPtr[2 + 2*i] & 0xF);
switch (overrideSHRMode) {
case A2SM_SHR4SHR:
lineStartPtr[2 + 2*i] = _lowNibble;
break;
case A2SM_SHR4RGGB:
lineStartPtr[2 + 2*i] = _lowNibble + (1 << 4);
break;
case A2SM_SHR4PAL256:
lineStartPtr[2 + 2*i] = _lowNibble + (2 << 4);
break;
case A2SM_SHR4R4G4B4:
lineStartPtr[2 + 2*i] = _lowNibble + (3 << 4);
break;
default:
break;
}
}
}
vrams_write->frameSHRModes |= scanlineSHR4Modes; // Add to the frame's SHR4 modes the new modes found on this line
// page mode is the first byte of the 4 control bytes which come just before the magic bytes
vrams_write->pagedMode = (memPtr + _A2VIDEO_SHR_CTRL_BYTES)[0];
} else if (
((magicBytes == _A2VIDEO_3200_MAGIC_STRING) || (overrideSHRMode == A2SM_SHR3200))
&& ((overrideSHRMode & A2SM_SHR4SHR) == 0)
) // i.e. if not overridden by any other SHR mode, and it's a 3200 image or we force it to be
{
// 3200 mode is enabled
// There's a pointer to the start of the 200 palettes (1 per line) right before the magic bytes
// The palettes could exist anywhere in memory so the developer tells us where they are
// The 4 bytes are, from most to least significant:
// - Paged Mode (00: no, 01: Interlace, 02: Pageflip) - defined in DoubleMode_e
// - Bank: 00 for Main (E0), 01 for Aux (E1)
// - Low byte of memory
// - High byte of memory
// Example: 00 00 80 22 means the palettes start at 0x2280 in main memory
uint8_t* pCtrlStart = memPtr + _A2VIDEO_SHR_CTRL_BYTES;
vrams_write->pagedMode = pCtrlStart[0]; // page mode is the first control byte
uint8_t* bankPtr = (pCtrlStart[1] == 1 ? memMgr->GetApple2MemAuxPtr() : memMgr->GetApple2MemPtr());
uint16_t palStart = (((uint16_t)pCtrlStart[3]) << 8) | pCtrlStart[2];
if (palStart < (_A2_MEMORY_SHADOW_END - 200 * 32)) {
// we may not be shadowing the whole memory
memcpy(lineStartPtr + 1, // palette starts at byte 1 in our a2shr_vram
bankPtr + palStart + (_y * 32),
32); // palette length is 32 bytes
vrams_write->frameSHRModes = A2SM_SHR3200;
}
}
// Do the SCB and palettes for interlacing if requested
if (!bHasDoneDouble)
{
bHasDoneDouble = true;
bShouldPageDouble = (overrideDoubleSHR > 0 ? 1 : (vrams_write->pagedMode > 0));
if (bShouldPageDouble)
{
// For the additional interlacing mode data, use main mem and the second part of vram_shr
memPtr = memMgr->GetApple2MemPtr(); // main mem (E0)
lineStartPtr = vrams_write->vram_shr + vramSHRInterlaceOffset + GetVramWidthSHR() * _TR_ANY_Y;
goto DRAW_VRAM;
}
}
}
// reset the pointers
memPtr = memMgr->GetApple2MemAuxPtr();
lineStartPtr = vrams_write->vram_shr + GetVramWidthSHR() * _TR_ANY_Y;
auto memInterlacePtr = memMgr->GetApple2MemPtr();
uint8_t* lineInterlaceStartPtr = vrams_write->vram_shr + vramSHRInterlaceOffset + GetVramWidthSHR() * _TR_ANY_Y;
switch (beamState)
{
case BeamState_e::UNKNOWN:
break;
case BeamState_e::NBHBLANK:
// do nothing
break;
case BeamState_e::NBVBLANK:
// do nothing
break;
case BeamState_e::BORDER_LEFT:
case BeamState_e::BORDER_RIGHT:
case BeamState_e::BORDER_TOP:
case BeamState_e::BORDER_BOTTOM:
memset(lineStartPtr + _COLORBYTESOFFSET + (_TR_ANY_X * 4), (uint8_t)memMgr->switch_c034, 4);
if (bShouldPageDouble)
memset(lineInterlaceStartPtr + _COLORBYTESOFFSET + (_TR_ANY_X * 4), (uint8_t)memMgr->switch_c034, 4);
break;
case BeamState_e::CONTENT:
{
if (_x < CYCLES_SC_HBL || _y >= mode_scanlines)
{
// Somehow in the middle of the frame the mode was switched, and we're beyond the
// legacy content area. Disregard.
break;
}
// Get the color info for the 4 bytes where the beam is
auto xfb = (_x - CYCLES_SC_HBL) * 4; // the x first byte, given that every beam cycle renders 4 bytes
auto scb = lineStartPtr[0];
memcpy(lineStartPtr + _COLORBYTESOFFSET + _TR_ANY_X * 4,
memPtr + _A2VIDEO_SHR_START + _y * _A2VIDEO_SHR_BYTES_PER_LINE + xfb, 4);
if (!(scb & 0x80u) && (scb & 0x20u)) // 320 mode and colorfill
{
// Pre-calculate colorfill, so that the shader doesn't have to do it
// It's completely wasted on the shader. Here it's much more efficient
for (uint32_t i = 0; i < 4; i++)
{
auto byteColor = lineStartPtr[_COLORBYTESOFFSET + (_TR_ANY_X * 4) + i];
// if the first color of the byte is 0, give it the last color of the previous byte
// assuming this is not the first byte of the line
if (((byteColor & 0xF0) == 0) && ((xfb + i) != 0))
byteColor |= (lineStartPtr[_COLORBYTESOFFSET + (_TR_ANY_X * 4) + i - 1] & 0b1111) << 4;
// if the second color of the byte is 0, give it the first color of the byte
if ((byteColor & 0x0F) == 0)
byteColor |= (byteColor >> 4);
lineStartPtr[_COLORBYTESOFFSET + (_TR_ANY_X * 4) + i] = byteColor;
}
}
// Here deal with the new SHR4 mode PAL256, where each byte is an index into the full palette
// of 256 colors. We have to do it here because the palette can be dynamically modified while
// racing the beam.
if (((scanlineSHR4Modes & A2SM_SHR4PAL256) != 0)
|| (overrideSHRMode == A2SM_SHR4PAL256))
{
// calculate x value where x is 0-40 in the content area
auto _x_just_content = _x - CYCLES_SC_HBL;
auto pal256ByteStartPtr = vrams_write->vram_pal256 + (_y * _A2VIDEO_SHR_BYTES_PER_LINE + (4 * _x_just_content))*2;
for (uint32_t i = 0; i < 4; i++)
{
// get the byte value, a pointer to the palette color
auto byteColor = lineStartPtr[_COLORBYTESOFFSET + (_TR_ANY_X * 4) + i];
// get the palette color (2 bytes), the palette being all 256 colors in a single palette
auto paletteColorStart = memPtr + _A2VIDEO_SHR_PALETTE_START + ((uint32_t)byteColor * 2);
pal256ByteStartPtr[2*i] = paletteColorStart[0];
pal256ByteStartPtr[2*i + 1] = paletteColorStart[1];
}
}
// Do the exact same thing for double SHR if necessary, getting the data from main RAM
if (bShouldPageDouble)
{
scb = lineInterlaceStartPtr[0];
memcpy(lineInterlaceStartPtr + _COLORBYTESOFFSET + _TR_ANY_X * 4,
memInterlacePtr + _A2VIDEO_SHR_START + _y * _A2VIDEO_SHR_BYTES_PER_LINE + xfb, 4);
if (!(scb & 0x80u) && (scb & 0x20u)) // 320 mode and colorfill
{
// Pre-calculate colorfill, so that the shader doesn't have to do it
// It's completely wasted on the shader. Here it's much more efficient
for (uint32_t i = 0; i < 4; i++)
{
auto byteColor = lineInterlaceStartPtr[_COLORBYTESOFFSET + (_TR_ANY_X * 4) + i];
// if the first color of the byte is 0, give it the last color of the previous byte
// assuming this is not the first byte of the line
if (((byteColor & 0xF0) == 0) && ((xfb + i) != 0))
byteColor |= (lineInterlaceStartPtr[_COLORBYTESOFFSET + (_TR_ANY_X * 4) + i - 1] & 0b1111) << 4;
// if the second color of the byte is 0, give it the first color of the byte
if ((byteColor & 0x0F) == 0)
byteColor |= (byteColor >> 4);
lineInterlaceStartPtr[_COLORBYTESOFFSET + (_TR_ANY_X * 4) + i] = byteColor;
}
}
// Here deal with the new SHR4 mode PAL256, where each byte is an index into the full palette
// of 256 colors. We have to do it here because the palette can be dynamically modified while
// racing the beam.
if (((scanlineSHR4Modes & A2SM_SHR4PAL256) != 0)
|| (overrideSHRMode == A2SM_SHR4PAL256))
{
// calculate x value where x is 0-40 in the content area
// move to the offset to the interlace area which is the second half of the vram (i.e. slide down by _A2VIDEO_SHR_SCANLINES)
auto _x_just_content = _x - CYCLES_SC_HBL;
auto pal256ByteStartPtr = vrams_write->vram_pal256 + ((_y + _A2VIDEO_SHR_SCANLINES) * _A2VIDEO_SHR_BYTES_PER_LINE + (4 * _x_just_content))*2;
for (uint32_t i = 0; i < 4; i++)
{
// get the byte value, a pointer to the palette color
auto byteColor = lineInterlaceStartPtr[_COLORBYTESOFFSET + (_TR_ANY_X * 4) + i];
// get the palette color (2 bytes), the palette being all 256 colors in a single palette
auto paletteColorStart = memInterlacePtr + _A2VIDEO_SHR_PALETTE_START + ((uint32_t)byteColor * 2);
pal256ByteStartPtr[2*i] = paletteColorStart[0];
pal256ByteStartPtr[2*i + 1] = paletteColorStart[1];
}
}
} // end interlacing
}
break;
default:
break;
}
return;
} // if (memMgr->IsSoftSwitch(A2SS_SHR))
// The byte isn't SHR, it's legacy
// at least 1 byte in this vblank cycle is LEGACY
// Note that overlay bytes are always legacy
switch (vrams_write->mode)
{
case A2Mode_e::NONE:
vrams_write->mode = A2Mode_e::LEGACY;
break;
case A2Mode_e::SHR:
SwitchToMergedMode(_y);
break;
default:
break;
}
// the flags byte is:
// bits 0-2: mode (TEXT, DTEXT, LGR, DLGR, HGR, DHGR, DHGRMONO, DHGR160)
// bit 3: 1: BORDER, 0: CONTENT
// bits 4-7: A2ESpecialMode_e legacy mode flags. They're not all useful to all modes
uint8_t flags = 0;
// the colors byte is:
// bits 0-3: background color
// bits 4-7: foreground color (also BORDER color)
uint8_t colors = 0;
bShouldPageDouble = (overrideLegacyPaging > 0 ? 1 : 0);
if (beamState >= BeamState_e::BORDER_LEFT) // the beam is in a visible area!
{
uint8_t* byteStartPtr = vrams_write->vram_legacy + (GetVramWidthLegacy() * _TR_ANY_Y + _TR_ANY_X) * 4;
auto _vramInterlaceOffset = GetVramSizeLegacy() / _INTERLACE_MULTIPLIER; // Offset to 2nd half of the vram
uint8_t* byteStartPtrInterlace = byteStartPtr + _vramInterlaceOffset; // For paged mode
// ======================================= SET MODE =======================================
// Always determine which mode we're in
if (!memMgr->IsSoftSwitch(A2SS_TEXT))
{
if (memMgr->IsSoftSwitch(A2SS_MIXED) && _y > 159) // check mixed mode
{
if (memMgr->IsSoftSwitch(A2SS_80COL))
flags |= 1; // DTEXT
else
flags |= 0; // TEXT
}
else if (memMgr->IsSoftSwitch(A2SS_80COL) && memMgr->IsSoftSwitch(A2SS_DHGR)) // double resolution
{
if (memMgr->IsSoftSwitch(A2SS_HIRES))
{
if (memMgr->IsSoftSwitch(A2SS_DHGRMONO))
flags |= 6; // DHGRMONO
else
{
if (bUseDHGR160)
flags |= 7; // DHGR160
else
flags |= 5; // DHGR
}
}
else
flags |= 3; // DLGR
}
else if (memMgr->IsSoftSwitch(A2SS_HIRES)) // standard hires
{
flags |= 4; // HGR
}
else { // standard lores
flags |= 2; // LGR
}
}
else { // Now check the text modes
if (memMgr->IsSoftSwitch(A2SS_80COL))
flags |= 1; // DTEXT