From f8bd94ae8727b558034a09839e61ce596846a023 Mon Sep 17 00:00:00 2001 From: tomcw Date: Sat, 20 Apr 2019 15:03:18 +0100 Subject: [PATCH 01/21] History.txt: updated for U3 Jukebox being fixed at 1.28.0 --- bin/History.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/History.txt b/bin/History.txt index 899d480e..e997aac0 100644 --- a/bin/History.txt +++ b/bin/History.txt @@ -63,7 +63,7 @@ Tom Charlesworth - Any v1 save-state files should be loaded into AppleWin 1.27, and then re-saved to a v2 save-state file. . [Change #597] Removed the functionality for CTRL+F10 to reveal the mouse cursor. . [Change #585] Added a 'Swap' HDD button to the Configuration->Input property sheet. -. [Bug #608] Mockingboard's 6522 TIMER1 wasn't generating an interrupt quickly enough for Broadside's detection routine. +. [Bug #608,#236] Mockingboard's 6522 TIMER1 wasn't generating an interrupt quickly enough for detection routines for Broadside and Ultima III Jukebox. 1.27.13.0 - 8 Dec 2018 From 941ef46e9a5b5b1d52f4a4ee973e5bf27c38a031 Mon Sep 17 00:00:00 2001 From: tomcw Date: Sat, 1 Jun 2019 12:21:00 +0100 Subject: [PATCH 02/21] 6522: account for underflowed cycles to ensure consistent interrupt period (#651) --- source/Mockingboard.cpp | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/source/Mockingboard.cpp b/source/Mockingboard.cpp index b4044263..4d713904 100644 --- a/source/Mockingboard.cpp +++ b/source/Mockingboard.cpp @@ -1797,11 +1797,19 @@ void MB_UpdateCycles(ULONG uExecutedCycles) { // Free-running mode // - Ultima4/5 change ACCESS_TIMER1 after a couple of IRQs into tune - pMB->sy6522.TIMER1_COUNTER.w = pMB->sy6522.TIMER1_LATCH.w; + pMB->sy6522.TIMER1_COUNTER.w += pMB->sy6522.TIMER1_LATCH.w; // GH#651: account for underflowed cycles too + if (pMB->sy6522.TIMER1_COUNTER.w > pMB->sy6522.TIMER1_LATCH.w) + { + if (pMB->sy6522.TIMER1_LATCH.w) + pMB->sy6522.TIMER1_COUNTER.w %= pMB->sy6522.TIMER1_LATCH.w; // Only occurs if LATCH.w<0x0007 (# cycles for longest opcode) + else + pMB->sy6522.TIMER1_COUNTER.w = 0; + } StartTimer1(pMB); } } - else if (pMB->bTimer2Active && bTimer2Underflow) + + if (pMB->bTimer2Active && bTimer2Underflow) { UpdateIFR(pMB, 0, IxR_TIMER2); @@ -1811,7 +1819,14 @@ void MB_UpdateCycles(ULONG uExecutedCycles) } else { - pMB->sy6522.TIMER2_COUNTER.w = pMB->sy6522.TIMER2_LATCH.w; + pMB->sy6522.TIMER2_COUNTER.w += pMB->sy6522.TIMER2_LATCH.w; + if (pMB->sy6522.TIMER2_COUNTER.w > pMB->sy6522.TIMER2_LATCH.w) + { + if (pMB->sy6522.TIMER2_LATCH.w) + pMB->sy6522.TIMER2_COUNTER.w %= pMB->sy6522.TIMER2_LATCH.w; + else + pMB->sy6522.TIMER2_COUNTER.w = 0; + } StartTimer2(pMB); } } From 3a41061f836896a2e8e7ac8587476b2c82e1aee3 Mon Sep 17 00:00:00 2001 From: tomcw Date: Sat, 1 Jun 2019 16:54:58 +0100 Subject: [PATCH 03/21] Check interrupt sources after every opcode when in normal speed. (#651) --- source/CPU.cpp | 13 +++++++------ source/CPU/cpu6502.h | 2 +- source/CPU/cpu65C02.h | 2 +- source/CPU/cpu65d02.h | 2 +- test/TestCPU6502/TestCPU6502.cpp | 2 +- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/source/CPU.cpp b/source/CPU.cpp index c951431d..8b1e9635 100644 --- a/source/CPU.cpp +++ b/source/CPU.cpp @@ -132,10 +132,11 @@ unsigned __int64 g_nCumulativeCycles = 0; static ULONG g_nCyclesExecuted; // # of cycles executed up to last IO access //static signed long g_uInternalExecutedCycles; -// TODO: Use IRQ_CHECK_TIMEOUT=128 when running at full-speed else with IRQ_CHECK_TIMEOUT=1 +// Use IRQ_CHECK_TIMEOUT=128 when running at full-speed; else use IRQ_CHECK_TIMEOUT=1 (GH#651) // - What about when running benchmark? -static const int IRQ_CHECK_TIMEOUT = 128; -static signed int g_nIrqCheckTimeout = IRQ_CHECK_TIMEOUT; +static const int IRQ_CHECK_TIMEOUT_FULL_SPEED = 128; +static const int IRQ_CHECK_TIMEOUT_NORMAL_SPEED = 1; +static signed int g_nIrqCheckTimeout = IRQ_CHECK_TIMEOUT_NORMAL_SPEED; // @@ -417,20 +418,20 @@ static __forceinline void IRQ(ULONG& uExecutedCycles, BOOL& flagc, BOOL& flagn, } } -static __forceinline void CheckInterruptSources(ULONG uExecutedCycles) +static __forceinline void CheckInterruptSources(ULONG uExecutedCycles, const bool bVideoUpdate) { if (g_nIrqCheckTimeout < 0) { MB_UpdateCycles(uExecutedCycles); sg_Mouse.SetVBlank( !VideoGetVblBar(uExecutedCycles) ); - g_nIrqCheckTimeout = IRQ_CHECK_TIMEOUT; + g_nIrqCheckTimeout = bVideoUpdate ? IRQ_CHECK_TIMEOUT_NORMAL_SPEED : IRQ_CHECK_TIMEOUT_FULL_SPEED; } } // GH#608: IRQ needs to occur within 17 cycles (6 opcodes) of configuring the timer interrupt void CpuAdjustIrqCheck(UINT uCyclesUntilInterrupt) { - if (uCyclesUntilInterrupt < IRQ_CHECK_TIMEOUT) + if (g_bFullSpeed && uCyclesUntilInterrupt < IRQ_CHECK_TIMEOUT_FULL_SPEED) g_nIrqCheckTimeout = uCyclesUntilInterrupt; } diff --git a/source/CPU/cpu6502.h b/source/CPU/cpu6502.h index a9b87701..29b8452b 100644 --- a/source/CPU/cpu6502.h +++ b/source/CPU/cpu6502.h @@ -318,7 +318,7 @@ static DWORD Cpu6502(DWORD uTotalCycles, const bool bVideoUpdate) #undef $ } - CheckInterruptSources(uExecutedCycles); + CheckInterruptSources(uExecutedCycles, bVideoUpdate); NMI(uExecutedCycles, flagc, flagn, flagv, flagz); IRQ(uExecutedCycles, flagc, flagn, flagv, flagz); diff --git a/source/CPU/cpu65C02.h b/source/CPU/cpu65C02.h index 67279db3..1dc06227 100644 --- a/source/CPU/cpu65C02.h +++ b/source/CPU/cpu65C02.h @@ -321,7 +321,7 @@ static DWORD Cpu65C02(DWORD uTotalCycles, const bool bVideoUpdate) #undef $ } - CheckInterruptSources(uExecutedCycles); + CheckInterruptSources(uExecutedCycles, bVideoUpdate); NMI(uExecutedCycles, flagc, flagn, flagv, flagz); IRQ(uExecutedCycles, flagc, flagn, flagv, flagz); diff --git a/source/CPU/cpu65d02.h b/source/CPU/cpu65d02.h index e7e5b2e9..bdb78634 100644 --- a/source/CPU/cpu65d02.h +++ b/source/CPU/cpu65d02.h @@ -406,7 +406,7 @@ static DWORD Cpu65D02(DWORD uTotalCycles, const bool bVideoUpdate) } #undef $ - CheckInterruptSources(uExecutedCycles); + CheckInterruptSources(uExecutedCycles, bVideoUpdate); NMI(uExecutedCycles, flagc, flagn, flagv, flagz); IRQ(uExecutedCycles, flagc, flagn, flagv, flagz); diff --git a/test/TestCPU6502/TestCPU6502.cpp b/test/TestCPU6502/TestCPU6502.cpp index 08342e73..2e869469 100644 --- a/test/TestCPU6502/TestCPU6502.cpp +++ b/test/TestCPU6502/TestCPU6502.cpp @@ -54,7 +54,7 @@ static __forceinline void DoIrqProfiling(DWORD uCycles) { } -static __forceinline void CheckInterruptSources(ULONG uExecutedCycles) +static __forceinline void CheckInterruptSources(ULONG uExecutedCycles, const bool bVideoUpdate) { } From 98a733ba732801503cac3370309904e98c75aaba Mon Sep 17 00:00:00 2001 From: tomcw Date: Sat, 1 Jun 2019 17:01:15 +0100 Subject: [PATCH 04/21] Removed comment about benchmark: as benchmark is now run in both normal and full-speed modes --- source/CPU.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/source/CPU.cpp b/source/CPU.cpp index 8b1e9635..bcdff140 100644 --- a/source/CPU.cpp +++ b/source/CPU.cpp @@ -133,7 +133,6 @@ static ULONG g_nCyclesExecuted; // # of cycles executed up to last IO access //static signed long g_uInternalExecutedCycles; // Use IRQ_CHECK_TIMEOUT=128 when running at full-speed; else use IRQ_CHECK_TIMEOUT=1 (GH#651) -// - What about when running benchmark? static const int IRQ_CHECK_TIMEOUT_FULL_SPEED = 128; static const int IRQ_CHECK_TIMEOUT_NORMAL_SPEED = 1; static signed int g_nIrqCheckTimeout = IRQ_CHECK_TIMEOUT_NORMAL_SPEED; From 1f2dc6ee8a9130807c69b6940048532eab9eee5d Mon Sep 17 00:00:00 2001 From: tomcw Date: Sun, 2 Jun 2019 14:30:54 +0100 Subject: [PATCH 05/21] Full-speed: only do interrupt checking every 40 opcodes & simplify CYC macro (#651) --- source/CPU.cpp | 25 ++++++++++++++----------- source/CPU/cpu_general.inl | 2 +- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/source/CPU.cpp b/source/CPU.cpp index bcdff140..4db08186 100644 --- a/source/CPU.cpp +++ b/source/CPU.cpp @@ -130,12 +130,7 @@ regsrec regs; unsigned __int64 g_nCumulativeCycles = 0; static ULONG g_nCyclesExecuted; // # of cycles executed up to last IO access - //static signed long g_uInternalExecutedCycles; -// Use IRQ_CHECK_TIMEOUT=128 when running at full-speed; else use IRQ_CHECK_TIMEOUT=1 (GH#651) -static const int IRQ_CHECK_TIMEOUT_FULL_SPEED = 128; -static const int IRQ_CHECK_TIMEOUT_NORMAL_SPEED = 1; -static signed int g_nIrqCheckTimeout = IRQ_CHECK_TIMEOUT_NORMAL_SPEED; // @@ -417,21 +412,29 @@ static __forceinline void IRQ(ULONG& uExecutedCycles, BOOL& flagc, BOOL& flagn, } } +const int IRQ_CHECK_OPCODE_FULL_SPEED = 40; // ~128 cycles (assume 3 cycles per opcode) +static int g_fullSpeedOpcodeCount = IRQ_CHECK_OPCODE_FULL_SPEED; + static __forceinline void CheckInterruptSources(ULONG uExecutedCycles, const bool bVideoUpdate) { - if (g_nIrqCheckTimeout < 0) + if (!bVideoUpdate) { - MB_UpdateCycles(uExecutedCycles); - sg_Mouse.SetVBlank( !VideoGetVblBar(uExecutedCycles) ); - g_nIrqCheckTimeout = bVideoUpdate ? IRQ_CHECK_TIMEOUT_NORMAL_SPEED : IRQ_CHECK_TIMEOUT_FULL_SPEED; + g_fullSpeedOpcodeCount--; + if (g_fullSpeedOpcodeCount >= 0) + return; + g_fullSpeedOpcodeCount = IRQ_CHECK_OPCODE_FULL_SPEED; } + + MB_UpdateCycles(uExecutedCycles); + sg_Mouse.SetVBlank( !VideoGetVblBar(uExecutedCycles) ); } // GH#608: IRQ needs to occur within 17 cycles (6 opcodes) of configuring the timer interrupt void CpuAdjustIrqCheck(UINT uCyclesUntilInterrupt) { - if (g_bFullSpeed && uCyclesUntilInterrupt < IRQ_CHECK_TIMEOUT_FULL_SPEED) - g_nIrqCheckTimeout = uCyclesUntilInterrupt; + const UINT opcodesUntilInterrupt = uCyclesUntilInterrupt/3; // assume 3 cycles per opcode + if (g_bFullSpeed && opcodesUntilInterrupt < IRQ_CHECK_OPCODE_FULL_SPEED) + g_fullSpeedOpcodeCount = opcodesUntilInterrupt; } //=========================================================================== diff --git a/source/CPU/cpu_general.inl b/source/CPU/cpu_general.inl index 4d565efb..8f0b7a87 100644 --- a/source/CPU/cpu_general.inl +++ b/source/CPU/cpu_general.inl @@ -49,7 +49,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA | (flagz ? AF_ZERO : 0) \ | AF_RESERVED | AF_BREAK; // CYC(a): This can be optimised, as only certain opcodes will affect uExtraCycles -#define CYC(a) uExecutedCycles += (a)+uExtraCycles; g_nIrqCheckTimeout -= (a)+uExtraCycles; +#define CYC(a) uExecutedCycles += (a)+uExtraCycles; #define POP (*(mem+((regs.sp >= 0x1FF) ? (regs.sp = 0x100) : ++regs.sp))) #define PUSH(a) *(mem+regs.sp--) = (a); \ if (regs.sp < 0x100) \ From 9a7424e7046584d7938e3fbebae95926f05cac59 Mon Sep 17 00:00:00 2001 From: tomcw Date: Sun, 2 Jun 2019 17:41:51 +0100 Subject: [PATCH 06/21] 1.28.6.0: Updated version & history.txt --- bin/History.txt | 7 +++++++ resource/version.h | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/bin/History.txt b/bin/History.txt index e997aac0..65331cba 100644 --- a/bin/History.txt +++ b/bin/History.txt @@ -9,6 +9,13 @@ https://github.com/AppleWin/AppleWin/issues/new Tom Charlesworth +1.28.6.0 - 1 Jun 2019 +--------------------- +. [Bug #651] Cycle-accurate interrupts: + - Interrupts sources are checked after every opcode (full-speed after every 40 opcodes). + - 6522 TIMERs in free-running mode now account for the underflowed cycles when resetting the count. + + 1.28.5.0 - 6 Apr 2019 --------------------- . [Change #631] Improvements for the RGB AppleColor card: diff --git a/resource/version.h b/resource/version.h index 4b56dec7..e5ea18f6 100644 --- a/resource/version.h +++ b/resource/version.h @@ -1,4 +1,4 @@ -#define APPLEWIN_VERSION 1,28,5,0 +#define APPLEWIN_VERSION 1,28,6,0 #define xstr(a) str(a) #define str(a) #a From 3fbe41642482ef8b3d1ef1325af8f34640c6f9c0 Mon Sep 17 00:00:00 2001 From: tomcw Date: Sat, 15 Jun 2019 17:41:53 +0100 Subject: [PATCH 07/21] 6522: Underflow on 0x0001 -> 0x0000; and FRT's period is N+2 cycles (#652) --- source/Mockingboard.cpp | 79 ++++++++++++++++++++++++------- source/SaveState_Structs_common.h | 2 + 2 files changed, 63 insertions(+), 18 deletions(-) diff --git a/source/Mockingboard.cpp b/source/Mockingboard.cpp index 4d713904..8e373727 100644 --- a/source/Mockingboard.cpp +++ b/source/Mockingboard.cpp @@ -1737,13 +1737,46 @@ void MB_EndOfVideoFrame() //----------------------------------------------------------------------------- +static bool CheckTimerUnderflowAndIrq(USHORT& timerCounter, int& timerIrqDelay, const USHORT nClocks, bool* pTimerUnderflow=NULL) +{ + int oldTimer = timerCounter; // Catch the case for 0x0000 -> -ve, as this isn't an underflow + int timer = timerCounter; + timer -= nClocks; + timerCounter = (USHORT)timer; + + bool timerIrq = false; + + if (timerIrqDelay) // Deal with any previous counter underflow which didn't yet result in an IRQ + { + timerIrqDelay -= nClocks; + if (timerIrqDelay <= 0) + { + timerIrqDelay = 0; + timerIrq = true; + } + // don't re-underflow if TIMER = 0x0000 or 0xFFFF (so just return) + } + else if (oldTimer > 0 && timer <= 0) // Underflow occurs for 0x0001 -> 0x0000 + { + if (pTimerUnderflow) + *pTimerUnderflow = true; // Just for Willy Byte! + + if (timer <= -2) + timerIrq = true; + else // TIMER = 0x0000 or 0xFFFF + timerIrqDelay = 2 + timer; // ...so 2 or 1 cycles until IRQ + } + + return timerIrq; +} + // Called by: // . CpuExecute() every ~1000 @ 1MHz // . CheckInterruptSources() every 128 cycles // . MB_Read() / MB_Write() void MB_UpdateCycles(ULONG uExecutedCycles) { - if(g_SoundcardType == CT_Empty) + if (g_SoundcardType == CT_Empty) return; CpuCalcCycles(uExecutedCycles); @@ -1752,19 +1785,13 @@ void MB_UpdateCycles(ULONG uExecutedCycles) _ASSERT(uCycles < 0x10000); USHORT nClocks = (USHORT) uCycles; - for(int i=0; isy6522.TIMER1_COUNTER.w; - USHORT OldTimer2 = pMB->sy6522.TIMER2_COUNTER.w; - - pMB->sy6522.TIMER1_COUNTER.w -= nClocks; - pMB->sy6522.TIMER2_COUNTER.w -= nClocks; - - // Check for counter underflow - bool bTimer1Underflow = (!(OldTimer1 & 0x8000) && (pMB->sy6522.TIMER1_COUNTER.w & 0x8000)); - bool bTimer2Underflow = (!(OldTimer2 & 0x8000) && (pMB->sy6522.TIMER2_COUNTER.w & 0x8000)); + bool bTimer1Underflow = false; // Just for Willy Byte! + const bool bTimer1Irq = CheckTimerUnderflowAndIrq(pMB->sy6522.TIMER1_COUNTER.w, pMB->sy6522.timer1IrqDelay, nClocks, &bTimer1Underflow); + const bool bTimer2Irq = CheckTimerUnderflowAndIrq(pMB->sy6522.TIMER2_COUNTER.w, pMB->sy6522.timer2IrqDelay, nClocks); if (!pMB->bTimer1Active && bTimer1Underflow) { @@ -1774,11 +1801,12 @@ void MB_UpdateCycles(ULONG uExecutedCycles) { // Fix for Willy Byte - need to confirm that 6522 really does this! // . It never accesses IER/IFR/TIMER1 regs to clear IRQ + // . NB. Willy Byte doesn't work with Phasor. UpdateIFR(pMB, IxR_TIMER1); // Deassert the TIMER IRQ } } - if (pMB->bTimer1Active && bTimer1Underflow) + if (pMB->bTimer1Active && bTimer1Irq) { UpdateIFR(pMB, 0, IxR_TIMER1); @@ -1786,7 +1814,7 @@ void MB_UpdateCycles(ULONG uExecutedCycles) if (g_nMBTimerDevice == i) MB_Update(); - if((pMB->sy6522.ACR & RUNMODE) == RM_ONESHOT) + if ((pMB->sy6522.ACR & RUNMODE) == RM_ONESHOT) { // One-shot mode // - Phasor's playback code uses one-shot mode @@ -1798,6 +1826,8 @@ void MB_UpdateCycles(ULONG uExecutedCycles) // Free-running mode // - Ultima4/5 change ACCESS_TIMER1 after a couple of IRQs into tune pMB->sy6522.TIMER1_COUNTER.w += pMB->sy6522.TIMER1_LATCH.w; // GH#651: account for underflowed cycles too + pMB->sy6522.TIMER1_COUNTER.w += 2; // GH#652: account for extra 2 cycles (Rockwell, Fig.16: period=N+2cycles) + // - or maybe the counter doesn't count down during these 2 cycles? if (pMB->sy6522.TIMER1_COUNTER.w > pMB->sy6522.TIMER1_LATCH.w) { if (pMB->sy6522.TIMER1_LATCH.w) @@ -1809,7 +1839,7 @@ void MB_UpdateCycles(ULONG uExecutedCycles) } } - if (pMB->bTimer2Active && bTimer2Underflow) + if (pMB->bTimer2Active && bTimer2Irq) { UpdateIFR(pMB, 0, IxR_TIMER2); @@ -1918,7 +1948,8 @@ void MB_GetSnapshot_v1(SS_CARD_MOCKINGBOARD_v1* const pSS, const DWORD dwSlot) // Unit version history: // 2: Added: Timer1 & Timer2 active // 3: Added: Unit state -const UINT kUNIT_VERSION = 3; +// 4: Added: 6522 timerIrqDelay +const UINT kUNIT_VERSION = 4; const UINT NUM_MB_UNITS = 2; const UINT NUM_PHASOR_UNITS = 2; @@ -1952,6 +1983,8 @@ const UINT NUM_PHASOR_UNITS = 2; #define SS_YAML_KEY_SPEECH_IRQ "Speech IRQ Pending" #define SS_YAML_KEY_TIMER1_ACTIVE "Timer1 Active" #define SS_YAML_KEY_TIMER2_ACTIVE "Timer2 Active" +#define SS_YAML_KEY_SY6522_TIMER1_IRQ_DELAY "Timer1 IRQ Delay" +#define SS_YAML_KEY_SY6522_TIMER2_IRQ_DELAY "Timer2 IRQ Delay" #define SS_YAML_KEY_PHASOR_UNIT "Unit" #define SS_YAML_KEY_PHASOR_CLOCK_SCALE_FACTOR "Clock Scale Factor" @@ -1979,8 +2012,10 @@ static void SaveSnapshotSY6522(YamlSaveHelper& yamlSaveHelper, SY6522& sy6522) yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_SY6522_REG_DDRA, sy6522.DDRA); yamlSaveHelper.SaveHexUint16(SS_YAML_KEY_SY6522_REG_T1_COUNTER, sy6522.TIMER1_COUNTER.w); yamlSaveHelper.SaveHexUint16(SS_YAML_KEY_SY6522_REG_T1_LATCH, sy6522.TIMER1_LATCH.w); + yamlSaveHelper.SaveUint(SS_YAML_KEY_SY6522_TIMER1_IRQ_DELAY, sy6522.timer1IrqDelay); // v4 yamlSaveHelper.SaveHexUint16(SS_YAML_KEY_SY6522_REG_T2_COUNTER, sy6522.TIMER2_COUNTER.w); yamlSaveHelper.SaveHexUint16(SS_YAML_KEY_SY6522_REG_T2_LATCH, sy6522.TIMER2_LATCH.w); + yamlSaveHelper.SaveUint(SS_YAML_KEY_SY6522_TIMER2_IRQ_DELAY, sy6522.timer2IrqDelay); // v4 yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_SY6522_REG_SERIAL_SHIFT, sy6522.SERIAL_SHIFT); yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_SY6522_REG_ACR, sy6522.ACR); yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_SY6522_REG_PCR, sy6522.PCR); @@ -2032,7 +2067,7 @@ void MB_SaveSnapshot(YamlSaveHelper& yamlSaveHelper, const UINT uSlot) } } -static void LoadSnapshotSY6522(YamlLoadHelper& yamlLoadHelper, SY6522& sy6522) +static void LoadSnapshotSY6522(YamlLoadHelper& yamlLoadHelper, SY6522& sy6522, UINT version) { if (!yamlLoadHelper.GetSubMap(SS_YAML_KEY_SY6522)) throw std::string("Card: Expected key: ") + std::string(SS_YAML_KEY_SY6522); @@ -2052,6 +2087,14 @@ static void LoadSnapshotSY6522(YamlLoadHelper& yamlLoadHelper, SY6522& sy6522) sy6522.IER = yamlLoadHelper.LoadUint(SS_YAML_KEY_SY6522_REG_IER); sy6522.ORA_NO_HS = 0; // Not saved + sy6522.timer1IrqDelay = sy6522.timer2IrqDelay = 0; + + if (version >= 4) + { + sy6522.timer1IrqDelay = yamlLoadHelper.LoadUint(SS_YAML_KEY_SY6522_TIMER1_IRQ_DELAY); + sy6522.timer2IrqDelay = yamlLoadHelper.LoadUint(SS_YAML_KEY_SY6522_TIMER2_IRQ_DELAY); + } + yamlLoadHelper.PopMap(); } @@ -2094,7 +2137,7 @@ bool MB_LoadSnapshot(YamlLoadHelper& yamlLoadHelper, UINT slot, UINT version) if (!yamlLoadHelper.GetSubMap(unit)) throw std::string("Card: Expected key: ") + std::string(unit); - LoadSnapshotSY6522(yamlLoadHelper, pMB->sy6522); + LoadSnapshotSY6522(yamlLoadHelper, pMB->sy6522, version); AY8910_LoadSnapshot(yamlLoadHelper, nDeviceNum, std::string("")); LoadSnapshotSSI263(yamlLoadHelper, pMB->SpeechChip); @@ -2216,7 +2259,7 @@ bool Phasor_LoadSnapshot(YamlLoadHelper& yamlLoadHelper, UINT slot, UINT version if (!yamlLoadHelper.GetSubMap(unit)) throw std::string("Card: Expected key: ") + std::string(unit); - LoadSnapshotSY6522(yamlLoadHelper, pMB->sy6522); + LoadSnapshotSY6522(yamlLoadHelper, pMB->sy6522, version); AY8910_LoadSnapshot(yamlLoadHelper, nDeviceNum+0, std::string("-A")); AY8910_LoadSnapshot(yamlLoadHelper, nDeviceNum+1, std::string("-B")); LoadSnapshotSSI263(yamlLoadHelper, pMB->SpeechChip); diff --git a/source/SaveState_Structs_common.h b/source/SaveState_Structs_common.h index 8a44ce8f..e920753f 100644 --- a/source/SaveState_Structs_common.h +++ b/source/SaveState_Structs_common.h @@ -115,6 +115,8 @@ struct SY6522 IWORD TIMER1_LATCH; IWORD TIMER2_COUNTER; IWORD TIMER2_LATCH; + int timer1IrqDelay; + int timer2IrqDelay; // BYTE SERIAL_SHIFT; // $0A BYTE ACR; // $0B - Auxiliary Control Register From e6e52ffcf479cf930f39d6dd13dad900a625dbdb Mon Sep 17 00:00:00 2001 From: tomcw Date: Sat, 15 Jun 2019 18:15:00 +0100 Subject: [PATCH 08/21] Delay any video mode change by 1 cycle (#654) --- source/NTSC.cpp | 28 +++++++++++++++++++++++++--- source/NTSC.h | 2 +- source/Video.cpp | 2 +- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/source/NTSC.cpp b/source/NTSC.cpp index fe7b7c82..a7efccae 100644 --- a/source/NTSC.cpp +++ b/source/NTSC.cpp @@ -125,6 +125,9 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA static int g_nHiresPage = 1; static int g_nTextPage = 1; + static bool g_bDelayVideoMode = false; + static uint32_t g_uNewVideoModeFlags = 0; + // Understanding the Apple II, Timing Generation and the Video Scanner, Pg 3-11 // Vertical Scanning // Horizontal Scanning @@ -1852,7 +1855,7 @@ uint16_t NTSC_VideoGetScannerAddress ( const ULONG uExecutedCycles ) const uint16_t currVideoClockHorz = g_nVideoClockHorz; // Required for ANSI STORY (end credits) vert scrolling mid-scanline mixed mode: DGR80, TEXT80, DGR80 - g_nVideoClockHorz -= 2; + g_nVideoClockHorz -= 1; if ((SHORT)g_nVideoClockHorz < 0) { g_nVideoClockHorz += VIDEO_SCANNER_MAX_HORZ; @@ -1884,8 +1887,15 @@ void NTSC_SetVideoTextMode( int cols ) } //=========================================================================== -void NTSC_SetVideoMode( uint32_t uVideoModeFlags ) +void NTSC_SetVideoMode( uint32_t uVideoModeFlags, bool bDelay/*=false*/ ) { + if (bDelay) + { + g_bDelayVideoMode = true; + g_uNewVideoModeFlags = uVideoModeFlags; + return; + } + g_nVideoMixed = uVideoModeFlags & VF_MIXED; g_nVideoCharSet = VideoGetSWAltCharSet() ? 1 : 0; @@ -2227,7 +2237,19 @@ static void VideoUpdateCycles( int cyclesLeftToUpdate ) //=========================================================================== void NTSC_VideoUpdateCycles( long cycles6502 ) { - _ASSERT(cycles6502 < VIDEO_SCANNER_6502_CYCLES); // Use NTSC_VideoRedrawWholeScreen() instead + _ASSERT(cycles6502 && cycles6502 < VIDEO_SCANNER_6502_CYCLES); // Use NTSC_VideoRedrawWholeScreen() instead + + if (g_bDelayVideoMode) + { + VideoUpdateCycles(1); // Video mode change is delayed by 1 cycle + + g_bDelayVideoMode = false; + NTSC_SetVideoMode(g_uNewVideoModeFlags); + + cycles6502--; + if (!cycles6502) + return; + } VideoUpdateCycles(cycles6502); } diff --git a/source/NTSC.h b/source/NTSC.h index 23ce6dc2..44f7426f 100644 --- a/source/NTSC.h +++ b/source/NTSC.h @@ -4,7 +4,7 @@ extern uint32_t g_nChromaSize; // Prototypes (Public) ________________________________________________ - extern void NTSC_SetVideoMode( uint32_t uVideoModeFlags ); + extern void NTSC_SetVideoMode( uint32_t uVideoModeFlags, bool bDelay=false ); extern void NTSC_SetVideoStyle(); extern void NTSC_SetVideoTextMode( int cols ); extern uint32_t*NTSC_VideoGetChromaTable( bool bHueTypeMonochrome, bool bMonitorTypeColorTV ); diff --git a/source/Video.cpp b/source/Video.cpp index d3f77195..c5d37e19 100644 --- a/source/Video.cpp +++ b/source/Video.cpp @@ -675,7 +675,7 @@ BYTE VideoSetMode(WORD, WORD address, BYTE write, BYTE, ULONG uExecutedCycles) if (!IS_APPLE2) RGB_SetVideoMode(address); - NTSC_SetVideoMode( g_uVideoMode ); + NTSC_SetVideoMode( g_uVideoMode, true ); return MemReadFloatingBus(uExecutedCycles); } From 51802257aaba5c1ba49050f0258d2f4800aeb950 Mon Sep 17 00:00:00 2001 From: tomcw Date: Sat, 15 Jun 2019 22:38:31 +0100 Subject: [PATCH 09/21] 1.28.7.0: Updated version & history.txt --- bin/History.txt | 7 ++++++- resource/version.h | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/bin/History.txt b/bin/History.txt index 65331cba..cce7af1e 100644 --- a/bin/History.txt +++ b/bin/History.txt @@ -8,8 +8,13 @@ https://github.com/AppleWin/AppleWin/issues/new Tom Charlesworth +1.28.7.0 - 15 Jun 2019 +---------------------- +. [Bug #654] Fix for Sather's "Little Text Window" not rendering correctly. +. [Bug #654] Fix for 6522 TIMER1's period to be N+2 cycles. -1.28.6.0 - 1 Jun 2019 + +1.28.6.0 - 2 Jun 2019 --------------------- . [Bug #651] Cycle-accurate interrupts: - Interrupts sources are checked after every opcode (full-speed after every 40 opcodes). diff --git a/resource/version.h b/resource/version.h index e5ea18f6..57effbfb 100644 --- a/resource/version.h +++ b/resource/version.h @@ -1,4 +1,4 @@ -#define APPLEWIN_VERSION 1,28,6,0 +#define APPLEWIN_VERSION 1,28,7,0 #define xstr(a) str(a) #define str(a) #a From dbcb789442c4bfa5b06d61c3f80aa4bf7018b83a Mon Sep 17 00:00:00 2001 From: tomcw Date: Mon, 24 Jun 2019 22:05:32 +0100 Subject: [PATCH 10/21] Don't delay a PAGE1/2 video mode change (#656) --- source/Video.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/source/Video.cpp b/source/Video.cpp index c5d37e19..ede3af34 100644 --- a/source/Video.cpp +++ b/source/Video.cpp @@ -652,6 +652,8 @@ BYTE VideoSetMode(WORD, WORD address, BYTE write, BYTE, ULONG uExecutedCycles) { address &= 0xFF; + const uint32_t oldVideoMode = g_uVideoMode; + switch (address) { case 0x00: g_uVideoMode &= ~VF_80STORE; break; @@ -675,7 +677,11 @@ BYTE VideoSetMode(WORD, WORD address, BYTE write, BYTE, ULONG uExecutedCycles) if (!IS_APPLE2) RGB_SetVideoMode(address); - NTSC_SetVideoMode( g_uVideoMode, true ); + bool delay = true; + if ((oldVideoMode ^ g_uVideoMode) & VF_PAGE2) + delay = false; // PAGE2 flag changed state, so no 1 cycle delay (GH#656) + + NTSC_SetVideoMode( g_uVideoMode, delay ); return MemReadFloatingBus(uExecutedCycles); } From bd86088c5958ec8aa9bd2731b992c7a39ec316e1 Mon Sep 17 00:00:00 2001 From: TomCh Date: Fri, 28 Jun 2019 21:34:34 +0100 Subject: [PATCH 11/21] Support 50Hz(PAL) (#648) (PR #658) - Added Configuration GUI to include checkbox for "50Hz" - Implicitly use PAL or NTSC base 6502 clocks depending on video refresh rate - Added new -50hz and -60hz command line switches - Updated save-state for video refresh rate 1.28.8.0: Updated version & history.txt --- AppleWinExpress2015.vcxproj | 1 + bin/History.txt | 9 +- resource/Applewin.rc | 1 + resource/resource.h | 1 + resource/version.h | 2 +- source/Applewin.cpp | 39 ++++-- source/Applewin.h | 4 +- source/Common.h | 17 +-- source/Configuration/Config.h | 17 ++- source/Configuration/PageConfig.cpp | 10 ++ source/Configuration/PropertySheetHelper.cpp | 10 ++ source/Memory.cpp | 2 +- source/Mockingboard.cpp | 9 +- source/NTSC.cpp | 119 ++++++++++++++----- source/NTSC.h | 6 +- source/SaveState.cpp | 8 +- source/Video.cpp | 62 +++++++--- source/Video.h | 12 +- source/Z80VICE/z80.cpp | 5 +- 19 files changed, 251 insertions(+), 83 deletions(-) diff --git a/AppleWinExpress2015.vcxproj b/AppleWinExpress2015.vcxproj index da8de854..ad61ff30 100644 --- a/AppleWinExpress2015.vcxproj +++ b/AppleWinExpress2015.vcxproj @@ -369,6 +369,7 @@ true source\cpu;source\emulator;source\debugger;zlib;zip_lib;libyaml\include;%(AdditionalIncludeDirectories) MultiThreadedDebug + Default Windows diff --git a/bin/History.txt b/bin/History.txt index cce7af1e..fdf84d96 100644 --- a/bin/History.txt +++ b/bin/History.txt @@ -8,10 +8,17 @@ https://github.com/AppleWin/AppleWin/issues/new Tom Charlesworth +1.28.8.0 - 28 Jun 2019 +---------------------- +. [Change #648] Support 50Hz(PAL) video refresh rate and implicitly PAL 1.018MHz. + - NB. TV video modes still use NTSC rendering. +. [Bug #656] Fix for PAGE1/2 ($C054/55) not having a 1 cycle delay. + + 1.28.7.0 - 15 Jun 2019 ---------------------- . [Bug #654] Fix for Sather's "Little Text Window" not rendering correctly. -. [Bug #654] Fix for 6522 TIMER1's period to be N+2 cycles. +. [Bug #652] Fix for 6522 TIMER1's period to be N+2 cycles. 1.28.6.0 - 2 Jun 2019 diff --git a/resource/Applewin.rc b/resource/Applewin.rc index 2e2f4a68..a20b23a0 100644 --- a/resource/Applewin.rc +++ b/resource/Applewin.rc @@ -111,6 +111,7 @@ BEGIN CTEXT "2.0",IDC_2_0_MHz,96,180,20,10 RTEXT "Fastest",IDC_MAX_MHz,150,180,29,10 PUSHBUTTON "&Benchmark Emulator",IDC_BENCHMARK,15,194,85,15 + CONTROL "50Hz video",IDC_CHECK_50HZ_VIDEO,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,142,141,51,10 END IDD_PROPPAGE_INPUT DIALOGEX 0, 0, 210, 215 diff --git a/resource/resource.h b/resource/resource.h index 3b6df8e2..7c6b5402 100644 --- a/resource/resource.h +++ b/resource/resource.h @@ -116,6 +116,7 @@ #define IDC_COMBO_DISK2 1081 #define IDC_CHECK_FS_SHOW_SUBUNIT_STATUS 1082 #define IDC_CHECK_VERTICAL_BLEND 1083 +#define IDC_CHECK_50HZ_VIDEO 1084 #define IDM_EXIT 40001 #define IDM_HELP 40002 #define IDM_ABOUT 40003 diff --git a/resource/version.h b/resource/version.h index 57effbfb..bb51944c 100644 --- a/resource/version.h +++ b/resource/version.h @@ -1,4 +1,4 @@ -#define APPLEWIN_VERSION 1,28,7,0 +#define APPLEWIN_VERSION 1,28,8,0 #define xstr(a) str(a) #define str(a) #a diff --git a/source/Applewin.cpp b/source/Applewin.cpp index 2a879c3e..17270177 100644 --- a/source/Applewin.cpp +++ b/source/Applewin.cpp @@ -91,7 +91,7 @@ bool g_bRestart = false; bool g_bRestartFullScreen = false; DWORD g_dwSpeed = SPEED_NORMAL; // Affected by Config dialog's speed slider bar -double g_fCurrentCLK6502 = CLK_6502; // Affected by Config dialog's speed slider bar +double g_fCurrentCLK6502 = CLK_6502_NTSC; // Affected by Config dialog's speed slider bar static double g_fMHz = 1.0; // Affected by Config dialog's speed slider bar int g_nCpuCyclesFeedback = 0; @@ -345,6 +345,7 @@ static void ContinueExecution(void) // + const UINT dwClksPerFrame = NTSC_GetCyclesPerFrame(); if (g_dwCyclesThisFrame >= dwClksPerFrame) { g_dwCyclesThisFrame -= dwClksPerFrame; @@ -376,14 +377,21 @@ void SingleStep(bool bReinit) //=========================================================================== +double Get6502BaseClock(void) +{ + return (GetVideoRefreshRate() == VR_50HZ) ? CLK_6502_PAL : CLK_6502_NTSC; +} + void SetCurrentCLK6502(void) { static DWORD dwPrevSpeed = (DWORD) -1; + static VideoRefreshRate_e prevVideoRefreshRate = VR_NONE; - if(dwPrevSpeed == g_dwSpeed) + if (dwPrevSpeed == g_dwSpeed && GetVideoRefreshRate() == prevVideoRefreshRate) return; dwPrevSpeed = g_dwSpeed; + prevVideoRefreshRate = GetVideoRefreshRate(); // SPEED_MIN = 0 = 0.50 MHz // SPEED_NORMAL = 10 = 1.00 MHz @@ -396,7 +404,7 @@ void SetCurrentCLK6502(void) else g_fMHz = (double)g_dwSpeed / 10.0; - g_fCurrentCLK6502 = CLK_6502 * g_fMHz; + g_fCurrentCLK6502 = Get6502BaseClock() * g_fMHz; // // Now re-init modules that are dependent on /g_fCurrentCLK6502/ @@ -622,17 +630,15 @@ void LoadConfiguration(void) } REGLOAD(TEXT(REGVALUE_EMULATION_SPEED) ,&g_dwSpeed); + Config_Load_Video(); + SetCurrentCLK6502(); // Pre: g_dwSpeed && Config_Load_Video()->SetVideoRefreshRate() DWORD dwEnhanceDisk; REGLOAD(TEXT(REGVALUE_ENHANCE_DISK_SPEED), &dwEnhanceDisk); sg_Disk2Card.SetEnhanceDisk(dwEnhanceDisk ? true : false); - Config_Load_Video(); - REGLOAD(TEXT("Uthernet Active") ,(DWORD *)&tfe_enabled); - SetCurrentCLK6502(); - // DWORD dwTmp; @@ -1181,6 +1187,7 @@ int APIENTRY WinMain(HINSTANCE passinstance, HINSTANCE, LPSTR lpCmdLine, int) int newVideoType = -1; int newVideoStyleEnableMask = 0; int newVideoStyleDisableMask = 0; + VideoRefreshRate_e newVideoRefreshRate = VR_NONE; LPSTR szScreenshotFilename = NULL; while (*lpCmdLine) @@ -1427,6 +1434,14 @@ int APIENTRY WinMain(HINSTANCE passinstance, HINSTANCE, LPSTR lpCmdLine, int) szScreenshotFilename = GetCurrArg(lpNextArg); lpNextArg = GetNextArg(lpNextArg); } + else if (_stricmp(lpCmdLine, "-50hz") == 0) // (case-insensitive) + { + newVideoRefreshRate = VR_50HZ; + } + else if (_stricmp(lpCmdLine, "-60hz") == 0) // (case-insensitive) + { + newVideoRefreshRate = VR_60HZ; + } else // unsupported { LogFileOutput("Unsupported arg: %s\n", lpCmdLine); @@ -1537,9 +1552,19 @@ int APIENTRY WinMain(HINSTANCE passinstance, HINSTANCE, LPSTR lpCmdLine, int) LogFileOutput("Main: LoadConfiguration()\n"); if (newVideoType >= 0) + { SetVideoType( (VideoType_e)newVideoType ); + newVideoType = -1; // Don't reapply after a restart + } SetVideoStyle( (VideoStyle_e) ((GetVideoStyle() | newVideoStyleEnableMask) & ~newVideoStyleDisableMask) ); + if (newVideoRefreshRate != VR_NONE) + { + SetVideoRefreshRate(newVideoRefreshRate); + newVideoRefreshRate = VR_NONE; // Don't reapply after a restart + SetCurrentCLK6502(); + } + // Apply the memory expansion switches after loading the Apple II machine type #ifdef RAMWORKS if (uRamWorksExPages) diff --git a/source/Applewin.h b/source/Applewin.h index 1d3d950f..c1c5899a 100644 --- a/source/Applewin.h +++ b/source/Applewin.h @@ -6,7 +6,6 @@ void LogFileTimeUntilFirstKeyReadReset(void); void LogFileTimeUntilFirstKeyRead(void); -void SetCurrentCLK6502(); bool SetCurrentImageDir(const char* pszImageDir); extern const UINT16* GetOldAppleWinVersion(void); @@ -18,6 +17,9 @@ extern eApple2Type g_Apple2Type; eApple2Type GetApple2Type(void); void SetApple2Type(eApple2Type type); +double Get6502BaseClock(void); +void SetCurrentCLK6502(void); + void SingleStep(bool bReinit); extern bool g_bFullSpeed; diff --git a/source/Common.h b/source/Common.h index f72fbfa4..6cab9950 100644 --- a/source/Common.h +++ b/source/Common.h @@ -1,19 +1,11 @@ #pragma once -const double _M14 = (157500000.0 / 11.0); // 14.3181818... * 10^6 -const double CLK_6502 = ((_M14 * 65.0) / 912.0); // 65 cycles per 912 14M clocks +const double _14M_NTSC = (157500000.0 / 11.0); // 14.3181818... * 10^6 +const double _14M_PAL = 14.25045e6; // UTAIIe:3-17 +const double CLK_6502_NTSC = ((_14M_NTSC * 65.0) / 912.0); // 65 cycles per 912 14M clocks +const double CLK_6502_PAL = _14M_PAL / 14.0; //const double CLK_6502 = 23 * 44100; // 1014300 -// The effective Z-80 clock rate is 2.041MHz -// See: http://www.apple2info.net/hardware/softcard/SC-SWHW_a2in.pdf -const double CLK_Z80 = (CLK_6502 * 2); - -// TODO: Clean up from Common.h, Video.cpp, and NTSC.h !!! -const UINT uCyclesPerLine = 65; // 25 cycles of HBL & 40 cycles of HBL' -const UINT uVisibleLinesPerFrame = 64*3; // 192 -const UINT uLinesPerFrame = 262; // 64 in each third of the screen & 70 in VBL -const DWORD dwClksPerFrame = uCyclesPerLine * uLinesPerFrame; // 17030 - #define NUM_SLOTS 8 #define MAX(a,b) (((a) > (b)) ? (a) : (b)) @@ -107,6 +99,7 @@ enum AppMode_e #define REGVALUE_VIDEO_STYLE "Video Style" // GH#616: Added at 1.28.2 #define REGVALUE_VIDEO_HALF_SCAN_LINES "Half Scan Lines" // GH#616: Deprecated from 1.28.2 #define REGVALUE_VIDEO_MONO_COLOR "Monochrome Color" +#define REGVALUE_VIDEO_REFRESH_RATE "Video Refresh Rate" #define REGVALUE_SERIAL_PORT_NAME "Serial Port Name" #define REGVALUE_ENHANCE_DISK_SPEED "Enhance Disk Speed" #define REGVALUE_CUSTOM_SPEED "Custom Speed" diff --git a/source/Configuration/Config.h b/source/Configuration/Config.h index e60a3285..909f0d21 100644 --- a/source/Configuration/Config.h +++ b/source/Configuration/Config.h @@ -4,6 +4,7 @@ #include "../CPU.h" #include "../DiskImage.h" // Disk_Status_e #include "../Harddisk.h" // HD_CardIsEnabled() +#include "../Video.h" // VideoRefreshRate_e, GetVideoRefreshRate() class CConfigNeedingRestart { @@ -11,7 +12,8 @@ public: CConfigNeedingRestart(UINT bEnableTheFreezesF8Rom = false) : m_Apple2Type( GetApple2Type() ), m_CpuType( GetMainCpu() ), - m_uSaveLoadStateMsg(0) + m_uSaveLoadStateMsg(0), + m_videoRefreshRate( GetVideoRefreshRate() ) { m_bEnableHDD = HD_CardIsEnabled(); m_bEnableTheFreezesF8Rom = bEnableTheFreezesF8Rom; @@ -29,17 +31,19 @@ public: m_bEnableHDD = other.m_bEnableHDD; m_bEnableTheFreezesF8Rom = other.m_bEnableTheFreezesF8Rom; m_uSaveLoadStateMsg = other.m_uSaveLoadStateMsg; + m_videoRefreshRate = other.m_videoRefreshRate; return *this; } bool operator== (const CConfigNeedingRestart& other) const { return m_Apple2Type == other.m_Apple2Type && - m_CpuType == other.m_CpuType && - memcmp(m_Slot, other.m_Slot, sizeof(m_Slot)) == 0 && - m_bEnableHDD == other.m_bEnableHDD && - m_bEnableTheFreezesF8Rom == other.m_bEnableTheFreezesF8Rom && - m_uSaveLoadStateMsg == other.m_uSaveLoadStateMsg; + m_CpuType == other.m_CpuType && + memcmp(m_Slot, other.m_Slot, sizeof(m_Slot)) == 0 && + m_bEnableHDD == other.m_bEnableHDD && + m_bEnableTheFreezesF8Rom == other.m_bEnableTheFreezesF8Rom && + m_uSaveLoadStateMsg == other.m_uSaveLoadStateMsg && + m_videoRefreshRate == other.m_videoRefreshRate; } bool operator!= (const CConfigNeedingRestart& other) const @@ -54,4 +58,5 @@ public: bool m_bEnableHDD; UINT m_bEnableTheFreezesF8Rom; UINT m_uSaveLoadStateMsg; + VideoRefreshRate_e m_videoRefreshRate; }; diff --git a/source/Configuration/PageConfig.cpp b/source/Configuration/PageConfig.cpp index ca4b5faf..7d548589 100644 --- a/source/Configuration/PageConfig.cpp +++ b/source/Configuration/PageConfig.cpp @@ -121,6 +121,7 @@ BOOL CPageConfig::DlgProcInternal(HWND hWnd, UINT message, WPARAM wparam, LPARAM case IDC_CHECK_HALF_SCAN_LINES: case IDC_CHECK_VERTICAL_BLEND: case IDC_CHECK_FS_SHOW_SUBUNIT_STATUS: + case IDC_CHECK_50HZ_VIDEO: // Checked in DlgOK() break; @@ -205,6 +206,8 @@ BOOL CPageConfig::DlgProcInternal(HWND hWnd, UINT message, WPARAM wparam, LPARAM m_PropertySheetHelper.FillComboBox(hWnd,IDC_SERIALPORT, sg_SSC.GetSerialPortChoices(), sg_SSC.GetSerialPort()); EnableWindow(GetDlgItem(hWnd, IDC_SERIALPORT), !sg_SSC.IsActive() ? TRUE : FALSE); + CheckDlgButton(hWnd, IDC_CHECK_50HZ_VIDEO, (GetVideoRefreshRate() == VR_50HZ) ? BST_CHECKED : BST_UNCHECKED); + SendDlgItemMessage(hWnd,IDC_SLIDER_CPU_SPEED,TBM_SETRANGE,1,MAKELONG(0,40)); SendDlgItemMessage(hWnd,IDC_SLIDER_CPU_SPEED,TBM_SETPAGESIZE,0,5); SendDlgItemMessage(hWnd,IDC_SLIDER_CPU_SPEED,TBM_SETTICFREQ,10,0); @@ -286,6 +289,13 @@ void CPageConfig::DlgOK(HWND hWnd) bVideoReinit = true; } + const bool isNewVideoRate50Hz = IsDlgButtonChecked(hWnd, IDC_CHECK_50HZ_VIDEO) != 0; + const bool isCurrentVideoRate50Hz = GetVideoRefreshRate() == VR_50HZ; + if (isCurrentVideoRate50Hz != isNewVideoRate50Hz) + { + m_PropertySheetHelper.GetConfigNew().m_videoRefreshRate = isNewVideoRate50Hz ? VR_50HZ : VR_60HZ; + } + if (bVideoReinit) { Config_Save_Video(); diff --git a/source/Configuration/PropertySheetHelper.cpp b/source/Configuration/PropertySheetHelper.cpp index 37c79443..94967fc4 100644 --- a/source/Configuration/PropertySheetHelper.cpp +++ b/source/Configuration/PropertySheetHelper.cpp @@ -407,6 +407,11 @@ void CPropertySheetHelper::ApplyNewConfig(const CConfigNeedingRestart& ConfigNew { REGSAVE(TEXT(REGVALUE_THE_FREEZES_F8_ROM), ConfigNew.m_bEnableTheFreezesF8Rom); } + + if (CONFIG_CHANGED_LOCAL(m_videoRefreshRate)) + { + REGSAVE(TEXT(REGVALUE_VIDEO_REFRESH_RATE), ConfigNew.m_videoRefreshRate); + } } void CPropertySheetHelper::ApplyNewConfig(void) @@ -423,6 +428,7 @@ void CPropertySheetHelper::SaveCurrentConfig(void) m_ConfigOld.m_Slot[5] = g_Slot5; m_ConfigOld.m_bEnableHDD = HD_CardIsEnabled(); m_ConfigOld.m_bEnableTheFreezesF8Rom = sg_PropertySheet.GetTheFreezesF8Rom(); + m_ConfigOld.m_videoRefreshRate = GetVideoRefreshRate(); // Reset flags each time: m_ConfigOld.m_uSaveLoadStateMsg = 0; @@ -441,6 +447,7 @@ void CPropertySheetHelper::RestoreCurrentConfig(void) g_Slot5 = m_ConfigOld.m_Slot[5]; HD_SetEnabled(m_ConfigOld.m_bEnableHDD); sg_PropertySheet.SetTheFreezesF8Rom(m_ConfigOld.m_bEnableTheFreezesF8Rom); + SetVideoRefreshRate(m_ConfigOld.m_videoRefreshRate); } bool CPropertySheetHelper::IsOkToSaveLoadState(HWND hWnd, const bool bConfigChanged) @@ -491,6 +498,9 @@ bool CPropertySheetHelper::HardwareConfigChanged(HWND hWnd) if (CONFIG_CHANGED(m_CpuType)) strMsgMain += ". Emulated main CPU has changed\n"; + if (CONFIG_CHANGED(m_videoRefreshRate)) + strMsgMain += ". Video refresh rate has changed\n"; + if (CONFIG_CHANGED(m_Slot[4])) strMsgMain += GetSlot(4); diff --git a/source/Memory.cpp b/source/Memory.cpp index 2329d886..8c547203 100644 --- a/source/Memory.cpp +++ b/source/Memory.cpp @@ -2202,7 +2202,7 @@ bool MemLoadSnapshot(YamlLoadHelper& yamlLoadHelper, UINT unitVersion) SetLastRamWrite( yamlLoadHelper.LoadUint(SS_YAML_KEY_LASTRAMWRITE) ? TRUE : FALSE ); // NB. This is set later for II,II+ by slot-0 LC or Saturn } - if (unitVersion == 3) + if (unitVersion >= 3) { for (UINT i=0; i MB_SetSoundcardType() @@ -2312,7 +2313,7 @@ bool Phasor_LoadSnapshot(YamlLoadHelper& yamlLoadHelper, UINT slot, UINT version pMB++; } - AY8910_InitClock((int)(CLK_6502 * g_PhasorClockScaleFactor)); + AY8910_InitClock((int)(Get6502BaseClock() * g_PhasorClockScaleFactor)); // NB. g_SoundcardType & g_bPhasorEnable setup in MB_InitializeIO() -> MB_SetSoundcardType() diff --git a/source/NTSC.cpp b/source/NTSC.cpp index a7efccae..0d2eb052 100644 --- a/source/NTSC.cpp +++ b/source/NTSC.cpp @@ -134,7 +134,13 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // "There are exactly 17030 (65 x 262) 6502 cycles in every television scan of an American Apple." #define VIDEO_SCANNER_MAX_HORZ 65 // TODO: use Video.cpp: kHClocks #define VIDEO_SCANNER_MAX_VERT 262 // TODO: use Video.cpp: kNTSCScanLines - static const int VIDEO_SCANNER_6502_CYCLES = VIDEO_SCANNER_MAX_HORZ * VIDEO_SCANNER_MAX_VERT; + static const UINT VIDEO_SCANNER_6502_CYCLES = VIDEO_SCANNER_MAX_HORZ * VIDEO_SCANNER_MAX_VERT; + + #define VIDEO_SCANNER_MAX_VERT_PAL 312 + static const UINT VIDEO_SCANNER_6502_CYCLES_PAL = VIDEO_SCANNER_MAX_HORZ * VIDEO_SCANNER_MAX_VERT_PAL; + + static UINT g_videoScannerMaxVert = VIDEO_SCANNER_MAX_VERT; // default to NTSC + static UINT g_videoScanner6502Cycles = VIDEO_SCANNER_6502_CYCLES; // default to NTSC #define VIDEO_SCANNER_HORZ_COLORBURST_BEG 12 #define VIDEO_SCANNER_HORZ_COLORBURST_END 16 @@ -212,9 +218,9 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // Tables // Video scanner tables are now runtime-generated using UTAIIe logic - static unsigned short g_aClockVertOffsetsHGR[VIDEO_SCANNER_MAX_VERT]; - static unsigned short g_aClockVertOffsetsTXT[33]; - static unsigned short APPLE_IIP_HORZ_CLOCK_OFFSET[5][VIDEO_SCANNER_MAX_HORZ]; + static unsigned short g_aClockVertOffsetsHGR[VIDEO_SCANNER_MAX_VERT_PAL]; + static unsigned short g_aClockVertOffsetsTXT[VIDEO_SCANNER_MAX_VERT_PAL/8]; + static unsigned short APPLE_IIP_HORZ_CLOCK_OFFSET[5][VIDEO_SCANNER_MAX_HORZ]; // 5 = CEILING(312/64) = CEILING(262/64) static unsigned short APPLE_IIE_HORZ_CLOCK_OFFSET[5][VIDEO_SCANNER_MAX_HORZ]; #ifdef _DEBUG @@ -243,7 +249,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 0x0B80,0x0F80,0x1380,0x1780,0x1B80,0x1F80 }; - static unsigned short g_kClockVertOffsetsTXT[33] = + static unsigned short g_kClockVertOffsetsTXT[33] = // 33 = CEILING(262/8) { 0x0000,0x0080,0x0100,0x0180,0x0200,0x0280,0x0300,0x0380, 0x0000,0x0080,0x0100,0x0180,0x0200,0x0280,0x0300,0x0380, @@ -768,7 +774,7 @@ inline void updateVideoScannerHorzEOLSimple() { g_nVideoClockHorz = 0; - if (++g_nVideoClockVert == VIDEO_SCANNER_MAX_VERT) + if (++g_nVideoClockVert == g_videoScannerMaxVert) { g_nVideoClockVert = 0; @@ -807,7 +813,7 @@ inline void updateVideoScannerHorzEOL() g_nVideoClockHorz = 0; - if (++g_nVideoClockVert == VIDEO_SCANNER_MAX_VERT) + if (++g_nVideoClockVert == g_videoScannerMaxVert) { g_nVideoClockVert = 0; @@ -851,7 +857,7 @@ inline void updateVideoScannerHorzEOL_14M() g_nVideoClockHorz = 0; - if (++g_nVideoClockVert == VIDEO_SCANNER_MAX_VERT) + if (++g_nVideoClockVert == g_videoScannerMaxVert) { g_nVideoClockVert = 0; @@ -897,7 +903,7 @@ inline void updateVideoScannerHorzEOL() g_nVideoClockHorz = 0; - if (++g_nVideoClockVert == VIDEO_SCANNER_MAX_VERT) + if (++g_nVideoClockVert == g_videoScannerMaxVert) { g_nVideoClockVert = 0; @@ -1838,8 +1844,8 @@ uint32_t*NTSC_VideoGetChromaTable( bool bHueTypeMonochrome, bool bMonitorTypeCol //=========================================================================== void NTSC_VideoClockResync(const DWORD dwCyclesThisFrame) { - g_nVideoClockVert = (uint16_t) (dwCyclesThisFrame / VIDEO_SCANNER_MAX_HORZ) % VIDEO_SCANNER_MAX_VERT; - g_nVideoClockHorz = (uint16_t) (dwCyclesThisFrame % VIDEO_SCANNER_MAX_HORZ); + g_nVideoClockVert = (uint16_t)(dwCyclesThisFrame / VIDEO_SCANNER_MAX_HORZ) % g_videoScannerMaxVert; + g_nVideoClockHorz = (uint16_t)(dwCyclesThisFrame % VIDEO_SCANNER_MAX_HORZ); } //=========================================================================== @@ -1861,7 +1867,7 @@ uint16_t NTSC_VideoGetScannerAddress ( const ULONG uExecutedCycles ) g_nVideoClockHorz += VIDEO_SCANNER_MAX_HORZ; g_nVideoClockVert -= 1; if ((SHORT)g_nVideoClockVert < 0) - g_nVideoClockVert = VIDEO_SCANNER_MAX_VERT-1; + g_nVideoClockVert = g_videoScannerMaxVert-1; } uint16_t addr; @@ -2154,8 +2160,8 @@ void NTSC_VideoInit( uint8_t* pFramebuffer ) // wsVideoInit //=========================================================================== void NTSC_VideoReinitialize( DWORD cyclesThisFrame, bool bInitVideoScannerAddress ) { - _ASSERT(cyclesThisFrame < VIDEO_SCANNER_6502_CYCLES); - if (cyclesThisFrame >= VIDEO_SCANNER_6502_CYCLES) cyclesThisFrame = 0; // error + _ASSERT(cyclesThisFrame < g_videoScanner6502Cycles); + if (cyclesThisFrame >= g_videoScanner6502Cycles) cyclesThisFrame = 0; // error g_nVideoClockVert = (uint16_t) (cyclesThisFrame / VIDEO_SCANNER_MAX_HORZ); g_nVideoClockHorz = cyclesThisFrame % VIDEO_SCANNER_MAX_HORZ; @@ -2193,7 +2199,7 @@ void NTSC_VideoInitChroma() //=========================================================================== -// Pre: cyclesLeftToUpdate = [0...VIDEO_SCANNER_6502_CYCLES] +// Pre: cyclesLeftToUpdate = [0...g_videoScanner6502Cycles] // . 2-14: After one emulated 6502/65C02 opcode (optionally with IRQ) // . ~1000: After 1ms of Z80 emulation // . 17030: From NTSC_VideoRedrawWholeScreen() @@ -2208,7 +2214,7 @@ static void VideoUpdateCycles( int cyclesLeftToUpdate ) g_pFuncUpdateGraphicsScreen(cycles); // lines [currV...159] cyclesLeftToUpdate -= cycles; - const int cyclesFromLine160ToLine261 = VIDEO_SCANNER_6502_CYCLES - (VIDEO_SCANNER_MAX_HORZ * VIDEO_SCANNER_Y_MIXED); + const int cyclesFromLine160ToLine261 = g_videoScanner6502Cycles - (VIDEO_SCANNER_MAX_HORZ * VIDEO_SCANNER_Y_MIXED); cycles = cyclesLeftToUpdate < cyclesFromLine160ToLine261 ? cyclesLeftToUpdate : cyclesFromLine160ToLine261; g_pFuncUpdateGraphicsScreen(cycles); // lines [160..191..261] cyclesLeftToUpdate -= cycles; @@ -2217,7 +2223,7 @@ static void VideoUpdateCycles( int cyclesLeftToUpdate ) } else { - const int cyclesToLine262 = VIDEO_SCANNER_MAX_HORZ * (VIDEO_SCANNER_MAX_VERT - g_nVideoClockVert - 1) + cyclesToEndOfLine; + const int cyclesToLine262 = VIDEO_SCANNER_MAX_HORZ * (g_videoScannerMaxVert - g_nVideoClockVert - 1) + cyclesToEndOfLine; int cycles = cyclesLeftToUpdate < cyclesToLine262 ? cyclesLeftToUpdate : cyclesToLine262; g_pFuncUpdateGraphicsScreen(cycles); // lines [currV...261] cyclesLeftToUpdate -= cycles; @@ -2235,9 +2241,9 @@ static void VideoUpdateCycles( int cyclesLeftToUpdate ) } //=========================================================================== -void NTSC_VideoUpdateCycles( long cycles6502 ) +void NTSC_VideoUpdateCycles( UINT cycles6502 ) { - _ASSERT(cycles6502 && cycles6502 < VIDEO_SCANNER_6502_CYCLES); // Use NTSC_VideoRedrawWholeScreen() instead + _ASSERT(cycles6502 && cycles6502 < g_videoScanner6502Cycles); // Use NTSC_VideoRedrawWholeScreen() instead if (g_bDelayVideoMode) { @@ -2269,7 +2275,7 @@ void NTSC_VideoRedrawWholeScreen( void ) g_nVideoClockHorz = 0; updateVideoScannerAddress(); - VideoUpdateCycles(VIDEO_SCANNER_6502_CYCLES); + VideoUpdateCycles(g_videoScanner6502Cycles); VideoUpdateCycles(horz); // Finally update to get to correct H-pos @@ -2324,19 +2330,37 @@ static void CheckVideoTables( void ) CheckVideoTables2(A2TYPE_APPLE2E, VF_TEXT); } +static bool IsNTSC(void) +{ + return g_videoScannerMaxVert == VIDEO_SCANNER_MAX_VERT; +} + static void GenerateVideoTables( void ) { eApple2Type currentApple2Type = GetApple2Type(); + uint32_t currentVideoMode = g_uVideoMode; + int currentHiresPage = g_nHiresPage; + int currentTextPage = g_nTextPage; + + g_nHiresPage = g_nTextPage = 1; // // g_aClockVertOffsetsHGR[] // g_uVideoMode = VF_HIRES; - for (UINT i=0, cycle=VIDEO_SCANNER_HORZ_START; i= 4) + { + VideoRefreshRate_e rate = (VideoRefreshRate_e)yamlLoadHelper.LoadUint(SS_YAML_KEY_VIDEO_REFRESH_RATE); + SetVideoRefreshRate(rate); // Trashes: g_dwCyclesThisFrame + SetCurrentCLK6502(); + } + + g_nAltCharSetOffset = yamlLoadHelper.LoadBool(SS_YAML_KEY_ALT_CHARSET) ? 256 : 0; + g_uVideoMode = yamlLoadHelper.LoadUint(SS_YAML_KEY_VIDEO_MODE); + g_dwCyclesThisFrame = yamlLoadHelper.LoadUint(SS_YAML_KEY_CYCLES_THIS_FRAME); yamlLoadHelper.PopMap(); } @@ -777,7 +790,7 @@ WORD VideoGetScannerAddress(DWORD nCycles, VideoScanner_e videoScannerAddr /*= V // const int kScanLines = g_bVideoScannerNTSC ? kNTSCScanLines : kPALScanLines; const int kScanCycles = kScanLines * kHClocks; - _ASSERT(nCycles < kScanCycles); + _ASSERT(nCycles < (UINT)kScanCycles); nCycles %= kScanCycles; // calculate horizontal scanning state @@ -795,7 +808,7 @@ WORD VideoGetScannerAddress(DWORD nCycles, VideoScanner_e videoScannerAddr /*= V int h_4 = (nHState >> 4) & 1; int h_5 = (nHState >> 5) & 1; - // calculate vertical scanning state + // calculate vertical scanning state (UTAIIe:3-15,T3.2) // int nVLine = nCycles / kHClocks; // which vertical scanning line int nVState = kVLine0State + nVLine; // V state bits @@ -868,7 +881,7 @@ WORD VideoGetScannerAddress(DWORD nCycles, VideoScanner_e videoScannerAddr /*= V nAddressP |= p2b << 11; // a11 } - // VBL' = v_4' | v_3' = (v_4 & v_3)' (UTAIIe:5-10,#3) + // VBL' = v_4' | v_3' = (v_4 & v_3)' (UTAIIe:5-10,#3), (UTAIIe:3-15,T3.2) if (videoScannerAddr == VS_PartialAddrH) return nAddressH; @@ -1231,6 +1244,10 @@ void Config_Load_Video() REGLOAD(TEXT(REGVALUE_VIDEO_STYLE) ,(DWORD*)&g_eVideoStyle); REGLOAD(TEXT(REGVALUE_VIDEO_MONO_COLOR),&g_nMonochromeRGB); + DWORD rate = VR_60HZ; + REGLOAD(TEXT(REGVALUE_VIDEO_REFRESH_RATE), &rate); + SetVideoRefreshRate((VideoRefreshRate_e)rate); + // const UINT16* pOldVersion = GetOldAppleWinVersion(); @@ -1275,6 +1292,7 @@ void Config_Save_Video() REGSAVE(TEXT(REGVALUE_VIDEO_MODE) ,g_eVideoType); REGSAVE(TEXT(REGVALUE_VIDEO_STYLE) ,g_eVideoStyle); REGSAVE(TEXT(REGVALUE_VIDEO_MONO_COLOR),g_nMonochromeRGB); + REGSAVE(TEXT(REGVALUE_VIDEO_REFRESH_RATE), GetVideoRefreshRate()); } //=========================================================================== @@ -1305,6 +1323,22 @@ bool IsVideoStyle(VideoStyle_e mask) return (g_eVideoStyle & mask) != 0; } +//=========================================================================== + +VideoRefreshRate_e GetVideoRefreshRate(void) +{ + return (g_bVideoScannerNTSC == false) ? VR_50HZ : VR_60HZ; +} + +void SetVideoRefreshRate(VideoRefreshRate_e rate) +{ + if (rate != VR_50HZ) + rate = VR_60HZ; + + g_bVideoScannerNTSC = (rate == VR_60HZ); + NTSC_SetRefreshRate(rate); +} + //=========================================================================== static void videoCreateDIBSection() { diff --git a/source/Video.h b/source/Video.h index 2a7e2253..693e7dc7 100644 --- a/source/Video.h +++ b/source/Video.h @@ -29,6 +29,13 @@ // VS_TEXT_OPTIMIZED=4, }; + enum VideoRefreshRate_e + { + VR_NONE, + VR_50HZ, + VR_60HZ + }; + enum VideoFlag_e { VF_80COL = 0x00000001, @@ -193,7 +200,7 @@ bool VideoGetSWTEXT(void); bool VideoGetSWAltCharSet(void); void VideoSaveSnapshot(class YamlSaveHelper& yamlSaveHelper); -void VideoLoadSnapshot(class YamlLoadHelper& yamlLoadHelper); +void VideoLoadSnapshot(class YamlLoadHelper& yamlLoadHelper, UINT version); extern bool g_bDisplayPrintScreenFileName; extern bool g_bShowPrintScreenWarningDialog; @@ -226,3 +233,6 @@ void SetVideoType(VideoType_e newVideoType); VideoStyle_e GetVideoStyle(void); void SetVideoStyle(VideoStyle_e newVideoStyle); bool IsVideoStyle(VideoStyle_e mask); + +VideoRefreshRate_e GetVideoRefreshRate(void); +void SetVideoRefreshRate(VideoRefreshRate_e rate); diff --git a/source/Z80VICE/z80.cpp b/source/Z80VICE/z80.cpp index c4b1ba5c..27563cb4 100644 --- a/source/Z80VICE/z80.cpp +++ b/source/Z80VICE/z80.cpp @@ -5510,7 +5510,10 @@ static void opcode_fd(BYTE ip1, BYTE ip2, BYTE ip3, WORD ip12, WORD ip23) /* Z80 mainloop. */ -static const double uZ80ClockMultiplier = CLK_Z80 / CLK_6502; +// The effective Z-80 clock rate is 2.041MHz +// See: http://www.apple2info.net/hardware/softcard/SC-SWHW_a2in.pdf +static const double uZ80ClockMultiplier = 2; + inline static ULONG ConvertZ80TStatesTo6502Cycles(UINT uTStates) { return (uTStates < 0) ? 0 : (ULONG) ((double)uTStates / uZ80ClockMultiplier); From f0f63f934fcdfb1121ef0a7c95c9eb96b5ae62b4 Mon Sep 17 00:00:00 2001 From: tomcw Date: Fri, 28 Jun 2019 21:45:43 +0100 Subject: [PATCH 12/21] Tweak PAL 6502 base clock --- bin/History.txt | 2 +- source/Common.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/History.txt b/bin/History.txt index fdf84d96..d4dbe89b 100644 --- a/bin/History.txt +++ b/bin/History.txt @@ -10,7 +10,7 @@ Tom Charlesworth 1.28.8.0 - 28 Jun 2019 ---------------------- -. [Change #648] Support 50Hz(PAL) video refresh rate and implicitly PAL 1.018MHz. +. [Change #648] Support 50Hz(PAL) video refresh rate and implicitly PAL 1.016MHz. - NB. TV video modes still use NTSC rendering. . [Bug #656] Fix for PAGE1/2 ($C054/55) not having a 1 cycle delay. diff --git a/source/Common.h b/source/Common.h index 6cab9950..05662a8a 100644 --- a/source/Common.h +++ b/source/Common.h @@ -2,8 +2,8 @@ const double _14M_NTSC = (157500000.0 / 11.0); // 14.3181818... * 10^6 const double _14M_PAL = 14.25045e6; // UTAIIe:3-17 -const double CLK_6502_NTSC = ((_14M_NTSC * 65.0) / 912.0); // 65 cycles per 912 14M clocks -const double CLK_6502_PAL = _14M_PAL / 14.0; +const double CLK_6502_NTSC = (_14M_NTSC * 65.0) / (65.0*14.0+2.0); // 65 cycles per 912 14M clocks +const double CLK_6502_PAL = (_14M_PAL * 65.0) / (65.0*14.0+2.0); //const double CLK_6502 = 23 * 44100; // 1014300 #define NUM_SLOTS 8 From 73ce127eefee8e1f81002534137745ca8aca8bec Mon Sep 17 00:00:00 2001 From: tomcw Date: Sat, 29 Jun 2019 17:05:07 +0100 Subject: [PATCH 13/21] Removed some old commented out code --- source/Video.cpp | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/source/Video.cpp b/source/Video.cpp index fc489a9c..a5229d6d 100644 --- a/source/Video.cpp +++ b/source/Video.cpp @@ -542,21 +542,7 @@ void VideoRedrawScreenDuringFullSpeed(DWORD dwCyclesThisFrame, bool bInit /*=fal void VideoRedrawScreenAfterFullSpeed(DWORD dwCyclesThisFrame) { -#if 1 NTSC_VideoClockResync(dwCyclesThisFrame); -#else - if (g_bVideoScannerNTSC) - { - NTSC_VideoClockResync(dwCyclesThisFrame); - } - else // PAL - { - _ASSERT(0); - g_nVideoClockVert = (uint16_t) (dwCyclesThisFrame / kHClocks) % kPALScanLines; - g_nVideoClockHorz = (uint16_t) (dwCyclesThisFrame % kHClocks); - } -#endif - VideoRedrawScreen(); // Better (no flicker) than using: NTSC_VideoReinitialize() or VideoReinitialize() } @@ -894,6 +880,7 @@ WORD VideoGetScannerAddress(DWORD nCycles, VideoScanner_e videoScannerAddr /*= V //=========================================================================== +// TODO: Consider replacing simply with: return g_nVideoClockVert < kVDisplayableScanLines bool VideoGetVblBar(const DWORD uExecutedCycles) { // get video scanner position From 4bc75093b8571a56adaa6028cdeb4adc706e184a Mon Sep 17 00:00:00 2001 From: TomCh Date: Fri, 5 Jul 2019 23:01:19 +0100 Subject: [PATCH 14/21] Support (read-only) WOZ1/WOZ2 images (#544) (PR #653) Supports: - all "woz test images" v1.3 (WOZ1, WOZ2) are working, except 3.5" - additionally: Frogger (spiradisc), Choplifter (not Enhanced //e!), Lode Runner, Marble Madness, Skyfox. - woz images can be .gz or .zip compressed (ie. same as other supported images) - save-state Limitations: - read-only, so WOZ images are forced to be write-protected . as a result, games that need r/w images won't work (Stickybear Town Builder, Wizardry) - 5.25" only (not 3.5") --- source/Applewin.cpp | 4 +- source/Debugger/Debug.cpp | 15 +- source/Disk.cpp | 674 +++++++++++++++++++++++++++------- source/Disk.h | 69 +++- source/DiskDefs.h | 4 +- source/DiskFormatTrack.cpp | 5 +- source/DiskImage.cpp | 63 +++- source/DiskImage.h | 9 +- source/DiskImageHelper.cpp | 380 +++++++++++++++---- source/DiskImageHelper.h | 120 +++++- source/Keyboard.cpp | 2 +- source/Memory.cpp | 2 + source/SaveState_Structs_v1.h | 2 +- source/YamlHelper.cpp | 40 +- source/YamlHelper.h | 6 +- 15 files changed, 1133 insertions(+), 262 deletions(-) diff --git a/source/Applewin.cpp b/source/Applewin.cpp index 17270177..9b9cd088 100644 --- a/source/Applewin.cpp +++ b/source/Applewin.cpp @@ -138,6 +138,7 @@ void LogFileTimeUntilFirstKeyReadReset(void) // Log the time from emulation restart/reboot until the first key read: BIT $C000 // . AZTEC.DSK (DOS 3.3) does prior LDY $C000 reads, but the BIT $C000 is at the "Press any key" message // . Phasor1.dsk / ProDOS 1.1.1: PC=E797: B1 50: LDA ($50),Y / "Select an Option:" message +// . Rescue Raiders v1.3,v1.5: PC=895: LDA $C000 / boot to intro void LogFileTimeUntilFirstKeyRead(void) { if (!g_fh || bLogKeyReadDone) @@ -145,6 +146,7 @@ void LogFileTimeUntilFirstKeyRead(void) if ( (mem[regs.pc-3] != 0x2C) // AZTEC: bit $c000 && !((regs.pc-2) == 0xE797 && mem[regs.pc-2] == 0xB1 && mem[regs.pc-1] == 0x50) // Phasor1: lda ($50),y + && !((regs.pc-3) == 0x0895 && mem[regs.pc-3] == 0xAD) // Rescue Raiders v1.3,v1.5: lda $c000 ) return; @@ -1638,7 +1640,7 @@ int APIENTRY WinMain(HINSTANCE passinstance, HINSTANCE, LPSTR lpCmdLine, int) } // Need to test if it's safe to call ResetMachineState(). In the meantime, just call DiskReset(): - sg_Disk2Card.Reset(); // Switch from a booting A][+ to a non-autostart A][, so need to turn off floppy motor + sg_Disk2Card.Reset(true); // Switch from a booting A][+ to a non-autostart A][, so need to turn off floppy motor LogFileOutput("Main: DiskReset()\n"); HD_Reset(); // GH#515 LogFileOutput("Main: HDDReset()\n"); diff --git a/source/Debugger/Debug.cpp b/source/Debugger/Debug.cpp index c29e3236..c9ff3689 100644 --- a/source/Debugger/Debug.cpp +++ b/source/Debugger/Debug.cpp @@ -3727,15 +3727,16 @@ Update_t CmdDisk ( int nArgs) if (nArgs > 2) goto _Help; - int drive = sg_Disk2Card.GetCurrentDrive() + 1; char buffer[200] = ""; - ConsoleBufferPushFormat(buffer, "D%d at T$%X (%d), phase $%X, offset $%X, %s", - drive, - sg_Disk2Card.GetCurrentTrack(), - sg_Disk2Card.GetCurrentTrack(), - sg_Disk2Card.GetCurrentPhase(), + ConsoleBufferPushFormat(buffer, "D%d at T$%s, phase $%s, offset $%X, mask $%02X, extraCycles %.2f, %s", + sg_Disk2Card.GetCurrentDrive() + 1, + sg_Disk2Card.GetCurrentTrackString().c_str(), + sg_Disk2Card.GetCurrentPhaseString().c_str(), sg_Disk2Card.GetCurrentOffset(), - sg_Disk2Card.GetCurrentState()); + sg_Disk2Card.GetCurrentLSSBitMask(), + sg_Disk2Card.GetCurrentExtraCycles(), + sg_Disk2Card.GetCurrentState() + ); return ConsoleUpdate(); } diff --git a/source/Disk.cpp b/source/Disk.cpp index c7ea7073..33b0355d 100644 --- a/source/Disk.cpp +++ b/source/Disk.cpp @@ -56,18 +56,17 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Disk2InterfaceCard::Disk2InterfaceCard(void) { - m_currDrive = 0; + ResetSwitches(); + m_floppyLatch = 0; - m_floppyMotorOn = 0; - m_floppyLoadMode = 0; - m_floppyWriteMode = 0; - m_phases = 0; m_saveDiskImage = true; // Save the DiskImage name to Registry m_slot = 0; m_diskLastCycle = 0; m_diskLastReadLatchCycle = 0; m_enhanceDisk = true; + ResetLogicStateSequencer(); + // Debug: #if LOG_DISK_NIBBLES_USE_RUNTIME_VAR m_bLogDisk_NibblesRW = false; @@ -82,11 +81,40 @@ bool Disk2InterfaceCard::GetEnhanceDisk(void) { return m_enhanceDisk; } void Disk2InterfaceCard::SetEnhanceDisk(bool bEnhanceDisk) { m_enhanceDisk = bEnhanceDisk; } int Disk2InterfaceCard::GetCurrentDrive(void) { return m_currDrive; } -int Disk2InterfaceCard::GetCurrentTrack(void) { return m_floppyDrive[m_currDrive].m_track; } -int Disk2InterfaceCard::GetCurrentPhase(void) { return m_floppyDrive[m_currDrive].m_phase; } +int Disk2InterfaceCard::GetCurrentTrack(void) { return ImagePhaseToTrack(m_floppyDrive[m_currDrive].m_disk.m_imagehandle, m_floppyDrive[m_currDrive].m_phasePrecise, false); } +float Disk2InterfaceCard::GetCurrentPhase(void) { return m_floppyDrive[m_currDrive].m_phasePrecise; } int Disk2InterfaceCard::GetCurrentOffset(void) { return m_floppyDrive[m_currDrive].m_disk.m_byte; } -int Disk2InterfaceCard::GetTrack(const int drive) { return m_floppyDrive[drive].m_track; } +BYTE Disk2InterfaceCard::GetCurrentLSSBitMask(void) { return m_floppyDrive[m_currDrive].m_disk.m_bitMask; } +double Disk2InterfaceCard::GetCurrentExtraCycles(void) { return m_floppyDrive[m_currDrive].m_disk.m_extraCycles; } +int Disk2InterfaceCard::GetTrack(const int drive) { return ImagePhaseToTrack(m_floppyDrive[drive].m_disk.m_imagehandle, m_floppyDrive[m_currDrive].m_phasePrecise, false); } +std::string Disk2InterfaceCard::GetCurrentTrackString(void) +{ + const UINT trackInt = (UINT)(m_floppyDrive[m_currDrive].m_phasePrecise / 2); + const float trackFrac = (m_floppyDrive[m_currDrive].m_phasePrecise / 2) - (float)trackInt; + + char szInt[8] = ""; + sprintf(szInt, "%02X", trackInt); // "$NN" + + char szFrac[8] = ""; + sprintf(szFrac, "%.02f", trackFrac); // "0.nn" + + return std::string(szInt) + std::string(szFrac+1); +} + +std::string Disk2InterfaceCard::GetCurrentPhaseString(void) +{ + const UINT phaseInt = (UINT)(m_floppyDrive[m_currDrive].m_phasePrecise); + const float phaseFrac = m_floppyDrive[m_currDrive].m_phasePrecise - (float)phaseInt; + + char szInt[8] = ""; + sprintf(szInt, "%02X", phaseInt); // "$NN" + + char szFrac[8] = ""; + sprintf(szFrac, "%.02f", phaseFrac); // "0.nn" + + return std::string(szInt) + std::string(szFrac+1); +} LPCTSTR Disk2InterfaceCard::GetCurrentState(void) { if (m_floppyDrive[m_currDrive].m_disk.m_imagehandle == NULL) @@ -175,7 +203,7 @@ void Disk2InterfaceCard::SaveLastDiskImage(const int drive) //=========================================================================== // Called by ControlMotor() & Enable() -void Disk2InterfaceCard::CheckSpinning(const ULONG nExecutedCycles) +void Disk2InterfaceCard::CheckSpinning(const ULONG uExecutedCycles) { DWORD modechange = (m_floppyMotorOn && !m_floppyDrive[m_currDrive].m_spinning); @@ -188,7 +216,7 @@ void Disk2InterfaceCard::CheckSpinning(const ULONG nExecutedCycles) if (modechange) { // Set m_diskLastCycle when motor changes: not spinning (ie. off for 1 sec) -> on - CpuCalcCycles(nExecutedCycles); + CpuCalcCycles(uExecutedCycles); m_diskLastCycle = g_nCumulativeCycles; } } @@ -237,7 +265,7 @@ void Disk2InterfaceCard::AllocTrack(const int drive) //=========================================================================== -void Disk2InterfaceCard::ReadTrack(const int drive) +void Disk2InterfaceCard::ReadTrack(const int drive, ULONG uExecutedCycles) { if (! IsDriveValid( drive )) return; @@ -245,8 +273,9 @@ void Disk2InterfaceCard::ReadTrack(const int drive) FloppyDrive* pDrive = &m_floppyDrive[ drive ]; FloppyDisk* pFloppy = &pDrive->m_disk; - if (pDrive->m_track >= ImageGetNumTracks(pFloppy->m_imagehandle)) + if (ImagePhaseToTrack(pFloppy->m_imagehandle, pDrive->m_phasePrecise, false) >= ImageGetNumTracks(pFloppy->m_imagehandle)) { + _ASSERT(0); // What can cause this? Add a comment to replace this assert. pFloppy->m_trackimagedata = false; return; } @@ -257,17 +286,45 @@ void Disk2InterfaceCard::ReadTrack(const int drive) if (pFloppy->m_trackimage && pFloppy->m_imagehandle) { #if LOG_DISK_TRACKS - LOG_DISK("track $%02X%s read\r\n", pDrive->m_track, (pDrive->m_phase & 1) ? ".5" : " "); + CpuCalcCycles(uExecutedCycles); + const ULONG cycleDelta = (ULONG)(g_nCumulativeCycles - pDrive->m_lastStepperCycle); + LOG_DISK("track $%s read (time since last stepper %.3fms)\r\n", GetCurrentTrackString().c_str(), ((float)cycleDelta) / (CLK_6502 / 1000.0)); #endif + const UINT32 currentPosition = pFloppy->m_byte; + const UINT32 currentTrackLength = pFloppy->m_nibbles; + ImageReadTrack( pFloppy->m_imagehandle, - pDrive->m_track, - pDrive->m_phase, + pDrive->m_phasePrecise, pFloppy->m_trackimage, &pFloppy->m_nibbles, + &pFloppy->m_bitCount, m_enhanceDisk); - pFloppy->m_byte = 0; + if (!ImageIsWOZ(pFloppy->m_imagehandle) || (currentTrackLength == 0)) + { + pFloppy->m_byte = 0; + } + else + { + _ASSERT(pFloppy->m_nibbles && pFloppy->m_bitCount); + if (pFloppy->m_nibbles == 0 || pFloppy->m_bitCount == 0) + { + pFloppy->m_nibbles = 1; + pFloppy->m_bitCount = 8; + } + + pFloppy->m_byte = (currentPosition * pFloppy->m_nibbles) / currentTrackLength; // Ref: WOZ-1.01 + + if (pFloppy->m_byte == (pFloppy->m_nibbles-1)) // Last nibble may not be complete, so advance by 1 nibble + pFloppy->m_byte = 0; + + pFloppy->m_bitOffset = pFloppy->m_byte*8; + pFloppy->m_bitMask = 1 << 7; + pFloppy->m_extraCycles = 0.0; + pDrive->m_headWindow = 0; + } + pFloppy->m_trackimagedata = (pFloppy->m_nibbles != 0); } } @@ -308,8 +365,11 @@ void Disk2InterfaceCard::WriteTrack(const int drive) FloppyDrive* pDrive = &m_floppyDrive[ drive ]; FloppyDisk* pFloppy = &pDrive->m_disk; - if (pDrive->m_track >= ImageGetNumTracks(pFloppy->m_imagehandle)) + if (ImagePhaseToTrack(pFloppy->m_imagehandle, pDrive->m_phasePrecise, false) >= ImageGetNumTracks(pFloppy->m_imagehandle)) + { + _ASSERT(0); // What can cause this? Add a comment to replace this assert. return; + } if (pFloppy->m_bWriteProtected) return; @@ -317,12 +377,11 @@ void Disk2InterfaceCard::WriteTrack(const int drive) if (pFloppy->m_trackimage && pFloppy->m_imagehandle) { #if LOG_DISK_TRACKS - LOG_DISK("track $%02X%s write\r\n", pDrive->m_track, (pDrive->m_phase & 0) ? ".5" : " "); // TODO: hard-coded to whole tracks - see below (nickw) + LOG_DISK("track $%s write\r\n", GetCurrentTrackString().c_str()); #endif ImageWriteTrack( pFloppy->m_imagehandle, - pDrive->m_track, - pDrive->m_phase, // TODO: this should never be used; it's the current phase (half-track), not that of the track to be written (nickw) + pDrive->m_phasePrecise, pFloppy->m_trackimage, pFloppy->m_nibbles); } @@ -359,7 +418,7 @@ void __stdcall Disk2InterfaceCard::ControlMotor(WORD, WORD address, BYTE, BYTE, m_floppyMotorOn = newState; // NB. Motor off doesn't reset the Command Decoder like reset. (UTAIIe figures 9.7 & 9.8 chip C2) - // - so it doesn't reset this state: m_floppyLoadMode, m_floppyWriteMode, m_phases + // - so it doesn't reset this state: m_floppyLoadMode, m_floppyWriteMode, m_magnetStates #if LOG_DISK_MOTOR LOG_DISK("motor %s\r\n", (m_floppyMotorOn) ? "on" : "off"); #endif @@ -388,68 +447,75 @@ void __stdcall Disk2InterfaceCard::ControlStepper(WORD, WORD address, BYTE, BYTE #endif } - int phase = (address >> 1) & 3; - int phase_bit = (1 << phase); + // update phases (magnet states) + { + const int phase = (address >> 1) & 3; + const int phase_bit = (1 << phase); -#if 1 - // update the magnet states - if (address & 1) - { - // phase on - m_phases |= phase_bit; - } - else - { - // phase off - m_phases &= ~phase_bit; + // update the magnet states + if (address & 1) + m_magnetStates |= phase_bit; // phase on + else + m_magnetStates &= ~phase_bit; // phase off } + CpuCalcCycles(uExecutedCycles); +#if LOG_DISK_PHASES + const ULONG cycleDelta = (ULONG)(g_nCumulativeCycles - pDrive->m_lastStepperCycle); +#endif + pDrive->m_lastStepperCycle = g_nCumulativeCycles; + // check for any stepping effect from a magnet // - move only when the magnet opposite the cog is off // - move in the direction of an adjacent magnet if one is on - // - do not move if both adjacent magnets are on + // - do not move if both adjacent magnets are on (ie. quarter track) // momentum and timing are not accounted for ... maybe one day! int direction = 0; - if (m_phases & (1 << ((pDrive->m_phase + 1) & 3))) + if (m_magnetStates & (1 << ((pDrive->m_phase + 1) & 3))) direction += 1; - if (m_phases & (1 << ((pDrive->m_phase + 3) & 3))) + if (m_magnetStates & (1 << ((pDrive->m_phase + 3) & 3))) direction -= 1; - // apply magnet step, if any - if (direction) + // Only calculate quarterDirection for WOZ, as NIB/DSK don't support half phases. + int quarterDirection = 0; + if (ImageIsWOZ(pFloppy->m_imagehandle)) { - pDrive->m_phase = MAX(0, MIN(79, pDrive->m_phase + direction)); - const int nNumTracksInImage = ImageGetNumTracks(pFloppy->m_imagehandle); - const int newtrack = (nNumTracksInImage == 0) ? 0 - : MIN(nNumTracksInImage-1, pDrive->m_phase >> 1); // (round half tracks down) - if (newtrack != pDrive->m_track) + if ((m_magnetStates == 0xC || // 1100 + m_magnetStates == 0x6 || // 0110 + m_magnetStates == 0x3 || // 0011 + m_magnetStates == 0x9)) // 1001 { - FlushCurrentTrack(m_currDrive); - pDrive->m_track = newtrack; - pFloppy->m_trackimagedata = false; - - m_formatTrack.DriveNotWritingTrack(); + quarterDirection = direction; + direction = 0; } - - // Feature Request #201 Show track status - // https://github.com/AppleWin/AppleWin/issues/201 - FrameDrawDiskStatus( (HDC)0 ); } -#else - // substitute alternate stepping code here to test -#endif + + pDrive->m_phase = MAX(0, MIN(79, pDrive->m_phase + direction)); + float newPhasePrecise = (float)(pDrive->m_phase) + (float)quarterDirection * 0.5f; + if (newPhasePrecise < 0) + newPhasePrecise = 0; + + // apply magnet step, if any + if (newPhasePrecise != pDrive->m_phasePrecise) + { + FlushCurrentTrack(m_currDrive); + pDrive->m_phasePrecise = newPhasePrecise; + pFloppy->m_trackimagedata = false; + m_formatTrack.DriveNotWritingTrack(); + FrameDrawDiskStatus((HDC)0); // Show track status (GH#201) + } #if LOG_DISK_PHASES - LOG_DISK("track $%02X%s phases %d%d%d%d phase %d %s address $%4X\r\n", - pDrive->m_phase >> 1, - (pDrive->m_phase & 1) ? ".5" : " ", - (m_phases >> 3) & 1, - (m_phases >> 2) & 1, - (m_phases >> 1) & 1, - (m_phases >> 0) & 1, - phase, + LOG_DISK("track $%s magnet-states %d%d%d%d phase %d %s address $%4X last-stepper %.3fms\r\n", + GetCurrentTrackString().c_str(), + (m_magnetStates >> 3) & 1, + (m_magnetStates >> 2) & 1, + (m_magnetStates >> 1) & 1, + (m_magnetStates >> 0) & 1, + m_magnetStates, (address & 1) ? "on " : "off", - address); + address, + ((float)cycleDelta)/(CLK_6502/1000.0)); #endif } @@ -797,14 +863,14 @@ bool Disk2InterfaceCard::LogWriteCheckSyncFF(ULONG& uCycleDelta) //=========================================================================== -void __stdcall Disk2InterfaceCard::ReadWrite(WORD pc, WORD addr, BYTE bWrite, BYTE d, ULONG nExecutedCycles) +void __stdcall Disk2InterfaceCard::ReadWrite(WORD pc, WORD addr, BYTE bWrite, BYTE d, ULONG uExecutedCycles) { /* m_floppyLoadMode = 0; */ FloppyDrive* pDrive = &m_floppyDrive[m_currDrive]; FloppyDisk* pFloppy = &pDrive->m_disk; if (!pFloppy->m_trackimagedata && pFloppy->m_imagehandle) - ReadTrack(m_currDrive); + ReadTrack(m_currDrive, uExecutedCycles); if (!pFloppy->m_trackimagedata) { @@ -814,7 +880,7 @@ void __stdcall Disk2InterfaceCard::ReadWrite(WORD pc, WORD addr, BYTE bWrite, BY // Improve precision of "authentic" drive mode - GH#125 UINT uSpinNibbleCount = 0; - CpuCalcCycles(nExecutedCycles); // g_nCumulativeCycles required for uSpinNibbleCount & LogWriteCheckSyncFF() + CpuCalcCycles(uExecutedCycles); // g_nCumulativeCycles required for uSpinNibbleCount & LogWriteCheckSyncFF() if (!m_enhanceDisk && pDrive->m_spinning) { @@ -877,6 +943,9 @@ void __stdcall Disk2InterfaceCard::ReadWrite(WORD pc, WORD addr, BYTE bWrite, BY } else if (!pFloppy->m_bWriteProtected) // && m_floppyWriteMode { + if (!pDrive->m_spinning) + return; // If not spinning then only 1 bit-cell gets written? + *(pFloppy->m_trackimage + pFloppy->m_byte) = m_floppyLatch; pFloppy->m_trackimagedirty = true; @@ -911,17 +980,247 @@ void __stdcall Disk2InterfaceCard::ReadWrite(WORD pc, WORD addr, BYTE bWrite, BY //=========================================================================== +void Disk2InterfaceCard::ResetLogicStateSequencer(void) +{ + m_shiftReg = 0; + m_latchDelay = 0; + m_resetSequencer = true; + m_dbgLatchDelayedCnt = 0; +} + +void Disk2InterfaceCard::UpdateBitStreamPositionAndDiskCycle(const ULONG uExecutedCycles) +{ + FloppyDisk& floppy = m_floppyDrive[m_currDrive].m_disk; + + CpuCalcCycles(uExecutedCycles); + const UINT bitCellDelta = GetBitCellDelta(ImageGetOptimalBitTiming(floppy.m_imagehandle)); + UpdateBitStreamPosition(floppy, bitCellDelta); + + m_diskLastCycle = g_nCumulativeCycles; +} + +UINT Disk2InterfaceCard::GetBitCellDelta(const BYTE optimalBitTiming) +{ + FloppyDisk& floppy = m_floppyDrive[m_currDrive].m_disk; + + // NB. m_extraCycles is needed to retain accuracy: + // . Read latch #1: 0-> 9: cycleDelta= 9, bitCellDelta=2, extraCycles=1 + // . Read latch #2: 11->20: cycleDelta=11, bitCellDelta=2, extraCycles=3 + // . Overall: 0->20: cycleDelta=20, bitCellDelta=5, extraCycles=0 + UINT bitCellDelta; +#if 0 + if (optimalBitTiming == 32) + { + const ULONG cycleDelta = (ULONG)(g_nCumulativeCycles - m_diskLastCycle) + (BYTE) m_extraCycles; + bitCellDelta = cycleDelta / 4; // DIV 4 for 4us per bit-cell + m_extraCycles = cycleDelta & 3; // MOD 4 : remainder carried forward for next time + } + else +#endif + { + const double cycleDelta = (double)(g_nCumulativeCycles - m_diskLastCycle) + floppy.m_extraCycles; + const double bitTime = 0.125 * (double)optimalBitTiming; // 125ns units + bitCellDelta = (UINT) floor( cycleDelta / bitTime ); + floppy.m_extraCycles = (double)cycleDelta - ((double)bitCellDelta * bitTime); + } + return bitCellDelta; +} + +void Disk2InterfaceCard::UpdateBitStreamPosition(FloppyDisk& floppy, const ULONG bitCellDelta) +{ + _ASSERT(floppy.m_bitCount); // Should never happen - ReadTrack() will handle this + if (floppy.m_bitCount == 0) + return; + + floppy.m_bitOffset += bitCellDelta; + if (floppy.m_bitOffset >= floppy.m_bitCount) + floppy.m_bitOffset %= floppy.m_bitCount; + + UpdateBitStreamOffsets(floppy); +} + +void Disk2InterfaceCard::UpdateBitStreamOffsets(FloppyDisk& floppy) +{ + floppy.m_byte = floppy.m_bitOffset / 8; + const UINT remainder = 7 - (floppy.m_bitOffset & 7); + floppy.m_bitMask = 1 << remainder; +} + +void __stdcall Disk2InterfaceCard::ReadWriteWOZ(WORD pc, WORD addr, BYTE bWrite, BYTE d, ULONG uExecutedCycles) +{ + /* m_floppyLoadMode = 0; */ + FloppyDrive& drive = m_floppyDrive[m_currDrive]; + FloppyDisk& floppy = drive.m_disk; + + if (!floppy.m_trackimagedata && floppy.m_imagehandle) + ReadTrack(m_currDrive, uExecutedCycles); + + if (!floppy.m_trackimagedata) + { + _ASSERT(0); // Can't happen for WOZ - ReadTrack() should return an empty track + m_floppyLatch = 0xFF; + return; + } + + // Don't change latch if drive off after 1 second drive-off delay (UTAIIe page 9-13) + // "DRIVES OFF forces the data register to hold its present state." (UTAIIe page 9-12) + // Note: Sherwood Forest sets shift mode and reads with the drive off. + // TODO: And same for a write? + if (!drive.m_spinning) // GH#599 + return; + + CpuCalcCycles(uExecutedCycles); + + // Skipping forward a large amount of bitcells means the bitstream will very likely be out-of-sync. + // The first 1-bit will produce a latch nibble, and this 1-bit is unlikely to be the nibble's high bit. + // So we need to ensure we run enough bits through the sequencer to re-sync. + // NB. For Planetfall 13 bitcells(NG) / 14 bitcells(OK) + const UINT significantBitCells = 50; // 5x 10-bit sync FF nibbles + UINT bitCellDelta = GetBitCellDelta(ImageGetOptimalBitTiming(floppy.m_imagehandle)); + + UINT bitCellRemainder; + if (bitCellDelta <= significantBitCells) + { + bitCellRemainder = bitCellDelta; + } + else + { + bitCellRemainder = significantBitCells; + bitCellDelta -= significantBitCells; + + UpdateBitStreamPosition(floppy, bitCellDelta); + + m_latchDelay = 0; + } + + m_diskLastCycle = g_nCumulativeCycles; + + // + + if (!m_floppyWriteMode) + { + // m_diskLastReadLatchCycle = g_nCumulativeCycles; // Not used by WOZ (only by NIB) +#if LOG_DISK_NIBBLES_READ + bool newLatchData = false; +#endif + + for (UINT i = 0; i < bitCellRemainder; i++) + { + BYTE n = floppy.m_trackimage[floppy.m_byte]; + + drive.m_headWindow <<= 1; + drive.m_headWindow |= (n & floppy.m_bitMask) ? 1 : 0; + BYTE outputBit = (drive.m_headWindow & 0xf) ? (drive.m_headWindow >> 1) & 1 + : rand() & 1; + + floppy.m_bitMask >>= 1; + if (!floppy.m_bitMask) + { + floppy.m_bitMask = 1 << 7; + floppy.m_byte++; + } + + floppy.m_bitOffset++; + if (floppy.m_bitOffset == floppy.m_bitCount) + { + floppy.m_bitMask = 1 << 7; + floppy.m_bitOffset = 0; + floppy.m_byte = 0; + } + + if (m_resetSequencer) + { + m_resetSequencer = false; // LSS takes some cycles to reset (ref?) + continue; + } + + // + + m_shiftReg <<= 1; + m_shiftReg |= outputBit; + + if (m_latchDelay) + { + m_latchDelay -= 4; + if (m_latchDelay < 0) + m_latchDelay = 0; + + if (m_shiftReg) + { + m_dbgLatchDelayedCnt = 0; + } + else // m_shiftReg==0 + { + if (m_latchDelay == 0) + m_latchDelay = 4; // extend for another 4us + + m_dbgLatchDelayedCnt++; +#if LOG_DISK_NIBBLES_READ + if (m_dbgLatchDelayedCnt >= 3) + { + LOG_DISK("read: latch held due to 0: PC=%04X, cnt=%02X\r\n", regs.pc, m_dbgLatchDelayedCnt); + } +#endif + } + } + + if (!m_latchDelay) + { +#if LOG_DISK_NIBBLES_READ + if (newLatchData) + { + LOG_DISK("read skipped latch data: %04X = %02X\r\n", floppy.m_byte, m_floppyLatch); + newLatchData = false; + } +#endif + m_floppyLatch = m_shiftReg; + + if (m_shiftReg & 0x80) + { + m_latchDelay = 7; + m_shiftReg = 0; +#if LOG_DISK_NIBBLES_READ + // May not actually be read by 6502 (eg. Prologue's CHKSUM 4&4 nibble pair), but still pass to the log's nibble reader + m_formatTrack.DecodeLatchNibbleRead(m_floppyLatch); + newLatchData = true; +#endif + } + } + } + +#if LOG_DISK_NIBBLES_READ + if (m_floppyLatch & 0x80) + { + #if LOG_DISK_NIBBLES_USE_RUNTIME_VAR + if (m_bLogDisk_NibblesRW) + #endif + { + LOG_DISK("read %04X = %02X\r\n", floppy.m_byte, m_floppyLatch); + } + } +#endif + } + else if (!floppy.m_bWriteProtected) // && m_floppyWriteMode + { + //TODO + } + + // Show track status (GH#201) - NB. Prevent flooding of forcing UI to redraw!!! + if ((floppy.m_byte & 0xFF) == 0) + FrameDrawDiskStatus( (HDC)0 ); +} + +//=========================================================================== + void Disk2InterfaceCard::Reset(const bool bIsPowerCycle/*=false*/) { // RESET forces all switches off (UTAIIe Table 9.1) - m_currDrive = 0; - m_floppyMotorOn = 0; - m_floppyLoadMode = 0; - m_floppyWriteMode = 0; - m_phases = 0; + ResetSwitches(); m_formatTrack.Reset(); + ResetLogicStateSequencer(); + if (bIsPowerCycle) // GH#460 { // NB. This doesn't affect the drive head (ie. drive's track position) @@ -937,6 +1236,15 @@ void Disk2InterfaceCard::Reset(const bool bIsPowerCycle/*=false*/) } } +void Disk2InterfaceCard::ResetSwitches(void) +{ + m_currDrive = 0; + m_floppyMotorOn = 0; + m_floppyLoadMode = 0; + m_floppyWriteMode = 0; + m_magnetStates = 0; +} + //=========================================================================== bool Disk2InterfaceCard::UserSelectNewDiskImage(const int drive, LPCSTR pszFilename/*=""*/) @@ -958,8 +1266,8 @@ bool Disk2InterfaceCard::UserSelectNewDiskImage(const int drive, LPCSTR pszFilen ofn.lStructSize = sizeof(OPENFILENAME); ofn.hwndOwner = g_hFrameWindow; ofn.hInstance = g_hInstance; - ofn.lpstrFilter = TEXT("All Images\0*.bin;*.do;*.dsk;*.nib;*.po;*.gz;*.zip;*.2mg;*.2img;*.iie;*.apl\0") - TEXT("Disk Images (*.bin,*.do,*.dsk,*.nib,*.po,*.gz,*.zip,*.2mg,*.2img,*.iie)\0*.bin;*.do;*.dsk;*.nib;*.po;*.gz;*.zip;*.2mg;*.2img;*.iie\0") + ofn.lpstrFilter = TEXT("All Images\0*.bin;*.do;*.dsk;*.nib;*.po;*.gz;*.woz;*.zip;*.2mg;*.2img;*.iie;*.apl\0") + TEXT("Disk Images (*.bin,*.do,*.dsk,*.nib,*.po,*.gz,*.woz,*.zip,*.2mg,*.2img,*.iie)\0*.bin;*.do;*.dsk;*.nib;*.po;*.gz;*.woz;*.zip;*.2mg;*.2img;*.iie\0") TEXT("All Files\0*.*\0"); ofn.lpstrFile = filename; ofn.nMaxFile = MAX_PATH; @@ -990,7 +1298,7 @@ bool Disk2InterfaceCard::UserSelectNewDiskImage(const int drive, LPCSTR pszFilen //=========================================================================== -void __stdcall Disk2InterfaceCard::LoadWriteProtect(WORD, WORD, BYTE write, BYTE value, ULONG) +void __stdcall Disk2InterfaceCard::LoadWriteProtect(WORD, WORD, BYTE write, BYTE value, ULONG uExecutedCycles) { /* m_floppyLoadMode = 1; */ @@ -1007,11 +1315,22 @@ void __stdcall Disk2InterfaceCard::LoadWriteProtect(WORD, WORD, BYTE write, BYTE // . write mode doesn't prevent reading write protect (GH#537): // "If for some reason the above write protect check were entered with the READ/WRITE switch in WRITE, // the write protect switch would still be read correctly" (UTAIIe page 9-21) + // . Sequencer "SR" (Shift Right) command only loads QA (bit7) of data register (UTAIIe page 9-21) if (m_floppyDrive[m_currDrive].m_disk.m_bWriteProtected) m_floppyLatch |= 0x80; else m_floppyLatch &= 0x7F; } + + if (ImageIsWOZ(m_floppyDrive[m_currDrive].m_disk.m_imagehandle)) + { +#if LOG_DISK_NIBBLES_READ + LOG_DISK("reset LSS: ~PC=%04X\r\n", regs.pc); +#endif + ResetLogicStateSequencer(); // reset sequencer (Ref: WOZ-1.01) +// m_latchDelay = 7; // TODO: Treat like a regular $C0EC latch load? + UpdateBitStreamPositionAndDiskCycle(uExecutedCycles); // Fix E7-copy protection + } } //=========================================================================== @@ -1181,6 +1500,9 @@ BYTE __stdcall Disk2InterfaceCard::IORead(WORD pc, WORD addr, BYTE bWrite, BYTE UINT uSlot = ((addr & 0xff) >> 4) - 8; Disk2InterfaceCard* pCard = (Disk2InterfaceCard*) MemGetSlotParameters(uSlot); + ImageInfo* pImage = pCard->m_floppyDrive[pCard->m_currDrive].m_disk.m_imagehandle; + bool isWOZ = ImageIsWOZ(pImage); + switch (addr & 0xF) { case 0x0: pCard->ControlStepper(pc, addr, bWrite, d, nExecutedCycles); break; @@ -1195,7 +1517,9 @@ BYTE __stdcall Disk2InterfaceCard::IORead(WORD pc, WORD addr, BYTE bWrite, BYTE case 0x9: pCard->ControlMotor(pc, addr, bWrite, d, nExecutedCycles); break; case 0xA: pCard->Enable(pc, addr, bWrite, d, nExecutedCycles); break; case 0xB: pCard->Enable(pc, addr, bWrite, d, nExecutedCycles); break; - case 0xC: pCard->ReadWrite(pc, addr, bWrite, d, nExecutedCycles); break; + case 0xC: if (!isWOZ) pCard->ReadWrite(pc, addr, bWrite, d, nExecutedCycles); + else pCard->ReadWriteWOZ(pc, addr, bWrite, d, nExecutedCycles); + break; case 0xD: pCard->LoadWriteProtect(pc, addr, bWrite, d, nExecutedCycles); break; case 0xE: pCard->SetReadMode(pc, addr, bWrite, d, nExecutedCycles); break; case 0xF: pCard->SetWriteMode(pc, addr, bWrite, d, nExecutedCycles); break; @@ -1213,6 +1537,9 @@ BYTE __stdcall Disk2InterfaceCard::IOWrite(WORD pc, WORD addr, BYTE bWrite, BYTE UINT uSlot = ((addr & 0xff) >> 4) - 8; Disk2InterfaceCard* pCard = (Disk2InterfaceCard*) MemGetSlotParameters(uSlot); + ImageInfo* pImage = pCard->m_floppyDrive[pCard->m_currDrive].m_disk.m_imagehandle; + bool isWOZ = ImageIsWOZ(pImage); + switch (addr & 0xF) { case 0x0: pCard->ControlStepper(pc, addr, bWrite, d, nExecutedCycles); break; @@ -1227,7 +1554,9 @@ BYTE __stdcall Disk2InterfaceCard::IOWrite(WORD pc, WORD addr, BYTE bWrite, BYTE case 0x9: pCard->ControlMotor(pc, addr, bWrite, d, nExecutedCycles); break; case 0xA: pCard->Enable(pc, addr, bWrite, d, nExecutedCycles); break; case 0xB: pCard->Enable(pc, addr, bWrite, d, nExecutedCycles); break; - case 0xC: pCard->ReadWrite(pc, addr, bWrite, d, nExecutedCycles); break; + case 0xC: if (!isWOZ) pCard->ReadWrite(pc, addr, bWrite, d, nExecutedCycles); + else pCard->ReadWriteWOZ(pc, addr, bWrite, d, nExecutedCycles); + break; case 0xD: pCard->LoadWriteProtect(pc, addr, bWrite, d, nExecutedCycles); break; case 0xE: pCard->SetReadMode(pc, addr, bWrite, d, nExecutedCycles); break; case 0xF: pCard->SetWriteMode(pc, addr, bWrite, d, nExecutedCycles); break; @@ -1246,7 +1575,9 @@ BYTE __stdcall Disk2InterfaceCard::IOWrite(WORD pc, WORD addr, BYTE bWrite, BYTE // Unit version history: // 2: Added: Format Track state & DiskLastCycle // 3: Added: DiskLastReadLatchCycle -static const UINT kUNIT_VERSION = 3; +// 4: Added: WOZ state +// Split up 'Unit' putting some state into a new 'Floppy' +static const UINT kUNIT_VERSION = 4; #define SS_YAML_VALUE_CARD_DISK2 "Disk][" @@ -1259,16 +1590,27 @@ static const UINT kUNIT_VERSION = 3; #define SS_YAML_KEY_FLOPPY_WRITE_MODE "Floppy Write Mode" #define SS_YAML_KEY_LAST_CYCLE "Last Cycle" #define SS_YAML_KEY_LAST_READ_LATCH_CYCLE "Last Read Latch Cycle" +#define SS_YAML_KEY_LSS_SHIFT_REG "LSS Shift Reg" +#define SS_YAML_KEY_LSS_LATCH_DELAY "LSS Latch Delay" +#define SS_YAML_KEY_LSS_RESET_SEQUENCER "LSS Reset Sequencer" #define SS_YAML_KEY_DISK2UNIT "Unit" #define SS_YAML_KEY_FILENAME "Filename" -#define SS_YAML_KEY_TRACK "Track" #define SS_YAML_KEY_PHASE "Phase" +#define SS_YAML_KEY_PHASE_PRECISE "Phase (precise)" +#define SS_YAML_KEY_TRACK "Track" // deprecated at v4 +#define SS_YAML_KEY_HEAD_WINDOW "Head Window" +#define SS_YAML_KEY_LAST_STEPPER_CYCLE "Last Stepper Cycle" + +#define SS_YAML_KEY_FLOPPY "Floppy" #define SS_YAML_KEY_BYTE "Byte" +#define SS_YAML_KEY_NIBBLES "Nibbles" +#define SS_YAML_KEY_BIT_OFFSET "Bit Offset" +#define SS_YAML_KEY_BIT_COUNT "Bit Count" +#define SS_YAML_KEY_EXTRA_CYCLES "Extra Cycles" #define SS_YAML_KEY_WRITE_PROTECTED "Write Protected" #define SS_YAML_KEY_SPINNING "Spinning" #define SS_YAML_KEY_WRITE_LIGHT "Write Light" -#define SS_YAML_KEY_NIBBLES "Nibbles" #define SS_YAML_KEY_TRACK_IMAGE_DATA "Track Image Data" #define SS_YAML_KEY_TRACK_IMAGE_DIRTY "Track Image Dirty" #define SS_YAML_KEY_TRACK_IMAGE "Track Image" @@ -1279,17 +1621,16 @@ std::string Disk2InterfaceCard::GetSnapshotCardName(void) return name; } -void Disk2InterfaceCard::SaveSnapshotDisk2Unit(YamlSaveHelper& yamlSaveHelper, UINT unit) +void Disk2InterfaceCard::SaveSnapshotFloppy(YamlSaveHelper& yamlSaveHelper, UINT unit) { - YamlSaveHelper::Label label(yamlSaveHelper, "%s%d:\n", SS_YAML_KEY_DISK2UNIT, unit); + YamlSaveHelper::Label label(yamlSaveHelper, "%s:\n", SS_YAML_KEY_FLOPPY); yamlSaveHelper.SaveString(SS_YAML_KEY_FILENAME, m_floppyDrive[unit].m_disk.m_fullname); - yamlSaveHelper.SaveUint(SS_YAML_KEY_TRACK, m_floppyDrive[unit].m_track); - yamlSaveHelper.SaveUint(SS_YAML_KEY_PHASE, m_floppyDrive[unit].m_phase); yamlSaveHelper.SaveHexUint16(SS_YAML_KEY_BYTE, m_floppyDrive[unit].m_disk.m_byte); - yamlSaveHelper.SaveBool(SS_YAML_KEY_WRITE_PROTECTED, m_floppyDrive[unit].m_disk.m_bWriteProtected); - yamlSaveHelper.SaveUint(SS_YAML_KEY_SPINNING, m_floppyDrive[unit].m_spinning); - yamlSaveHelper.SaveUint(SS_YAML_KEY_WRITE_LIGHT, m_floppyDrive[unit].m_writelight); yamlSaveHelper.SaveHexUint16(SS_YAML_KEY_NIBBLES, m_floppyDrive[unit].m_disk.m_nibbles); + yamlSaveHelper.SaveHexUint32(SS_YAML_KEY_BIT_OFFSET, m_floppyDrive[unit].m_disk.m_bitOffset); // v4 + yamlSaveHelper.SaveHexUint32(SS_YAML_KEY_BIT_COUNT, m_floppyDrive[unit].m_disk.m_bitCount); // v4 + yamlSaveHelper.SaveDouble(SS_YAML_KEY_EXTRA_CYCLES, m_floppyDrive[unit].m_disk.m_extraCycles); // v4 + yamlSaveHelper.SaveBool(SS_YAML_KEY_WRITE_PROTECTED, m_floppyDrive[unit].m_disk.m_bWriteProtected); yamlSaveHelper.SaveUint(SS_YAML_KEY_TRACK_IMAGE_DATA, m_floppyDrive[unit].m_disk.m_trackimagedata); yamlSaveHelper.SaveUint(SS_YAML_KEY_TRACK_IMAGE_DIRTY, m_floppyDrive[unit].m_disk.m_trackimagedirty); @@ -1300,13 +1641,26 @@ void Disk2InterfaceCard::SaveSnapshotDisk2Unit(YamlSaveHelper& yamlSaveHelper, U } } +void Disk2InterfaceCard::SaveSnapshotDriveUnit(YamlSaveHelper& yamlSaveHelper, UINT unit) +{ + YamlSaveHelper::Label label(yamlSaveHelper, "%s%d:\n", SS_YAML_KEY_DISK2UNIT, unit); + yamlSaveHelper.SaveUint(SS_YAML_KEY_PHASE, m_floppyDrive[unit].m_phase); + yamlSaveHelper.SaveFloat(SS_YAML_KEY_PHASE_PRECISE, m_floppyDrive[unit].m_phasePrecise); // v4 + yamlSaveHelper.SaveHexUint4(SS_YAML_KEY_HEAD_WINDOW, m_floppyDrive[unit].m_headWindow); // v4 + yamlSaveHelper.SaveHexUint64(SS_YAML_KEY_LAST_STEPPER_CYCLE, m_floppyDrive[unit].m_lastStepperCycle); // v4 + yamlSaveHelper.SaveUint(SS_YAML_KEY_SPINNING, m_floppyDrive[unit].m_spinning); + yamlSaveHelper.SaveUint(SS_YAML_KEY_WRITE_LIGHT, m_floppyDrive[unit].m_writelight); + + SaveSnapshotFloppy(yamlSaveHelper, unit); +} + void Disk2InterfaceCard::SaveSnapshot(class YamlSaveHelper& yamlSaveHelper) { YamlSaveHelper::Slot slot(yamlSaveHelper, GetSnapshotCardName(), m_slot, kUNIT_VERSION); YamlSaveHelper::Label state(yamlSaveHelper, "%s:\n", SS_YAML_KEY_STATE); - yamlSaveHelper.SaveHexUint4(SS_YAML_KEY_PHASES, m_phases); yamlSaveHelper.SaveUint(SS_YAML_KEY_CURRENT_DRIVE, m_currDrive); + yamlSaveHelper.SaveHexUint4(SS_YAML_KEY_PHASES, m_magnetStates); yamlSaveHelper.SaveBool(SS_YAML_KEY_DISK_ACCESSED, false); // deprecated yamlSaveHelper.SaveBool(SS_YAML_KEY_ENHANCE_DISK, m_enhanceDisk); yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_FLOPPY_LATCH, m_floppyLatch); @@ -1314,29 +1668,24 @@ void Disk2InterfaceCard::SaveSnapshot(class YamlSaveHelper& yamlSaveHelper) yamlSaveHelper.SaveBool(SS_YAML_KEY_FLOPPY_WRITE_MODE, m_floppyWriteMode == TRUE); yamlSaveHelper.SaveHexUint64(SS_YAML_KEY_LAST_CYCLE, m_diskLastCycle); // v2 yamlSaveHelper.SaveHexUint64(SS_YAML_KEY_LAST_READ_LATCH_CYCLE, m_diskLastReadLatchCycle); // v3 + yamlSaveHelper.SaveHexUint8(SS_YAML_KEY_LSS_SHIFT_REG, m_shiftReg); // v4 + yamlSaveHelper.SaveInt(SS_YAML_KEY_LSS_LATCH_DELAY, m_latchDelay); // v4 + yamlSaveHelper.SaveBool(SS_YAML_KEY_LSS_RESET_SEQUENCER, m_resetSequencer); // v4 m_formatTrack.SaveSnapshot(yamlSaveHelper); // v2 - SaveSnapshotDisk2Unit(yamlSaveHelper, DRIVE_1); - SaveSnapshotDisk2Unit(yamlSaveHelper, DRIVE_2); + SaveSnapshotDriveUnit(yamlSaveHelper, DRIVE_1); + SaveSnapshotDriveUnit(yamlSaveHelper, DRIVE_2); } -void Disk2InterfaceCard::LoadSnapshotDriveUnit(YamlLoadHelper& yamlLoadHelper, UINT unit) +bool Disk2InterfaceCard::LoadSnapshotFloppy(YamlLoadHelper& yamlLoadHelper, UINT unit, UINT version, std::vector& track) { - std::string disk2UnitName = std::string(SS_YAML_KEY_DISK2UNIT) + (unit == DRIVE_1 ? std::string("0") : std::string("1")); - if (!yamlLoadHelper.GetSubMap(disk2UnitName)) - throw std::string("Card: Expected key: ") + disk2UnitName; - - bool bImageError = false; - - m_floppyDrive[unit].m_disk.m_fullname[0] = 0; - m_floppyDrive[unit].m_disk.m_imagename[0] = 0; - m_floppyDrive[unit].m_disk.m_bWriteProtected = false; // Default to false (until image is successfully loaded below) - std::string filename = yamlLoadHelper.LoadString(SS_YAML_KEY_FILENAME); - if (!filename.empty()) + bool bImageError = filename.empty(); + + if (!bImageError) { DWORD dwAttributes = GetFileAttributes(filename.c_str()); - if(dwAttributes == INVALID_FILE_ATTRIBUTES) + if (dwAttributes == INVALID_FILE_ATTRIBUTES) { // Get user to browse for file UserSelectNewDiskImage(unit, filename.c_str()); @@ -1347,38 +1696,106 @@ void Disk2InterfaceCard::LoadSnapshotDriveUnit(YamlLoadHelper& yamlLoadHelper, U bImageError = (dwAttributes == INVALID_FILE_ATTRIBUTES); if (!bImageError) { - if(InsertDisk(unit, filename.c_str(), dwAttributes & FILE_ATTRIBUTE_READONLY, IMAGE_DONT_CREATE) != eIMAGE_ERROR_NONE) + if (InsertDisk(unit, filename.c_str(), dwAttributes & FILE_ATTRIBUTE_READONLY, IMAGE_DONT_CREATE) != eIMAGE_ERROR_NONE) bImageError = true; // DiskInsert() zeros m_floppyDrive[unit], then sets up: - // . imagename - // . fullname - // . writeprotected + // . m_imagename + // . m_fullname + // . m_bWriteProtected } } - m_floppyDrive[unit].m_track = yamlLoadHelper.LoadUint(SS_YAML_KEY_TRACK); - m_floppyDrive[unit].m_phase = yamlLoadHelper.LoadUint(SS_YAML_KEY_PHASE); - m_floppyDrive[unit].m_disk.m_byte = yamlLoadHelper.LoadUint(SS_YAML_KEY_BYTE); yamlLoadHelper.LoadBool(SS_YAML_KEY_WRITE_PROTECTED); // Consume - m_floppyDrive[unit].m_spinning = yamlLoadHelper.LoadUint(SS_YAML_KEY_SPINNING); - m_floppyDrive[unit].m_writelight = yamlLoadHelper.LoadUint(SS_YAML_KEY_WRITE_LIGHT); - m_floppyDrive[unit].m_disk.m_nibbles = yamlLoadHelper.LoadUint(SS_YAML_KEY_NIBBLES); - m_floppyDrive[unit].m_disk.m_trackimagedata = yamlLoadHelper.LoadUint(SS_YAML_KEY_TRACK_IMAGE_DATA) ? true : false; - m_floppyDrive[unit].m_disk.m_trackimagedirty = yamlLoadHelper.LoadUint(SS_YAML_KEY_TRACK_IMAGE_DIRTY) ? true : false; + m_floppyDrive[unit].m_disk.m_byte = yamlLoadHelper.LoadUint(SS_YAML_KEY_BYTE); + m_floppyDrive[unit].m_disk.m_nibbles = yamlLoadHelper.LoadUint(SS_YAML_KEY_NIBBLES); + m_floppyDrive[unit].m_disk.m_trackimagedata = yamlLoadHelper.LoadUint(SS_YAML_KEY_TRACK_IMAGE_DATA) ? true : false; + m_floppyDrive[unit].m_disk.m_trackimagedirty = yamlLoadHelper.LoadUint(SS_YAML_KEY_TRACK_IMAGE_DIRTY) ? true : false; + + if (version >= 4) + { + m_floppyDrive[unit].m_disk.m_bitOffset = yamlLoadHelper.LoadUint(SS_YAML_KEY_BIT_OFFSET); + m_floppyDrive[unit].m_disk.m_bitCount = yamlLoadHelper.LoadUint(SS_YAML_KEY_BIT_COUNT); + m_floppyDrive[unit].m_disk.m_extraCycles = yamlLoadHelper.LoadDouble(SS_YAML_KEY_EXTRA_CYCLES); + + if (m_floppyDrive[unit].m_disk.m_bitCount && (m_floppyDrive[unit].m_disk.m_bitOffset >= m_floppyDrive[unit].m_disk.m_bitCount)) + throw std::string("Disk image: bitOffset >= bitCount"); + + if (ImageIsWOZ(m_floppyDrive[unit].m_disk.m_imagehandle)) + UpdateBitStreamOffsets(m_floppyDrive[unit].m_disk); // overwrites m_byte, inits m_bitMask + } - std::vector track(NIBBLES_PER_TRACK); if (yamlLoadHelper.GetSubMap(SS_YAML_KEY_TRACK_IMAGE)) { yamlLoadHelper.LoadMemory(&track[0], NIBBLES_PER_TRACK); yamlLoadHelper.PopMap(); } + return bImageError; +} + +bool Disk2InterfaceCard::LoadSnapshotDriveUnitv3(YamlLoadHelper& yamlLoadHelper, UINT unit, UINT version, std::vector& track) +{ + _ASSERT(version <= 3); + + std::string disk2UnitName = std::string(SS_YAML_KEY_DISK2UNIT) + (unit == DRIVE_1 ? std::string("0") : std::string("1")); + if (!yamlLoadHelper.GetSubMap(disk2UnitName)) + throw std::string("Card: Expected key: ") + disk2UnitName; + + bool bImageError = LoadSnapshotFloppy(yamlLoadHelper, unit, version, track); + + yamlLoadHelper.LoadUint(SS_YAML_KEY_TRACK); // consume + m_floppyDrive[unit].m_phase = yamlLoadHelper.LoadUint(SS_YAML_KEY_PHASE); + m_floppyDrive[unit].m_phasePrecise = (float) m_floppyDrive[unit].m_phase; + m_floppyDrive[unit].m_spinning = yamlLoadHelper.LoadUint(SS_YAML_KEY_SPINNING); + m_floppyDrive[unit].m_writelight = yamlLoadHelper.LoadUint(SS_YAML_KEY_WRITE_LIGHT); + + yamlLoadHelper.PopMap(); + + return bImageError; +} + +bool Disk2InterfaceCard::LoadSnapshotDriveUnitv4(YamlLoadHelper& yamlLoadHelper, UINT unit, UINT version, std::vector& track) +{ + _ASSERT(version >= 4); + + std::string disk2UnitName = std::string(SS_YAML_KEY_DISK2UNIT) + (unit == DRIVE_1 ? std::string("0") : std::string("1")); + if (!yamlLoadHelper.GetSubMap(disk2UnitName)) + throw std::string("Card: Expected key: ") + disk2UnitName; + + if (!yamlLoadHelper.GetSubMap(SS_YAML_KEY_FLOPPY)) + throw std::string("Card: Expected key: ") + SS_YAML_KEY_FLOPPY; + + bool bImageError = LoadSnapshotFloppy(yamlLoadHelper, unit, version, track); + yamlLoadHelper.PopMap(); // - if (!filename.empty() && !bImageError) + m_floppyDrive[unit].m_phase = yamlLoadHelper.LoadUint(SS_YAML_KEY_PHASE); + m_floppyDrive[unit].m_phasePrecise = yamlLoadHelper.LoadFloat(SS_YAML_KEY_PHASE_PRECISE); + m_floppyDrive[unit].m_headWindow = yamlLoadHelper.LoadUint(SS_YAML_KEY_HEAD_WINDOW) & 0xf; + m_floppyDrive[unit].m_lastStepperCycle = yamlLoadHelper.LoadUint64(SS_YAML_KEY_LAST_STEPPER_CYCLE); + m_floppyDrive[unit].m_spinning = yamlLoadHelper.LoadUint(SS_YAML_KEY_SPINNING); + m_floppyDrive[unit].m_writelight = yamlLoadHelper.LoadUint(SS_YAML_KEY_WRITE_LIGHT); + + yamlLoadHelper.PopMap(); + + return bImageError; +} + +void Disk2InterfaceCard::LoadSnapshotDriveUnit(YamlLoadHelper& yamlLoadHelper, UINT unit, UINT version) +{ + bool bImageError = false; + std::vector track(NIBBLES_PER_TRACK); + + if (version <= 3) + bImageError = LoadSnapshotDriveUnitv3(yamlLoadHelper, unit, version, track); + else + bImageError = LoadSnapshotDriveUnitv4(yamlLoadHelper, unit, version, track); + + + if (!bImageError) { if ((m_floppyDrive[unit].m_disk.m_trackimage == NULL) && m_floppyDrive[unit].m_disk.m_nibbles) AllocTrack(unit); @@ -1391,9 +1808,9 @@ void Disk2InterfaceCard::LoadSnapshotDriveUnit(YamlLoadHelper& yamlLoadHelper, U if (bImageError) { - m_floppyDrive[unit].m_disk.m_trackimagedata = false; - m_floppyDrive[unit].m_disk.m_trackimagedirty = false; - m_floppyDrive[unit].m_disk.m_nibbles = 0; + m_floppyDrive[unit].m_disk.m_trackimagedata = false; + m_floppyDrive[unit].m_disk.m_trackimagedirty = false; + m_floppyDrive[unit].m_disk.m_nibbles = 0; } } @@ -1405,8 +1822,8 @@ bool Disk2InterfaceCard::LoadSnapshot(class YamlLoadHelper& yamlLoadHelper, UINT if (version < 1 || version > kUNIT_VERSION) throw std::string("Card: wrong version"); - m_phases = yamlLoadHelper.LoadUint(SS_YAML_KEY_PHASES); - m_currDrive = yamlLoadHelper.LoadUint(SS_YAML_KEY_CURRENT_DRIVE); + m_currDrive = yamlLoadHelper.LoadUint(SS_YAML_KEY_CURRENT_DRIVE); + m_magnetStates = yamlLoadHelper.LoadUint(SS_YAML_KEY_PHASES); (void) yamlLoadHelper.LoadBool(SS_YAML_KEY_DISK_ACCESSED); // deprecated - but retrieve the value to avoid the "State: Unknown key (Disk Accessed)" warning m_enhanceDisk = yamlLoadHelper.LoadBool(SS_YAML_KEY_ENHANCE_DISK); m_floppyLatch = yamlLoadHelper.LoadUint(SS_YAML_KEY_FLOPPY_LATCH); @@ -1424,6 +1841,13 @@ bool Disk2InterfaceCard::LoadSnapshot(class YamlLoadHelper& yamlLoadHelper, UINT m_diskLastReadLatchCycle = yamlLoadHelper.LoadUint64(SS_YAML_KEY_LAST_READ_LATCH_CYCLE); } + if (version >= 4) + { + m_shiftReg = yamlLoadHelper.LoadUint(SS_YAML_KEY_LSS_SHIFT_REG) & 0xff; + m_latchDelay = yamlLoadHelper.LoadInt(SS_YAML_KEY_LSS_LATCH_DELAY); + m_resetSequencer = yamlLoadHelper.LoadBool(SS_YAML_KEY_LSS_RESET_SEQUENCER); + } + // Eject all disks first in case Drive-2 contains disk to be inserted into Drive-1 for (UINT i=0; i ImageInfo* m_imagehandle; // Init'd by InsertDisk() -> ImageOpen() bool m_bWriteProtected; - int m_byte; - int m_nibbles; // Init'd by ReadTrack() -> ImageReadTrack() + int m_byte; // byte offset + int m_nibbles; // # nibbles in track / Init'd by ReadTrack() -> ImageReadTrack() + UINT m_bitOffset; // bit offset + UINT m_bitCount; // # bits in track + BYTE m_bitMask; + double m_extraCycles; LPBYTE m_trackimage; bool m_trackimagedata; bool m_trackimagedirty; @@ -91,16 +99,20 @@ public: void clear() { + m_phasePrecise = 0; m_phase = 0; - m_track = 0; + m_lastStepperCycle = 0; + m_headWindow = 0; m_spinning = 0; m_writelight = 0; m_disk.clear(); } public: - int m_phase; - int m_track; + float m_phasePrecise; // Phase precise to half a phase (aka quarter track) + int m_phase; // Integral phase number + unsigned __int64 m_lastStepperCycle; + BYTE m_headWindow; DWORD m_spinning; DWORD m_writelight; FloppyDisk m_disk; @@ -132,10 +144,14 @@ public: bool GetProtect(const int drive); void SetProtect(const int drive, const bool bWriteProtect); int GetCurrentDrive(void); - int GetCurrentTrack(); - int GetTrack(const int drive); - int GetCurrentPhase(void); + int GetCurrentTrack(void); + float GetCurrentPhase(void); int GetCurrentOffset(void); + BYTE GetCurrentLSSBitMask(void); + double GetCurrentExtraCycles(void); + int GetTrack(const int drive); + std::string GetCurrentTrackString(void); + std::string GetCurrentPhaseString(void); LPCTSTR GetCurrentState(void); bool UserSelectNewDiskImage(const int drive, LPCSTR pszFilename=""); void UpdateDriveState(DWORD cycles); @@ -158,21 +174,32 @@ public: static BYTE __stdcall IOWrite(WORD pc, WORD addr, BYTE bWrite, BYTE d, ULONG nExecutedCycles); private: - void CheckSpinning(const ULONG nExecutedCycles); + void ResetSwitches(void); + void CheckSpinning(const ULONG uExecutedCycles); Disk_Status_e GetDriveLightStatus(const int drive); bool IsDriveValid(const int drive); void AllocTrack(const int drive); - void ReadTrack(const int drive); + void ReadTrack(const int drive, ULONG uExecutedCycles); void RemoveDisk(const int drive); void WriteTrack(const int drive); LPCTSTR DiskGetFullPathName(const int drive); - void SaveSnapshotDisk2Unit(YamlSaveHelper& yamlSaveHelper, UINT unit); - void LoadSnapshotDriveUnit(YamlLoadHelper& yamlLoadHelper, UINT unit); + void ResetLogicStateSequencer(void); + void UpdateBitStreamPositionAndDiskCycle(const ULONG uExecutedCycles); + UINT GetBitCellDelta(const BYTE optimalBitTiming); + void UpdateBitStreamPosition(FloppyDisk& floppy, const ULONG bitCellDelta); + void UpdateBitStreamOffsets(FloppyDisk& floppy); + void SaveSnapshotFloppy(YamlSaveHelper& yamlSaveHelper, UINT unit); + void SaveSnapshotDriveUnit(YamlSaveHelper& yamlSaveHelper, UINT unit); + bool LoadSnapshotFloppy(YamlLoadHelper& yamlLoadHelper, UINT unit, UINT version, std::vector& track); + bool LoadSnapshotDriveUnitv3(YamlLoadHelper& yamlLoadHelper, UINT unit, UINT version, std::vector& track); + bool LoadSnapshotDriveUnitv4(YamlLoadHelper& yamlLoadHelper, UINT unit, UINT version, std::vector& track); + void LoadSnapshotDriveUnit(YamlLoadHelper& yamlLoadHelper, UINT unit, UINT version); void __stdcall ControlStepper(WORD, WORD address, BYTE, BYTE, ULONG uExecutedCycles); void __stdcall ControlMotor(WORD, WORD address, BYTE, BYTE, ULONG uExecutedCycles); void __stdcall Enable(WORD, WORD address, BYTE, BYTE, ULONG uExecutedCycles); - void __stdcall ReadWrite(WORD pc, WORD addr, BYTE bWrite, BYTE d, ULONG nExecutedCycles); + void __stdcall ReadWrite(WORD pc, WORD addr, BYTE bWrite, BYTE d, ULONG uExecutedCycles); + void __stdcall ReadWriteWOZ(WORD pc, WORD addr, BYTE bWrite, BYTE d, ULONG uExecutedCycles); void __stdcall LoadWriteProtect(WORD, WORD, BYTE write, BYTE value, ULONG); void __stdcall SetReadMode(WORD, WORD, BYTE, BYTE, ULONG); void __stdcall SetWriteMode(WORD, WORD, BYTE, BYTE, ULONG uExecutedCycles); @@ -189,7 +216,11 @@ private: BOOL m_floppyMotorOn; BOOL m_floppyLoadMode; // for efficiency this is not used; it's extremely unlikely to affect emulation (nickw) BOOL m_floppyWriteMode; - WORD m_phases; // state bits for stepper magnet phases 0 - 3 + + // Although the magnets are a property of the drive, their state is a property of the controller card, + // since the magnets will only be on for whichever of the 2 drives is currently selected. + WORD m_magnetStates; // state bits for stepper motor magnet states (phases 0 - 3) + bool m_saveDiskImage; UINT m_slot; unsigned __int64 m_diskLastCycle; @@ -197,8 +228,14 @@ private: FormatTrack m_formatTrack; bool m_enhanceDisk; - static const UINT SPINNING_CYCLES = 20000*64; // 1280000 cycles = 1.25s - static const UINT WRITELIGHT_CYCLES = 20000*64; // 1280000 cycles = 1.25s + static const UINT SPINNING_CYCLES = 1000*1000; // 1M cycles = ~1.000s + static const UINT WRITELIGHT_CYCLES = 1000*1000; // 1M cycles = ~1.000s + + // Logic State Sequencer (for WOZ): + BYTE m_shiftReg; + int m_latchDelay; + bool m_resetSequencer; + UINT m_dbgLatchDelayedCnt; // Debug: #if LOG_DISK_NIBBLES_USE_RUNTIME_VAR diff --git a/source/DiskDefs.h b/source/DiskDefs.h index 90a2e17b..1b809218 100644 --- a/source/DiskDefs.h +++ b/source/DiskDefs.h @@ -1,5 +1,7 @@ #pragma once -#define NIBBLES_PER_TRACK 0x1A00 +#define NIBBLES_PER_TRACK_NIB 0x1A00 +#define NIBBLES_PER_TRACK_WOZ2 0x1A18 // 6680 +#define NIBBLES_PER_TRACK NIBBLES_PER_TRACK_WOZ2 // MAX(NIBBLES_PER_TRACK_NIB, NIBBLES_PER_TRACK_WOZ2) const UINT NUM_SECTORS = 16; diff --git a/source/DiskFormatTrack.cpp b/source/DiskFormatTrack.cpp index 201ac392..229fedee 100644 --- a/source/DiskFormatTrack.cpp +++ b/source/DiskFormatTrack.cpp @@ -264,17 +264,18 @@ void FormatTrack::DecodeLatchNibble(BYTE floppylatch, bool bIsWrite, bool bIsSyn m_VolTrkSecChk[i] = ((m_VolTrkSecChk4and4[i*2] & 0x55) << 1) | (m_VolTrkSecChk4and4[i*2+1] & 0x55); #if LOG_DISK_NIBBLES_READ + const bool chk = (m_VolTrkSecChk[0] ^ m_VolTrkSecChk[1] ^ m_VolTrkSecChk[2] ^ m_VolTrkSecChk[3]) == 0; if (!bIsWrite) { BYTE addrPrologue = m_bAddressPrologueIsDOS3_2 ? (BYTE)kADDR_PROLOGUE_DOS3_2 : (BYTE)kADDR_PROLOGUE_DOS3_3; - LOG_DISK("read D5AA%02X detected - Vol:%02X Trk:%02X Sec:%02X Chk:%02X\r\n", addrPrologue, m_VolTrkSecChk[0], m_VolTrkSecChk[1], m_VolTrkSecChk[2], m_VolTrkSecChk[3]); + LOG_DISK("read D5AA%02X detected - Vol:%02X Trk:%02X Sec:%02X Chk:%02X %s\r\n", addrPrologue, m_VolTrkSecChk[0], m_VolTrkSecChk[1], m_VolTrkSecChk[2], m_VolTrkSecChk[3], chk?"":"(bad)"); } #endif #if LOG_DISK_NIBBLES_WRITE if (bIsWrite) { BYTE addrPrologue = m_bAddressPrologueIsDOS3_2 ? (BYTE)kADDR_PROLOGUE_DOS3_2 : (BYTE)kADDR_PROLOGUE_DOS3_3; - LOG_DISK("write D5AA%02X detected - Vol:%02X Trk:%02X Sec:%02X Chk:%02X\r\n", addrPrologue, m_VolTrkSecChk[0], m_VolTrkSecChk[1], m_VolTrkSecChk[2], m_VolTrkSecChk[3]); + LOG_DISK("write D5AA%02X detected - Vol:%02X Trk:%02X Sec:%02X Chk:%02X %s\r\n", addrPrologue, m_VolTrkSecChk[0], m_VolTrkSecChk[1], m_VolTrkSecChk[2], m_VolTrkSecChk[3], chk?"":"(bad)"); } #endif diff --git a/source/DiskImage.cpp b/source/DiskImage.cpp index 0d2d300e..8fb3beac 100644 --- a/source/DiskImage.cpp +++ b/source/DiskImage.cpp @@ -28,6 +28,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include "StdAfx.h" +#include "Common.h" #include "DiskImage.h" #include "DiskImageHelper.h" @@ -152,19 +153,21 @@ void ImageInitialize(void) //=========================================================================== void ImageReadTrack( ImageInfo* const pImageInfo, - const int nTrack, - const int nQuarterTrack, + float phase, // phase [0..79] +/- 0.5 LPBYTE pTrackImageBuffer, int* pNibbles, + UINT* pBitCount, bool enhanceDisk) { - _ASSERT(nTrack >= 0); - if (nTrack < 0) - return; + _ASSERT(phase >= 0); + if (phase < 0) + phase = 0; - if (pImageInfo->pImageType->AllowRW() && pImageInfo->ValidTrack[nTrack]) + const UINT track = pImageInfo->pImageType->PhaseToTrack(phase); + + if (pImageInfo->pImageType->AllowRW() && pImageInfo->ValidTrack[track]) { - pImageInfo->pImageType->Read(pImageInfo, nTrack, nQuarterTrack, pTrackImageBuffer, pNibbles, enhanceDisk); + pImageInfo->pImageType->Read(pImageInfo, phase, pTrackImageBuffer, pNibbles, pBitCount, enhanceDisk); } else { @@ -176,19 +179,20 @@ void ImageReadTrack( ImageInfo* const pImageInfo, //=========================================================================== void ImageWriteTrack( ImageInfo* const pImageInfo, - const int nTrack, - const int nQuarterTrack, - LPBYTE pTrackImage, + float phase, // phase [0..79] +/- 0.5 + LPBYTE pTrackImageBuffer, const int nNibbles) { - _ASSERT(nTrack >= 0); - if (nTrack < 0) - return; + _ASSERT(phase >= 0); + if (phase < 0) + phase = 0; + + const UINT track = pImageInfo->pImageType->PhaseToTrack(phase); if (pImageInfo->pImageType->AllowRW() && !pImageInfo->bWriteProtected) { - pImageInfo->pImageType->Write(pImageInfo, nTrack, nQuarterTrack, pTrackImage, nNibbles); - pImageInfo->ValidTrack[nTrack] = 1; + pImageInfo->pImageType->Write(pImageInfo, phase, pTrackImageBuffer, nNibbles); + pImageInfo->ValidTrack[track] = 1; } } @@ -220,7 +224,7 @@ bool ImageWriteBlock( ImageInfo* const pImageInfo, //=========================================================================== -int ImageGetNumTracks(ImageInfo* const pImageInfo) +UINT ImageGetNumTracks(ImageInfo* const pImageInfo) { return pImageInfo ? pImageInfo->uNumTracks : 0; } @@ -246,6 +250,33 @@ UINT ImageGetImageSize(ImageInfo* const pImageInfo) return pImageInfo ? pImageInfo->uImageSize : 0; } +bool ImageIsWOZ(ImageInfo* const pImageInfo) +{ + return pImageInfo ? (pImageInfo->pImageType->GetType() == eImageWOZ1 || pImageInfo->pImageType->GetType() == eImageWOZ2) : false; +} + +BYTE ImageGetOptimalBitTiming(ImageInfo* const pImageInfo) +{ + return pImageInfo ? pImageInfo->optimalBitTiming : 32; +} + +UINT ImagePhaseToTrack(ImageInfo* const pImageInfo, const float phase, const bool limit/*=true*/) +{ + if (!pImageInfo) + return 0; + + UINT track = pImageInfo->pImageType->PhaseToTrack(phase); + + if (limit) + { + const UINT numTracksInImage = ImageGetNumTracks(pImageInfo); + track = (numTracksInImage == 0) ? 0 + : MIN(numTracksInImage - 1, track); + } + + return track; +} + void GetImageTitle(LPCTSTR pPathname, TCHAR* pImageName, TCHAR* pFullName) { TCHAR imagetitle[ MAX_DISK_FULL_NAME+1 ]; diff --git a/source/DiskImage.h b/source/DiskImage.h index 7a8277c4..14489677 100644 --- a/source/DiskImage.h +++ b/source/DiskImage.h @@ -71,15 +71,18 @@ BOOL ImageBoot(ImageInfo* const pImageInfo); void ImageDestroy(void); void ImageInitialize(void); -void ImageReadTrack(ImageInfo* const pImageInfo, int nTrack, int nQuarterTrack, LPBYTE pTrackImageBuffer, int* pNibbles, bool enhanceDisk); -void ImageWriteTrack(ImageInfo* const pImageInfo, int nTrack, int nQuarterTrack, LPBYTE pTrackImage, int nNibbles); +void ImageReadTrack(ImageInfo* const pImageInfo, float phase, LPBYTE pTrackImageBuffer, int* pNibbles, UINT* pBitCount, bool enhanceDisk); +void ImageWriteTrack(ImageInfo* const pImageInfo, float phase, LPBYTE pTrackImageBuffer, int nNibbles); bool ImageReadBlock(ImageInfo* const pImageInfo, UINT nBlock, LPBYTE pBlockBuffer); bool ImageWriteBlock(ImageInfo* const pImageInfo, UINT nBlock, LPBYTE pBlockBuffer); -int ImageGetNumTracks(ImageInfo* const pImageInfo); +UINT ImageGetNumTracks(ImageInfo* const pImageInfo); bool ImageIsWriteProtected(ImageInfo* const pImageInfo); bool ImageIsMultiFileZip(ImageInfo* const pImageInfo); const char* ImageGetPathname(ImageInfo* const pImageInfo); UINT ImageGetImageSize(ImageInfo* const pImageInfo); +bool ImageIsWOZ(ImageInfo* const pImageInfo); +BYTE ImageGetOptimalBitTiming(ImageInfo* const pImageInfo); +UINT ImagePhaseToTrack(ImageInfo* const pImageInfo, const float phase, const bool limit=true); void GetImageTitle(LPCTSTR pPathname, TCHAR* pImageName, TCHAR* pFullName); diff --git a/source/DiskImageHelper.cpp b/source/DiskImageHelper.cpp index e84b2faf..fefc440a 100644 --- a/source/DiskImageHelper.cpp +++ b/source/DiskImageHelper.cpp @@ -70,8 +70,8 @@ LPBYTE CImageBase::ms_pWorkBuffer = NULL; bool CImageBase::ReadTrack(ImageInfo* pImageInfo, const int nTrack, LPBYTE pTrackBuffer, const UINT uTrackSize) { - const long Offset = pImageInfo->uOffset + nTrack * uTrackSize; - memcpy(pTrackBuffer, &pImageInfo->pImageBuffer[Offset], uTrackSize); + const long offset = pImageInfo->uOffset + nTrack * uTrackSize; + memcpy(pTrackBuffer, &pImageInfo->pImageBuffer[offset], uTrackSize); return true; } @@ -630,18 +630,20 @@ public: return ePossibleMatch; } - virtual void Read(ImageInfo* pImageInfo, int nTrack, int nQuarterTrack, LPBYTE pTrackImageBuffer, int* pNibbles, bool enhanceDisk) + virtual void Read(ImageInfo* pImageInfo, const float phase, LPBYTE pTrackImageBuffer, int* pNibbles, UINT* pBitCount, bool enhanceDisk) { - ReadTrack(pImageInfo, nTrack, ms_pWorkBuffer, TRACK_DENIBBLIZED_SIZE); - *pNibbles = NibblizeTrack(pTrackImageBuffer, eDOSOrder, nTrack); + const UINT track = PhaseToTrack(phase); + ReadTrack(pImageInfo, track, ms_pWorkBuffer, TRACK_DENIBBLIZED_SIZE); + *pNibbles = NibblizeTrack(pTrackImageBuffer, eDOSOrder, track); if (!enhanceDisk) - SkewTrack(nTrack, *pNibbles, pTrackImageBuffer); + SkewTrack(track, *pNibbles, pTrackImageBuffer); } - virtual void Write(ImageInfo* pImageInfo, int nTrack, int nQuarterTrack, LPBYTE pTrackImage, int nNibbles) + virtual void Write(ImageInfo* pImageInfo, const float phase, LPBYTE pTrackImageBuffer, int nNibbles) { - DenibblizeTrack(pTrackImage, eDOSOrder, nNibbles); - WriteTrack(pImageInfo, nTrack, ms_pWorkBuffer, TRACK_DENIBBLIZED_SIZE); + const UINT track = PhaseToTrack(phase); + DenibblizeTrack(pTrackImageBuffer, eDOSOrder, nNibbles); + WriteTrack(pImageInfo, track, ms_pWorkBuffer, TRACK_DENIBBLIZED_SIZE); } virtual bool AllowCreate(void) { return true; } @@ -696,23 +698,25 @@ public: return ePossibleMatch; } - virtual void Read(ImageInfo* pImageInfo, int nTrack, int nQuarterTrack, LPBYTE pTrackImageBuffer, int* pNibbles, bool enhanceDisk) + virtual void Read(ImageInfo* pImageInfo, const float phase, LPBYTE pTrackImageBuffer, int* pNibbles, UINT* pBitCount, bool enhanceDisk) { - ReadTrack(pImageInfo, nTrack, ms_pWorkBuffer, TRACK_DENIBBLIZED_SIZE); - *pNibbles = NibblizeTrack(pTrackImageBuffer, eProDOSOrder, nTrack); + const UINT track = PhaseToTrack(phase); + ReadTrack(pImageInfo, track, ms_pWorkBuffer, TRACK_DENIBBLIZED_SIZE); + *pNibbles = NibblizeTrack(pTrackImageBuffer, eProDOSOrder, track); if (!enhanceDisk) - SkewTrack(nTrack, *pNibbles, pTrackImageBuffer); + SkewTrack(track, *pNibbles, pTrackImageBuffer); } - virtual void Write(ImageInfo* pImageInfo, int nTrack, int nQuarterTrack, LPBYTE pTrackImage, int nNibbles) + virtual void Write(ImageInfo* pImageInfo, const float phase, LPBYTE pTrackImageBuffer, int nNibbles) { - DenibblizeTrack(pTrackImage, eProDOSOrder, nNibbles); - WriteTrack(pImageInfo, nTrack, ms_pWorkBuffer, TRACK_DENIBBLIZED_SIZE); + const UINT track = PhaseToTrack(phase); + DenibblizeTrack(pTrackImageBuffer, eProDOSOrder, nNibbles); + WriteTrack(pImageInfo, track, ms_pWorkBuffer, TRACK_DENIBBLIZED_SIZE); } virtual eImageType GetType(void) { return eImagePO; } virtual const char* GetCreateExtensions(void) { return ".po"; } - virtual const char* GetRejectExtensions(void) { return ".do;.iie;.nib;.prg"; } + virtual const char* GetRejectExtensions(void) { return ".do;.iie;.nib;.prg;.woz"; } }; //------------------------------------- @@ -724,7 +728,7 @@ public: CNib1Image(void) {} virtual ~CNib1Image(void) {} - static const UINT NIB1_TRACK_SIZE = NIBBLES_PER_TRACK; + static const UINT NIB1_TRACK_SIZE = NIBBLES_PER_TRACK_NIB; virtual eDetectResult Detect(const LPBYTE pImage, const DWORD dwImageSize, const TCHAR* pszExt) { @@ -735,16 +739,18 @@ public: return eMatch; } - virtual void Read(ImageInfo* pImageInfo, int nTrack, int nQuarterTrack, LPBYTE pTrackImageBuffer, int* pNibbles, bool enhanceDisk) + virtual void Read(ImageInfo* pImageInfo, const float phase, LPBYTE pTrackImageBuffer, int* pNibbles, UINT* pBitCount, bool enhanceDisk) { - ReadTrack(pImageInfo, nTrack, pTrackImageBuffer, NIB1_TRACK_SIZE); + const UINT track = PhaseToTrack(phase); + ReadTrack(pImageInfo, track, pTrackImageBuffer, NIB1_TRACK_SIZE); *pNibbles = NIB1_TRACK_SIZE; } - virtual void Write(ImageInfo* pImageInfo, int nTrack, int nQuarterTrack, LPBYTE pTrackImage, int nNibbles) + virtual void Write(ImageInfo* pImageInfo, const float phase, LPBYTE pTrackImageBuffer, int nNibbles) { _ASSERT(nNibbles == NIB1_TRACK_SIZE); // Must be true - as nNibbles gets init'd by ImageReadTrace() - WriteTrack(pImageInfo, nTrack, pTrackImage, nNibbles); + const UINT track = PhaseToTrack(phase); + WriteTrack(pImageInfo, track, pTrackImageBuffer, nNibbles); } virtual bool AllowCreate(void) { return true; } @@ -752,7 +758,7 @@ public: virtual eImageType GetType(void) { return eImageNIB1; } virtual const char* GetCreateExtensions(void) { return ".nib"; } - virtual const char* GetRejectExtensions(void) { return ".do;.iie;.po;.prg"; } + virtual const char* GetRejectExtensions(void) { return ".do;.iie;.po;.prg;.woz"; } }; //------------------------------------- @@ -775,21 +781,23 @@ public: return eMatch; } - virtual void Read(ImageInfo* pImageInfo, int nTrack, int nQuarterTrack, LPBYTE pTrackImageBuffer, int* pNibbles, bool enhanceDisk) + virtual void Read(ImageInfo* pImageInfo, const float phase, LPBYTE pTrackImageBuffer, int* pNibbles, UINT* pBitCount, bool enhanceDisk) { - ReadTrack(pImageInfo, nTrack, pTrackImageBuffer, NIB2_TRACK_SIZE); + const UINT track = PhaseToTrack(phase); + ReadTrack(pImageInfo, track, pTrackImageBuffer, NIB2_TRACK_SIZE); *pNibbles = NIB2_TRACK_SIZE; } - virtual void Write(ImageInfo* pImageInfo, int nTrack, int nQuarterTrack, LPBYTE pTrackImage, int nNibbles) + virtual void Write(ImageInfo* pImageInfo, const float phase, LPBYTE pTrackImageBuffer, int nNibbles) { _ASSERT(nNibbles == NIB2_TRACK_SIZE); // Must be true - as nNibbles gets init'd by ImageReadTrace() - WriteTrack(pImageInfo, nTrack, pTrackImage, nNibbles); + const UINT track = PhaseToTrack(phase); + WriteTrack(pImageInfo, track, pTrackImageBuffer, nNibbles); } virtual eImageType GetType(void) { return eImageNIB2; } virtual const char* GetCreateExtensions(void) { return ".nb2"; } - virtual const char* GetRejectExtensions(void) { return ".do;.iie;.po;.prg;.2mg;.2img"; } + virtual const char* GetRejectExtensions(void) { return ".do;.iie;.po;.prg;.woz;.2mg;.2img"; } }; //------------------------------------- @@ -851,8 +859,10 @@ public: return eMatch; } - virtual void Read(ImageInfo* pImageInfo, int nTrack, int nQuarterTrack, LPBYTE pTrackImageBuffer, int* pNibbles, bool enhanceDisk) + virtual void Read(ImageInfo* pImageInfo, const float phase, LPBYTE pTrackImageBuffer, int* pNibbles, UINT* pBitCount, bool enhanceDisk) { + UINT track = PhaseToTrack(phase); + // IF WE HAVEN'T ALREADY DONE SO, READ THE IMAGE FILE HEADER if (!m_pHeader) { @@ -872,19 +882,19 @@ public: if (*(m_pHeader+13) <= 2) { ConvertSectorOrder(m_pHeader+14); - SetFilePointer(pImageInfo->hFile, nTrack*TRACK_DENIBBLIZED_SIZE+30, NULL, FILE_BEGIN); + SetFilePointer(pImageInfo->hFile, track*TRACK_DENIBBLIZED_SIZE+30, NULL, FILE_BEGIN); ZeroMemory(ms_pWorkBuffer, TRACK_DENIBBLIZED_SIZE); DWORD bytesread; ReadFile(pImageInfo->hFile, ms_pWorkBuffer, TRACK_DENIBBLIZED_SIZE, &bytesread, NULL); - *pNibbles = NibblizeTrack(pTrackImageBuffer, eSIMSYSTEMOrder, nTrack); + *pNibbles = NibblizeTrack(pTrackImageBuffer, eSIMSYSTEMOrder, track); } // OTHERWISE, IF THIS IMAGE CONTAINS NIBBLE INFORMATION, READ IT DIRECTLY INTO THE TRACK BUFFER else { - *pNibbles = *(LPWORD)(m_pHeader+nTrack*2+14); + *pNibbles = *(LPWORD)(m_pHeader+track*2+14); LONG Offset = 88; - while (nTrack--) - Offset += *(LPWORD)(m_pHeader+nTrack*2+14); + while (track--) + Offset += *(LPWORD)(m_pHeader+track*2+14); SetFilePointer(pImageInfo->hFile, Offset, NULL,FILE_BEGIN); ZeroMemory(pTrackImageBuffer, *pNibbles); DWORD dwBytesRead; @@ -892,14 +902,14 @@ public: } } - virtual void Write(ImageInfo* pImageInfo, int nTrack, int nQuarterTrack, LPBYTE pTrackImage, int nNibbles) + virtual void Write(ImageInfo* pImageInfo, const float phase, LPBYTE pTrackImageBuffer, int nNibbles) { // note: unimplemented } virtual eImageType GetType(void) { return eImageIIE; } virtual const char* GetCreateExtensions(void) { return ".iie"; } - virtual const char* GetRejectExtensions(void) { return ".do.;.nib;.po;.prg;.2mg;.2img"; } + virtual const char* GetRejectExtensions(void) { return ".do.;.nib;.po;.prg;.woz;.2mg;.2img"; } private: void ConvertSectorOrder(LPBYTE sourceorder) @@ -973,7 +983,7 @@ public: virtual eImageType GetType(void) { return eImageAPL; } virtual const char* GetCreateExtensions(void) { return ".apl"; } - virtual const char* GetRejectExtensions(void) { return ".do;.dsk;.iie;.nib;.po;.2mg;.2img"; } + virtual const char* GetRejectExtensions(void) { return ".do;.dsk;.iie;.nib;.po;.woz;.2mg;.2img"; } }; //------------------------------------- @@ -1024,7 +1034,154 @@ public: virtual eImageType GetType(void) { return eImagePRG; } virtual const char* GetCreateExtensions(void) { return ".prg"; } - virtual const char* GetRejectExtensions(void) { return ".do;.dsk;.iie;.nib;.po;.2mg;.2img"; } + virtual const char* GetRejectExtensions(void) { return ".do;.dsk;.iie;.nib;.po;.woz;.2mg;.2img"; } +}; + +//------------------------------------- + +class CWOZEmptyTrack +{ +public: + CWOZEmptyTrack(void) + { + m_pWOZEmptyTrack = new BYTE[CWOZHelper::EMPTY_TRACK_SIZE]; + + srand(1); // Use a fixed seed for determinism + for (UINT i = 0; i < CWOZHelper::EMPTY_TRACK_SIZE; i++) + { + BYTE n = 0; + for (UINT j = 0; j < 8; j++) + { + if (rand() < ((RAND_MAX * 3) / 10)) // ~30% of buffer are 1 bits + n |= 1 << j; + } + m_pWOZEmptyTrack[i] = n; + } + } + virtual ~CWOZEmptyTrack(void) { delete m_pWOZEmptyTrack; } + + void ReadEmptyTrack(LPBYTE pTrackImageBuffer, int* pNibbles, UINT* pBitCount) + { + memcpy(pTrackImageBuffer, m_pWOZEmptyTrack, CWOZHelper::EMPTY_TRACK_SIZE); + *pNibbles = CWOZHelper::EMPTY_TRACK_SIZE; + *pBitCount = CWOZHelper::EMPTY_TRACK_SIZE * 8; + return; + } + +private: + BYTE* m_pWOZEmptyTrack; +}; + +//------------------------------------- + +class CWOZ1Image : public CImageBase, private CWOZEmptyTrack +{ +public: + CWOZ1Image(void) {} + virtual ~CWOZ1Image(void) {} + + virtual eDetectResult Detect(const LPBYTE pImage, const DWORD dwImageSize, const TCHAR* pszExt) + { + CWOZHelper::WOZHeader* pWozHdr = (CWOZHelper::WOZHeader*) pImage; + + if (pWozHdr->id1 != CWOZHelper::ID1_WOZ1 || pWozHdr->id2 != CWOZHelper::ID2) + return eMismatch; + + if (pWozHdr->crc32) + { + // TODO: check crc + } + + m_uNumTracksInImage = CWOZHelper::MAX_TRACKS_5_25; + return eMatch; + } + + virtual void Read(ImageInfo* pImageInfo, const float phase, LPBYTE pTrackImageBuffer, int* pNibbles, UINT* pBitCount, bool enhanceDisk) + { + BYTE*& pTrackMap = pImageInfo->pTrackMap; + + const int trackFromTMAP = pTrackMap[(UINT)(phase * 2)]; + if (trackFromTMAP == 0xFF) + return ReadEmptyTrack(pTrackImageBuffer, pNibbles, pBitCount); + + ReadTrack(pImageInfo, trackFromTMAP, pTrackImageBuffer, CWOZHelper::WOZ1_TRACK_SIZE); + CWOZHelper::TRKv1* pTRK = (CWOZHelper::TRKv1*) &pTrackImageBuffer[CWOZHelper::WOZ1_TRK_OFFSET]; + *pNibbles = pTRK->bytesUsed; + *pBitCount = pTRK->bitCount; + } + + virtual void Write(ImageInfo* pImageInfo, const float phase, LPBYTE pTrackImageBuffer, int nNibbles) + { + // TODO + _ASSERT(0); + } + + // TODO: Uncomment and fix-up if we want to allow .woz image creation (eg. for INIT or FORMAT) +// virtual bool AllowCreate(void) { return true; } +// virtual UINT GetImageSizeForCreate(void) { return 0; }//TODO + + virtual eImageType GetType(void) { return eImageWOZ1; } + virtual const char* GetCreateExtensions(void) { return ".woz"; } + virtual const char* GetRejectExtensions(void) { return ".do;.dsk;.nib;.iie;.po;.prg"; } +}; + +//------------------------------------- + +class CWOZ2Image : public CImageBase, private CWOZEmptyTrack +{ +public: + CWOZ2Image(void) {} + virtual ~CWOZ2Image(void) {} + + virtual eDetectResult Detect(const LPBYTE pImage, const DWORD dwImageSize, const TCHAR* pszExt) + { + CWOZHelper::WOZHeader* pWozHdr = (CWOZHelper::WOZHeader*) pImage; + + if (pWozHdr->id1 != CWOZHelper::ID1_WOZ2 || pWozHdr->id2 != CWOZHelper::ID2) + return eMismatch; + + if (pWozHdr->crc32) + { + // TODO: check crc + } + + m_uNumTracksInImage = CWOZHelper::MAX_TRACKS_5_25; + return eMatch; + } + + virtual void Read(ImageInfo* pImageInfo, const float phase, LPBYTE pTrackImageBuffer, int* pNibbles, UINT* pBitCount, bool enhanceDisk) + { + BYTE*& pTrackMap = pImageInfo->pTrackMap; + + const int trackFromTMAP = pTrackMap[(UINT)(phase * 2)]; + if (trackFromTMAP == 0xFF) + return ReadEmptyTrack(pTrackImageBuffer, pNibbles, pBitCount); + + CWOZHelper::TRKv2* pTRKS = (CWOZHelper::TRKv2*) &pImageInfo->pImageBuffer[pImageInfo->uOffset]; + CWOZHelper::TRKv2* pTRK = &pTRKS[trackFromTMAP]; + *pBitCount = pTRK->bitCount; + *pNibbles = (pTRK->bitCount+7) / 8; + + _ASSERT(*pNibbles <= NIBBLES_PER_TRACK_WOZ2); + if (*pNibbles > NIBBLES_PER_TRACK_WOZ2) + return ReadEmptyTrack(pTrackImageBuffer, pNibbles, pBitCount); // TODO: Enlarge track buffer, but for now just return an empty track + + memcpy(pTrackImageBuffer, &pImageInfo->pImageBuffer[pTRK->startBlock*512], *pNibbles); + } + + virtual void Write(ImageInfo* pImageInfo, const float phase, LPBYTE pTrackImageBuffer, int nNibbles) + { + // TODO + _ASSERT(0); + } + + // TODO: Uncomment and fix-up if we want to allow .woz image creation (eg. for INIT or FORMAT) + // virtual bool AllowCreate(void) { return true; } + // virtual UINT GetImageSizeForCreate(void) { return 0; }//TODO + + virtual eImageType GetType(void) { return eImageWOZ2; } + virtual const char* GetCreateExtensions(void) { return ".woz"; } + virtual const char* GetRejectExtensions(void) { return ".do;.dsk;.nib;.iie;.po;.prg"; } }; //----------------------------------------------------------------------------- @@ -1048,6 +1205,8 @@ eDetectResult CMacBinaryHelper::DetectHdr(LPBYTE& pImage, DWORD& dwImageSize, DW return eMismatch; } +//----------------------------------------------------------------------------- + eDetectResult C2IMGHelper::DetectHdr(LPBYTE& pImage, DWORD& dwImageSize, DWORD& dwOffset) { Header2IMG* pHdr = (Header2IMG*) pImage; @@ -1099,7 +1258,7 @@ eDetectResult C2IMGHelper::DetectHdr(LPBYTE& pImage, DWORD& dwImageSize, DWORD& break; case e2IMGFormatNIBData: { - if (pHdr->DiskDataLength != TRACKS_STANDARD*NIBBLES_PER_TRACK) + if (pHdr->DiskDataLength != TRACKS_STANDARD*NIBBLES_PER_TRACK_NIB) return eMismatch; } break; @@ -1126,11 +1285,59 @@ bool C2IMGHelper::IsLocked(void) //----------------------------------------------------------------------------- +// Pre: already matched the WOZ header +eDetectResult CWOZHelper::ProcessChunks(const LPBYTE pImage, const DWORD dwImageSize, DWORD& dwOffset, BYTE*& pTrackMap) +{ + UINT32* pImage32 = (uint32_t*) (pImage + sizeof(WOZHeader)); + UINT32 imageSizeRemaining = dwImageSize - sizeof(WOZHeader); + + while(imageSizeRemaining > 8) + { + UINT32 chunkId = *pImage32++; + UINT32 chunkSize = *pImage32++; + imageSizeRemaining -= 8; + + switch(chunkId) + { + case INFO_CHUNK_ID: + m_pInfo = (InfoChunkv2*)(pImage32-2); + if (m_pInfo->v1.version > InfoChunk::maxSupportedVersion) + return eMismatch; + if (m_pInfo->v1.diskType != InfoChunk::diskType5_25) + return eMismatch; + break; + case TMAP_CHUNK_ID: + pTrackMap = (uint8_t*)pImage32; + break; + case TRKS_CHUNK_ID: + dwOffset = dwImageSize - imageSizeRemaining; // offset into image of track data + break; + case WRIT_CHUNK_ID: // WOZ v2 (optional) + break; + case META_CHUNK_ID: // (optional) + break; + default: // no idea what this chunk is, so skip it + _ASSERT(0); + break; + } + + pImage32 = (UINT32*) ((BYTE*)pImage32 + chunkSize); + imageSizeRemaining -= chunkSize; + _ASSERT(imageSizeRemaining >= 0); + if (imageSizeRemaining < 0) + return eMismatch; + } + + return eMatch; +} + +//----------------------------------------------------------------------------- + // NB. Of the 6 cases (floppy/harddisk x gzip/zip/normal) only harddisk-normal isn't read entirely to memory // - harddisk-normal-create also doesn't create a max size image-buffer // DETERMINE THE FILE'S EXTENSION AND CONVERT IT TO LOWERCASE -void GetCharLowerExt(TCHAR* pszExt, LPCTSTR pszImageFilename, const UINT uExtSize) +void CImageHelperBase::GetCharLowerExt(TCHAR* pszExt, LPCTSTR pszImageFilename, const UINT uExtSize) { LPCTSTR pImageFileExt = pszImageFilename; @@ -1146,7 +1353,7 @@ void GetCharLowerExt(TCHAR* pszExt, LPCTSTR pszImageFilename, const UINT uExtSiz CharLowerBuff(pszExt, _tcslen(pszExt)); } -void GetCharLowerExt2(TCHAR* pszExt, LPCTSTR pszImageFilename, const UINT uExtSize) +void CImageHelperBase::GetCharLowerExt2(TCHAR* pszExt, LPCTSTR pszImageFilename, const UINT uExtSize) { TCHAR szFilename[MAX_PATH]; _tcsncpy(szFilename, pszImageFilename, MAX_PATH); @@ -1187,7 +1394,7 @@ ImageError_e CImageHelperBase::CheckGZipFile(LPCTSTR pszImageFilename, ImageInfo DWORD dwSize = nLen; DWORD dwOffset = 0; - CImageBase* pImageType = Detect(pImageInfo->pImageBuffer, dwSize, szExt, dwOffset, &pImageInfo->bWriteProtected); + CImageBase* pImageType = Detect(pImageInfo->pImageBuffer, dwSize, szExt, dwOffset, pImageInfo->bWriteProtected, pImageInfo->pTrackMap, pImageInfo->optimalBitTiming); if (!pImageType) return eIMAGE_ERROR_UNSUPPORTED; @@ -1196,11 +1403,7 @@ ImageError_e CImageHelperBase::CheckGZipFile(LPCTSTR pszImageFilename, ImageInfo if (Type == eImageAPL || Type == eImageIIE || Type == eImagePRG) return eIMAGE_ERROR_UNSUPPORTED; - pImageInfo->FileType = eFileGZip; - pImageInfo->uOffset = dwOffset; - pImageInfo->pImageType = pImageType; - pImageInfo->uImageSize = dwSize; - + SetImageInfo(pImageInfo, eFileGZip, dwOffset, pImageType, dwSize); return eIMAGE_ERROR_NONE; } @@ -1283,7 +1486,7 @@ ImageError_e CImageHelperBase::CheckZipFile(LPCTSTR pszImageFilename, ImageInfo* DWORD dwSize = nLen; DWORD dwOffset = 0; - CImageBase* pImageType = Detect(pImageInfo->pImageBuffer, dwSize, szExt, dwOffset, &pImageInfo->bWriteProtected); + CImageBase* pImageType = Detect(pImageInfo->pImageBuffer, dwSize, szExt, dwOffset, pImageInfo->bWriteProtected, pImageInfo->pTrackMap, pImageInfo->optimalBitTiming); if (!pImageType) { @@ -1300,11 +1503,7 @@ ImageError_e CImageHelperBase::CheckZipFile(LPCTSTR pszImageFilename, ImageInfo* if (global_info.number_entry > 1) pImageInfo->bWriteProtected = 1; // Zip archives with multiple files are read-only (for now) - pImageInfo->FileType = eFileZip; - pImageInfo->uOffset = dwOffset; - pImageInfo->pImageType = pImageType; - pImageInfo->uImageSize = dwSize; - + SetImageInfo(pImageInfo, eFileZip, dwOffset, pImageType, dwSize); return eIMAGE_ERROR_NONE; } @@ -1384,7 +1583,7 @@ ImageError_e CImageHelperBase::CheckNormalFile(LPCTSTR pszImageFilename, ImageIn return eIMAGE_ERROR_BAD_SIZE; } - pImageType = Detect(pImageInfo->pImageBuffer, dwSize, szExt, dwOffset, &pImageInfo->bWriteProtected); + pImageType = Detect(pImageInfo->pImageBuffer, dwSize, szExt, dwOffset, pImageInfo->bWriteProtected, pImageInfo->pTrackMap, pImageInfo->optimalBitTiming); if (bTempDetectBuffer) { delete [] pImageInfo->pImageBuffer; @@ -1437,12 +1636,18 @@ ImageError_e CImageHelperBase::CheckNormalFile(LPCTSTR pszImageFilename, ImageIn return eIMAGE_ERROR_UNSUPPORTED; } - pImageInfo->FileType = eFileNormal; + SetImageInfo(pImageInfo, eFileNormal, dwOffset, pImageType, dwSize); + return eIMAGE_ERROR_NONE; +} + +//------------------------------------- + +void CImageHelperBase::SetImageInfo(ImageInfo* pImageInfo, FileType_e eFileGZip, DWORD dwOffset, CImageBase* pImageType, DWORD dwSize) +{ + pImageInfo->FileType = eFileGZip; pImageInfo->uOffset = dwOffset; pImageInfo->pImageType = pImageType; pImageInfo->uImageSize = dwSize; - - return eIMAGE_ERROR_NONE; } //------------------------------------- @@ -1517,54 +1722,68 @@ CDiskImageHelper::CDiskImageHelper(void) : m_vecImageTypes.push_back( new CIIeImage ); m_vecImageTypes.push_back( new CAplImage ); m_vecImageTypes.push_back( new CPrgImage ); + m_vecImageTypes.push_back( new CWOZ1Image ); + m_vecImageTypes.push_back( new CWOZ2Image ); } -CImageBase* CDiskImageHelper::Detect(LPBYTE pImage, DWORD dwSize, const TCHAR* pszExt, DWORD& dwOffset, bool* pWriteProtected_) +CImageBase* CDiskImageHelper::Detect(LPBYTE pImage, DWORD dwSize, const TCHAR* pszExt, DWORD& dwOffset, bool& writeProtected, BYTE*& pTrackMap, BYTE& optimalBitTiming) { dwOffset = 0; m_MacBinaryHelper.DetectHdr(pImage, dwSize, dwOffset); m_Result2IMG = m_2IMGHelper.DetectHdr(pImage, dwSize, dwOffset); // CALL THE DETECTION FUNCTIONS IN ORDER, LOOKING FOR A MATCH - eImageType ImageType = eImageUNKNOWN; - eImageType PossibleType = eImageUNKNOWN; + eImageType imageType = eImageUNKNOWN; + eImageType possibleType = eImageUNKNOWN; if (m_Result2IMG == eMatch) { if (m_2IMGHelper.IsImageFormatDOS33()) - ImageType = eImageDO; + imageType = eImageDO; else if (m_2IMGHelper.IsImageFormatProDOS()) - ImageType = eImagePO; + imageType = eImagePO; - if (ImageType != eImageUNKNOWN) + if (imageType != eImageUNKNOWN) { - CImageBase* pImageType = GetImage(ImageType); + CImageBase* pImageType = GetImage(imageType); if (!pImageType || !pImageType->IsValidImageSize(dwSize)) - ImageType = eImageUNKNOWN; + imageType = eImageUNKNOWN; } } - if (ImageType == eImageUNKNOWN) + if (imageType == eImageUNKNOWN) { - for (UINT uLoop=0; uLoop < GetNumImages() && ImageType == eImageUNKNOWN; uLoop++) + for (UINT uLoop=0; uLoop < GetNumImages() && imageType == eImageUNKNOWN; uLoop++) { if (*pszExt && _tcsstr(GetImage(uLoop)->GetRejectExtensions(), pszExt)) continue; eDetectResult Result = GetImage(uLoop)->Detect(pImage, dwSize, pszExt); if (Result == eMatch) - ImageType = GetImage(uLoop)->GetType(); - else if ((Result == ePossibleMatch) && (PossibleType == eImageUNKNOWN)) - PossibleType = GetImage(uLoop)->GetType(); + imageType = GetImage(uLoop)->GetType(); + else if ((Result == ePossibleMatch) && (possibleType == eImageUNKNOWN)) + possibleType = GetImage(uLoop)->GetType(); } } - if (ImageType == eImageUNKNOWN) - ImageType = PossibleType; + if (imageType == eImageUNKNOWN) + imageType = possibleType; - CImageBase* pImageType = GetImage(ImageType); + CImageBase* pImageType = GetImage(imageType); + if (!pImageType) + return NULL; - if (pImageType) + if (imageType == eImageWOZ1 || imageType == eImageWOZ2) + { + if (m_WOZHelper.ProcessChunks(pImage, dwSize, dwOffset, pTrackMap) != eMatch) + return NULL; + +// if (m_WOZHelper.IsWriteProtected() && !writeProtected) // Force write-protected until writing is supported + writeProtected = true; + + optimalBitTiming = m_WOZHelper.GetOptimalBitTiming(); + } + else { if (pImageType->AllowRW()) { @@ -1578,8 +1797,8 @@ CImageBase* CDiskImageHelper::Detect(LPBYTE pImage, DWORD dwSize, const TCHAR* p { pImageType->SetVolumeNumber( m_2IMGHelper.GetVolumeNumber() ); - if (m_2IMGHelper.IsLocked() && !*pWriteProtected_) - *pWriteProtected_ = 1; + if (m_2IMGHelper.IsLocked() && !writeProtected) + writeProtected = true; } else { @@ -1634,7 +1853,7 @@ CHardDiskImageHelper::CHardDiskImageHelper(void) : m_vecImageTypes.push_back( new CHDVImage ); } -CImageBase* CHardDiskImageHelper::Detect(LPBYTE pImage, DWORD dwSize, const TCHAR* pszExt, DWORD& dwOffset, bool* pWriteProtected_) +CImageBase* CHardDiskImageHelper::Detect(LPBYTE pImage, DWORD dwSize, const TCHAR* pszExt, DWORD& dwOffset, bool& writeProtected, BYTE*& pTrackMap, BYTE& optimalBitTiming) { dwOffset = 0; m_Result2IMG = m_2IMGHelper.DetectHdr(pImage, dwSize, dwOffset); @@ -1659,11 +1878,14 @@ CImageBase* CHardDiskImageHelper::Detect(LPBYTE pImage, DWORD dwSize, const TCHA { if (m_Result2IMG == eMatch) { - if (m_2IMGHelper.IsLocked() && !*pWriteProtected_) - *pWriteProtected_ = 1; + if (m_2IMGHelper.IsLocked() && !writeProtected) + writeProtected = true; } } + pTrackMap = 0; // TODO: WOZ + optimalBitTiming = 0; // TODO: WOZ + return pImageType; } diff --git a/source/DiskImageHelper.h b/source/DiskImageHelper.h index a6e8b773..cf64bdac 100644 --- a/source/DiskImageHelper.h +++ b/source/DiskImageHelper.h @@ -10,7 +10,7 @@ #define ZIP_SUFFIX_LEN (sizeof(ZIP_SUFFIX)-1) -enum eImageType {eImageUNKNOWN, eImageDO, eImagePO, eImageNIB1, eImageNIB2, eImageHDV, eImageIIE, eImageAPL, eImagePRG}; +enum eImageType {eImageUNKNOWN, eImageDO, eImagePO, eImageNIB1, eImageNIB2, eImageHDV, eImageIIE, eImageAPL, eImagePRG, eImageWOZ1, eImageWOZ2}; enum eDetectResult {eMismatch, ePossibleMatch, eMatch}; class CImageBase; @@ -35,6 +35,8 @@ struct ImageInfo BYTE ValidTrack[TRACKS_MAX]; UINT uNumTracks; BYTE* pImageBuffer; + BYTE* pTrackMap; // WOZ only + BYTE optimalBitTiming; // WOZ only }; //------------------------------------- @@ -54,9 +56,9 @@ public: virtual bool Boot(ImageInfo* pImageInfo) { return false; } virtual eDetectResult Detect(const LPBYTE pImage, const DWORD dwImageSize, const TCHAR* pszExt) = 0; - virtual void Read(ImageInfo* pImageInfo, int nTrack, int nQuarterTrack, LPBYTE pTrackImageBuffer, int* pNibbles, bool enhanceDisk) { } + virtual void Read(ImageInfo* pImageInfo, const float phase, LPBYTE pTrackImageBuffer, int* pNibbles, UINT* pBitCount, bool enhanceDisk) { } virtual bool Read(ImageInfo* pImageInfo, UINT nBlock, LPBYTE pBlockBuffer) { return false; } - virtual void Write(ImageInfo* pImageInfo, int nTrack, int nQuarterTrack, LPBYTE pTrackImage, int nNibbles) { } + virtual void Write(ImageInfo* pImageInfo, const float phase, LPBYTE pTrackImageBuffer, int nNibbles) { } virtual bool Write(ImageInfo* pImageInfo, UINT nBlock, LPBYTE pBlockBuffer) { return false; } virtual bool AllowBoot(void) { return false; } // Only: APL and PRG @@ -71,6 +73,11 @@ public: void SetVolumeNumber(const BYTE uVolumeNumber) { m_uVolumeNumber = uVolumeNumber; } bool IsValidImageSize(const DWORD uImageSize); + // To accurately convert a half phase (quarter track) back to a track (round half tracks down), use: ceil(phase)/2, eg: + // . phase=4,+1 half phase = phase 4.5 => ceil(4.5)/2 = track 2 (OK) + // . phase=4,-1 half phase = phase 3.5 => ceil(3.5)/2 = track 2 (OK) + UINT PhaseToTrack(const float phase) { return ((UINT)ceil(phase)) >> 1; } + enum SectorOrder_e {eProDOSOrder, eDOSOrder, eSIMSYSTEMOrder, NUM_SECTOR_ORDERS}; protected: @@ -122,7 +129,7 @@ private: // http://apple2.org.za/gswv/a2zine/Docs/DiskImage_2MG_Info.txt #pragma pack(push) -#pragma pack(1) // Ensure Header2IMG is packed +#pragma pack(1) // Ensure Header2IMG & WOZ structs are packed class C2IMGHelper : public CHdrHelper { @@ -181,6 +188,98 @@ private: bool m_bIsFloppy; }; +class CWOZHelper : public CHdrHelper +{ +public: + CWOZHelper() : + m_pInfo(NULL) + {} + virtual ~CWOZHelper(void) {} + virtual eDetectResult DetectHdr(LPBYTE& pImage, DWORD& dwImageSize, DWORD& dwOffset) { _ASSERT(0); return eMismatch; } + virtual UINT GetMaxHdrSize(void) { return sizeof(WOZHeader); } + eDetectResult ProcessChunks(const LPBYTE pImage, const DWORD dwImageSize, DWORD& dwOffset, BYTE*& pTrackMap); + bool IsWriteProtected(void) { return m_pInfo->v1.writeProtected == 1; } + BYTE GetOptimalBitTiming(void) { return (m_pInfo->v1.version == 1) ? CWOZHelper::InfoChunkv2::optimalBitTiming5_25 : m_pInfo->optimalBitTiming; } + + static const UINT32 ID1_WOZ1 = '1ZOW'; // 'WOZ1' + static const UINT32 ID1_WOZ2 = '2ZOW'; // 'WOZ2' + static const UINT32 ID2 = 0x0A0D0AFF; + + struct WOZHeader + { + UINT32 id1; // 'WOZ1' or 'WOZ2' + UINT32 id2; + UINT32 crc32; + }; + + static const UINT32 MAX_TRACKS_5_25 = 40; + static const UINT32 WOZ1_TRACK_SIZE = 6656; // 0x1A00 + static const UINT32 WOZ1_TRK_OFFSET = 6646; + static const UINT32 EMPTY_TRACK_SIZE = 6400; + + struct TRKv1 + { + UINT16 bytesUsed; + UINT16 bitCount; + UINT16 splicePoint; + BYTE spliceNibble; + BYTE spliceBitCount; + UINT16 reserved; + }; + + struct TRKv2 + { + UINT16 startBlock; // relative to start of file + UINT16 blockCount; // number of blocks for this BITS data + UINT32 bitCount; + }; + +private: + static const UINT32 INFO_CHUNK_ID = 'OFNI'; // 'INFO' + static const UINT32 TMAP_CHUNK_ID = 'PAMT'; // 'TMAP' + static const UINT32 TRKS_CHUNK_ID = 'SKRT'; // 'TRKS' + static const UINT32 WRIT_CHUNK_ID = 'TIRW'; // 'WRIT' - WOZv2 + static const UINT32 META_CHUNK_ID = 'ATEM'; // 'META' + + struct InfoChunk + { + UINT32 id; + UINT32 size; + BYTE version; + BYTE diskType; + BYTE writeProtected; // 1 = Floppy is write protected + BYTE synchronized; // 1 = Cross track sync was used during imaging + BYTE cleaned; // 1 = MC3470 fake bits have been removed + BYTE creator[32]; // Name of software that created the WOZ file. + // String in UTF-8. No BOM. Padded to 32 bytes + // using space character (0x20). + + static const BYTE maxSupportedVersion = 2; + static const BYTE diskType5_25 = 1; + static const BYTE diskType3_5 = 2; + }; + + struct InfoChunkv2 + { + InfoChunk v1; + BYTE diskSides; // 5.25 will always be 1; 3.5 can be 1 or 2 + BYTE bootSectorFormat; + BYTE optimalBitTiming; // in 125ns increments (And a standard bit rate for 5.25 disk would be 32 (4us)) + UINT16 compatibleHardware; + UINT16 requiredRAM; // in K (1024 bytes) + UINT16 largestTrack; // in blocks (512 bytes) + + static const BYTE bootUnknown = 0; + static const BYTE bootSector16 = 1; + static const BYTE bootSector13 = 2; + static const BYTE bootSectorBoth = 3; + + static const BYTE optimalBitTiming5_25 = 32; + }; + + InfoChunkv2* m_pInfo; +}; + #pragma pack(pop) //------------------------------------- @@ -190,7 +289,8 @@ class CImageHelperBase public: CImageHelperBase(const bool bIsFloppy) : m_2IMGHelper(bIsFloppy), - m_Result2IMG(eMismatch) + m_Result2IMG(eMismatch), + m_WOZHelper() { } virtual ~CImageHelperBase(void) @@ -202,7 +302,7 @@ public: ImageError_e Open(LPCTSTR pszImageFilename, ImageInfo* pImageInfo, const bool bCreateIfNecessary, std::string& strFilenameInZip); void Close(ImageInfo* pImageInfo, const bool bDeleteFile); - virtual CImageBase* Detect(LPBYTE pImage, DWORD dwSize, const TCHAR* pszExt, DWORD& dwOffset, bool* pWriteProtected_) = 0; + virtual CImageBase* Detect(LPBYTE pImage, DWORD dwSize, const TCHAR* pszExt, DWORD& dwOffset, bool& writeProtected, BYTE*& pTrackMap, BYTE& optimalBitTiming) = 0; virtual CImageBase* GetImageForCreation(const TCHAR* pszExt, DWORD* pCreateImageSize) = 0; virtual UINT GetMaxImageSize(void) = 0; virtual UINT GetMinDetectSize(const UINT uImageSize, bool* pTempDetectBuffer) = 0; @@ -211,6 +311,9 @@ protected: ImageError_e CheckGZipFile(LPCTSTR pszImageFilename, ImageInfo* pImageInfo); ImageError_e CheckZipFile(LPCTSTR pszImageFilename, ImageInfo* pImageInfo, std::string& strFilenameInZip); ImageError_e CheckNormalFile(LPCTSTR pszImageFilename, ImageInfo* pImageInfo, const bool bCreateIfNecessary); + void GetCharLowerExt(TCHAR* pszExt, LPCTSTR pszImageFilename, const UINT uExtSize); + void GetCharLowerExt2(TCHAR* pszExt, LPCTSTR pszImageFilename, const UINT uExtSize); + void SetImageInfo(ImageInfo* pImageInfo, FileType_e eFileGZip, DWORD dwOffset, CImageBase* pImageType, DWORD dwSize); UINT GetNumImages(void) { return m_vecImageTypes.size(); }; CImageBase* GetImage(UINT uIndex) { _ASSERT(uIndex= 2) keywaiting = (BOOL) yamlLoadHelper.LoadBool(SS_YAML_KEY_KEYWAITING); yamlLoadHelper.PopMap(); diff --git a/source/Memory.cpp b/source/Memory.cpp index 8c547203..da593286 100644 --- a/source/Memory.cpp +++ b/source/Memory.cpp @@ -1745,6 +1745,8 @@ void MemReset() g_eExpansionRomType = eExpRomNull; g_uPeripheralRomSlot = 0; + ZeroMemory(memdirty, 0x100); + // int iByte; diff --git a/source/SaveState_Structs_v1.h b/source/SaveState_Structs_v1.h index ce171c84..3a48aba5 100644 --- a/source/SaveState_Structs_v1.h +++ b/source/SaveState_Structs_v1.h @@ -92,7 +92,7 @@ struct DISK2_Unit DWORD spinning; DWORD writelight; int nibbles; - BYTE nTrack[NIBBLES_PER_TRACK]; + BYTE nTrack[NIBBLES_PER_TRACK_NIB]; }; struct SS_CARD_DISK2 diff --git a/source/YamlHelper.cpp b/source/YamlHelper.cpp index 0e9f96cb..dbcaddcd 100644 --- a/source/YamlHelper.cpp +++ b/source/YamlHelper.cpp @@ -342,6 +342,34 @@ std::string YamlLoadHelper::LoadString(const std::string& key) return value; } +float YamlLoadHelper::LoadFloat(const std::string key) +{ + bool bFound; + std::string value = m_yamlHelper.GetMapValue(*m_pMapYaml, key, bFound); + if (value == "") + { + m_bDoGetMapRemainder = false; + throw std::string(m_currentMapName + ": Missing: " + key); + } +#if (_MSC_VER >= 1900) + return strtof(value.c_str(), NULL); // MSVC++ 14.0 _MSC_VER == 1900 (Visual Studio 2015 version 14.0) +#else + return (float) strtod(value.c_str(), NULL); // NB. strtof() requires VS2015 +#endif +} + +double YamlLoadHelper::LoadDouble(const std::string key) +{ + bool bFound; + std::string value = m_yamlHelper.GetMapValue(*m_pMapYaml, key, bFound); + if (value == "") + { + m_bDoGetMapRemainder = false; + throw std::string(m_currentMapName + ": Missing: " + key); + } + return strtod(value.c_str(), NULL); +} + void YamlLoadHelper::LoadMemory(const LPBYTE pMemBase, const size_t size) { m_yamlHelper.LoadMemory(*m_pMapYaml, pMemBase, size); @@ -371,7 +399,7 @@ void YamlSaveHelper::SaveUint(const char* key, UINT value) void YamlSaveHelper::SaveHexUint4(const char* key, UINT value) { - Save("%s: 0x%01X\n", key, value); + Save("%s: 0x%01X\n", key, value & 0xf); } void YamlSaveHelper::SaveHexUint8(const char* key, UINT value) @@ -414,6 +442,16 @@ void YamlSaveHelper::SaveString(const char* key, const char* value) Save("%s: %s\n", key, (value[0] != 0) ? value : "\"\""); } +void YamlSaveHelper::SaveFloat(const char* key, float value) +{ + Save("%s: %f\n", key, value); +} + +void YamlSaveHelper::SaveDouble(const char* key, double value) +{ + Save("%s: %f\n", key, value); +} + // Pre: uMemSize must be multiple of 8 void YamlSaveHelper::SaveMemory(const LPBYTE pMemBase, const UINT uMemSize) { diff --git a/source/YamlHelper.h b/source/YamlHelper.h index c96cca2c..48f0999a 100644 --- a/source/YamlHelper.h +++ b/source/YamlHelper.h @@ -97,6 +97,8 @@ public: bool LoadBool(const std::string key); std::string LoadString_NoThrow(const std::string& key, bool& bFound); std::string LoadString(const std::string& key); + float LoadFloat(const std::string key); + double LoadDouble(const std::string key); void LoadMemory(const LPBYTE pMemBase, const size_t size); bool GetSubMap(const std::string key) @@ -213,7 +215,9 @@ public: void SaveHexUint32(const char* key, UINT value); void SaveHexUint64(const char* key, UINT64 value); void SaveBool(const char* key, bool value); - void SaveString(const char* key, const char* value); + void SaveString(const char* key, const char* value); + void SaveFloat(const char* key, float value); + void SaveDouble(const char* key, double value); void SaveMemory(const LPBYTE pMemBase, const UINT uMemSize); class Label From 8e5505c734cfaab33d396aa8e7122a7d85992665 Mon Sep 17 00:00:00 2001 From: tomcw Date: Sat, 6 Jul 2019 12:03:15 +0100 Subject: [PATCH 15/21] Fixed LOG_DISK to use CLK_6502_NTSC --- source/Disk.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/Disk.cpp b/source/Disk.cpp index 33b0355d..3d036a3c 100644 --- a/source/Disk.cpp +++ b/source/Disk.cpp @@ -288,7 +288,7 @@ void Disk2InterfaceCard::ReadTrack(const int drive, ULONG uExecutedCycles) #if LOG_DISK_TRACKS CpuCalcCycles(uExecutedCycles); const ULONG cycleDelta = (ULONG)(g_nCumulativeCycles - pDrive->m_lastStepperCycle); - LOG_DISK("track $%s read (time since last stepper %.3fms)\r\n", GetCurrentTrackString().c_str(), ((float)cycleDelta) / (CLK_6502 / 1000.0)); + LOG_DISK("track $%s read (time since last stepper %.3fms)\r\n", GetCurrentTrackString().c_str(), ((float)cycleDelta) / (CLK_6502_NTSC / 1000.0)); #endif const UINT32 currentPosition = pFloppy->m_byte; const UINT32 currentTrackLength = pFloppy->m_nibbles; @@ -515,7 +515,7 @@ void __stdcall Disk2InterfaceCard::ControlStepper(WORD, WORD address, BYTE, BYTE m_magnetStates, (address & 1) ? "on " : "off", address, - ((float)cycleDelta)/(CLK_6502/1000.0)); + ((float)cycleDelta)/(CLK_6502_NTSC/1000.0)); #endif } From a73038fb74e30eba122b52283da8daeb9b049de1 Mon Sep 17 00:00:00 2001 From: tomcw Date: Mon, 8 Jul 2019 21:14:31 +0100 Subject: [PATCH 16/21] Disk: fix LOGGING and comment typo --- source/Disk.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/source/Disk.cpp b/source/Disk.cpp index 3d036a3c..5b4f7f21 100644 --- a/source/Disk.cpp +++ b/source/Disk.cpp @@ -512,7 +512,7 @@ void __stdcall Disk2InterfaceCard::ControlStepper(WORD, WORD address, BYTE, BYTE (m_magnetStates >> 2) & 1, (m_magnetStates >> 1) & 1, (m_magnetStates >> 0) & 1, - m_magnetStates, + (address >> 1) & 3, // phase (address & 1) ? "on " : "off", address, ((float)cycleDelta)/(CLK_6502_NTSC/1000.0)); @@ -1004,9 +1004,9 @@ UINT Disk2InterfaceCard::GetBitCellDelta(const BYTE optimalBitTiming) FloppyDisk& floppy = m_floppyDrive[m_currDrive].m_disk; // NB. m_extraCycles is needed to retain accuracy: - // . Read latch #1: 0-> 9: cycleDelta= 9, bitCellDelta=2, extraCycles=1 - // . Read latch #2: 11->20: cycleDelta=11, bitCellDelta=2, extraCycles=3 - // . Overall: 0->20: cycleDelta=20, bitCellDelta=5, extraCycles=0 + // . Read latch #1: 0-> 9: cycleDelta= 9, bitCellDelta=2, extraCycles=1 + // . Read latch #2: 9->20: cycleDelta=11, bitCellDelta=2, extraCycles=3 + // . Overall: 0->20: cycleDelta=20, bitCellDelta=5, extraCycles=0 UINT bitCellDelta; #if 0 if (optimalBitTiming == 32) From 65e1d9a80e2c94bb3b60dccbd3e840c8ea29e17b Mon Sep 17 00:00:00 2001 From: tomcw Date: Mon, 8 Jul 2019 21:46:52 +0100 Subject: [PATCH 17/21] 1.29.0.0: Updated version, history.txt & help. --- bin/History.txt | 9 +++++++++ help/CommandLine.html | 6 +++++- help/cfg-config.html | 5 +++++ help/ddi-formats.html | 13 +++++++++---- help/img/config.png | Bin 34427 -> 35144 bytes resource/version.h | 2 +- 6 files changed, 29 insertions(+), 6 deletions(-) diff --git a/bin/History.txt b/bin/History.txt index d4dbe89b..d5f9454c 100644 --- a/bin/History.txt +++ b/bin/History.txt @@ -8,6 +8,15 @@ https://github.com/AppleWin/AppleWin/issues/new Tom Charlesworth +1.29.0.0 - 8 Jul 2019 +--------------------- +. [Change #544] Support for .woz disk images. + - WOZ1 and WOZ2 formats supported. + - read-only: images forced to write-protected (so 'Stickybear Town Builder' doesn't work). + - only 5.25" (not 3.5"). + - known issues: 'Wizardry III' not booting. + + 1.28.8.0 - 28 Jun 2019 ---------------------- . [Change #648] Support 50Hz(PAL) video refresh rate and implicitly PAL 1.016MHz. diff --git a/help/CommandLine.html b/help/CommandLine.html index aca64edb..fee1b3f2 100644 --- a/help/CommandLine.html +++ b/help/CommandLine.html @@ -98,7 +98,11 @@
  • Or: Allow the emulated Apple II to read the Enter key state when Alt (Open Apple key) is pressed. -rgb-card-invert-bit7
    - Force the RGB card (in "Color (RGB Monitor)" video mode) to invert bit7 in MIX mode. Enables the correct rendering for Dragon Wars. + Force the RGB card (in "Color (RGB Monitor)" video mode) to invert bit7 in MIX mode. Enables the correct rendering for Dragon Wars.

    + -50hz
    + Support 50Hz(PAL) video refresh rate and implicitly PAL 1.016MHz.

    + -60hz
    + Support 60Hz(PAL) video refresh rate and implicitly PAL 1.020MHz (default).

    Debug arguments: diff --git a/help/cfg-config.html b/help/cfg-config.html index 17daf96e..f954ce6a 100644 --- a/help/cfg-config.html +++ b/help/cfg-config.html @@ -78,6 +78,11 @@ processor speed from half-speed to as fast as your PC can emulate.

    + 50Hz video:
    + When checked, this option will run the emulated machine with a 50Hz(PAL) video refresh rate. + The default is unchecked, for 60Hz(NTSC).
    +
    + Benchmark Emulator:
    This will run a benchmark test that will show how fast your PC can emulate an Apple //e system with this emulator. In order to run the benchmark, the diff --git a/help/ddi-formats.html b/help/ddi-formats.html index 0b197b37..b16c688d 100644 --- a/help/ddi-formats.html +++ b/help/ddi-formats.html @@ -57,7 +57,7 @@ successfully detect the format. Otherwise, it will revert to DOS order, which is by far the most common format. To force ProDOS order, give the file an extension of ".PO".

    -

    Nibble Images :

    +

    Nibble Images:

    Nibble images contain all of the data on a disk; not just the data in sectors but also the sector headers @@ -66,13 +66,18 @@ that would be recorded on a real disk's surface. At 232,960 bytes, nibble images are bigger than other images, but they can be useful for making images of copy protected software.

    -

    2mg Images :

    +

    2mg Images:

    2mg (or 2img) images are a wrapper around DOS, ProDOS or Nibble images. They contain extra meta-data describing for example, DOS volume number and write-protection.

    +

    WOZ Images:

    + +

    The WOZ Disk Image format is an offshoot of the Applesauce project. Capturing highly accurate bit data is of no use if you don't have a container to hold the data. The WOZ format was designed to be able to contain every possible Apple ][ disk structure and layout. It can be so accurate that even copy protected software can't tell that it isn't an original disk. +

    +

    Compressed Images :

    All of the above can optionally be either gzip'ed or zipped. If a zip archive @@ -80,8 +85,8 @@ contains multiple files, then AppleWin only supports using the first file. For b with hard disk images, uncompress first, as writing back to the image requires a full image re-compression after every block write. Examples of typical extensions are:

      -
    • .gz, .dsk.gz, .nib.gz, .2mg.gz
    • -
    • .zip, .dsk.zip, .nib.zip, .2mg.zip
    • +
    • .gz, .dsk.gz, .nib.gz, .2mg.gz, .woz.gz
    • +
    • .zip, .dsk.zip, .nib.zip, .2mg.zip, .woz.zip

    diff --git a/help/img/config.png b/help/img/config.png index c4e0b9414897ad6249f55df84d678b39df81fdc2..d35785ca6248f7b4de30e5263bd98bad88b860d5 100644 GIT binary patch literal 35144 zcmY(qbyOSQ^9CFs!QGu;X(<|@K=I&|Af*LDu>dV@#a)60f)-i=6l;Mh1&X@`X>lpq z;#z2Nw>O{f@4W9h?;m^i>}K!Yo0&T^&&)iVU}T_8Mb1tR005|Tbu^3t00KMwF+fU) zzk|GczY2dM@G;g_1AHCg+`?Y~9bx(~0H89J;>s3;zb5n2G4}xgsJs3<2zos~JOcpG zzjZZWPXev~wvbqwm^Ge8IIIt~oVJ{*ti3)p_yc#4bk5LtH;TpD=}JGc&jIOEP9k@h zDx#=pX}1!eR1(^;&;cu17&V@_A^0k%1YUo~{u;iF$gi2S4*Ba%{)Uiqa`%v7D*WWB z>(sH5>))#3`PVir@&{RiB-eksmD@r~FU4;smZsXy)s@cbzMiTm{98~mCb`9owweE#&_+Wou`J;?O?{78FS=5fuxH@@?Y7fjoey*anT1q)}V8}rxthp7>l zpN1o-~Hs$gxQMN{SZf2l8*+jv4;Oat8pUs==~nshhTZ->GvJjQ%l;YkU(I7Qo^ z3r03$=RN0ovYR&Rn1KzkdAa^ju4~bYq{Tluu381eHUOIvWzB18<3`Q{z&?#!MEJ{TnXGv4XBW3z(dmIrKSFaEUMfp&MUuZ$-R-05@g#*iA}7?K83mDvy+$Q z4l4^CRm?Q`-xa;I)+4tt7bM~bR6~1I2D`GNe=#3FhP^L7$E6{wMwnuFf8*qdP3~*WvQQz>tkpFa85wsf}KYWP{^v$9hwbJhK zNmrP69XNk>e*)bXw=zTg=A94X8!jS76wY4S^nyq;=cG zi}6}3CIx-ps}SVxd@VT)OMRkvk5iw%|3_*;#8yp8@36JVY>38681=~wnL<%!Ia$2L5YX#ns=Y=QM2kTy#`~Eo>{J0>Z+k*Vv*dRfsSqNrSQUCLx zMqowYbLi-(Am&~Ijkxub{Dw_jt$WzKyV;8awVi{`mRCQ0*1K=FZ%;4r8*{EL=wt3$ zR>a>M&)f6BKwmrm4uV*U=>sA=Gf>ah9LBm}?<@AbOyJQ(OpTTyRHI$G&E3!dp|7L~ z&5n^fEROm4OTyK-dFV4^ZV!};l_ad zeTKX2vVWpVnY^6jBrEWez>by($Dv9>j5cCWz3Us;tzFZJVJjyTS0<4{sCG=X0P`ygJ;wvQhH81X2^4(IFW@eWmTatsW} ztV(WC0cO_iP(oQ?C{YR2y~C^$u)!DLQuiD?J`eQn_unBu#$Jwo{2p(dLGwN;BcKYKwyAeaVyJl1v6OccFhX zOI+hlISn4l)k+lIdvK%SeI}+O`q$rH$Hf{kTl!$@e2FcWpP5O+fqB5n@`L$khdR>|!O^|Qz8;7)&6yf5^sTGnAd#odWp8N1kt+W4AZh60z zohN6Q{8x>1ic+9@Eh@_{+)*qCDq}>IXRjKx-LA?8q~ar1N;tw2E}N-~JE-~otN*~c z83(u@H*8L9MY(Cy9taF(E-=(RCWHUmYTL`$KUCq&1@Kd|x+2Y%x1Bb%ecO47K^g*o zVa@mZrlSF>F(dzoDEW}mw$y0rj!$&V9fM(|(Ke5eFkJ$4Q66T}h88hmi}oaq2c3v8 zBHAT@_+R$_O{pdZO_vphCDQ4T6Jww+e%k`nbYQj}O-wCU8>iF=Rh>#B1;A)~rz~j` z(;G%gW_IJk2M{3QSz;bVSe+fUo)Mh$fgi7D$b;iju3*DlE+VJ12o#bYb*@1D$dhkq z;gNxrwV%udW;n(DcddfMzj&<+bQsy_E*AHEemN0-l}uJNgPKsr zYgwsRtcx#Es<4y+8lyDp9MwmmX+Q%AWU4|GD29xbbIrCyW zQP!W%2#LW@5$NJVd)l$GKGwH;YTY;yX(TFxFoc;myQ{hniT$7k=adikIDUQo%QX@G z3RrCzM*^dLHUlgd6f09yDmOcQ7i-}kH$2az93s1aLweMKo_S7JCCul*lY$Ks@II;R zGrz;Vw!>rEfqF=9t$# z49#2k{f>u@?l-kNI~Z71J(hs~;iG=={619R!&Y&2#{DfWoIs09{3I=uxbCs|v-xxl zdfkRZGY=whaDMp}=+0LW*v8!_JmvPmSm@D4jVdZeT65t_9)iI8PgFVKMi2{IxC zMpYub#^hQ5=_kgIKKwV`FvH(zMoDEQ?*)1Jb~lsJ>CQ;erm{>?{|5xCZ}X|2v>l;5 z-H-dEU9`~zC@LtUywPJ-Fjp|BZ8}4K(^IK^+us156D*uDB@p)KgrEeg3*7vAJ3FF;U`s;!HTjw2UZH+o=6(VdQTZ z!6oWSFZ!B>_%e&+(hPXXLw2H)n2*rpg!%#}20p0Pw;Kyk`Ft)lRFnx@9^JapF1<9P z%`&8sW99S$%ONAY+VdA9tvaFyh79F@_g^>hO z{olZ73RVaJyeJhEZa^l@>J+%;8X)?#&2c}jMep!Lbgp_~vn2P#nJWD{E!#Xn{JVYW z3+0c&E7rdGcJj{a-Luj)Sebp|{7840Bfa=o(6z^(B)?7H^KtGeXH$>A~Pb@Ah(SH5pj}Zgc4k@?9Gysf3VTD za$YbJIg0#|ykYGb&QHFkddF{;#et zJyp*Dz;e@e0FK8NrVF#>O@bNYpy=Id=>_MHc-W1Og_5?T*wdq~DPkEhS7;$2IT4%( zs2lh7+o#MosLWTfLc$c_baLnAiC3|UFETb%89?JWKR%t2h6!h!fR$16>E0W$ztm+m zHj66M&D{}}mACaT+h5EP^+pDeq|7Kd58UY5Q^UC*%LKu@SE2FmlRmzYT>r4VR~`pj zOQK2y6TZkHkc=*R^5BI~-~l!)qjx7;+M%}?I(H}eH?u^zYx~*T$o*J?6}>Fc?b#g= z7eve(ZrK`6(z8^}K-ctzR9PUyln*>c3NAb+p!g<}g{q>UnGT3T(tp3Fih034ik&Dh zDa0D&1jRyH3Rt5b=2)Ott8m^rwCaS-@D2TT7~%aPxm6Yk%+2p6Z1BqbKyt{)oBP|R z&j9A=@s1cFPC-@Lox5c-1WJ;z{9g{g0pP*H5}ZWO0EHW3V%Usy6@~laflC)chf$0T z_UjH*1yxs{XD}o{$53U#8Z&@((WZ?2zw4z@uJ~oaggO$=~`o*u2;RS#nuc~P?}&08gk zSBy?2j<&IkKxUH9L)T?~m+qIT12(T6|E=XZ*};)_tAF+PLUqY8F)EVMU;GCvH zfkVjRv4BJ{6`e8*Ua=Ffirb2?#e}XCIA^VEXN5b17hU&?mIs^cH{`Fr!4<84HgC>5 z^oDb6m+4Rm91Hv58fufKa;`;=)Olz)Qp4K1eU7<*uJU^aTLq*X3qkEaismDvxZ?baO*d zZ30q%^6H<_KeFm$Akp}2DXd5f&CKMI|EG7hT?b#HMmaQevP6kowtvA09KB@K)+vY$ zFai4SBvSuDR{C;gR0_L`f=ITb0dkQn5{U%Hs&#}~d_`=z4iHbQ439R?%Enq!A_6M? zY^yLk`?|E`94Ho3da$&4b{gN?tRPJ;K_I%9d}He#TBA-$NGbsW?keAnj8sMmflEo5 zk^5}Vh>S@qz|D(!nAgf@Kb`wq8JT@4B{AWiKYGAB)R+gJH2H@^i|Bwju+S!JjKA=_ zy$wpkwN;GkT_>SVk-u;Z6n!Ar*V#%+an+M)RZ-ZL<{X*CxM_aWt00nj1EN+RBOU}; z7atDAl2S-SgBsjm=`Z$J?fCfX3K!12{)pev*xb>(QM0%DgS-`j>)ZK9u)% zkfpZyTx~yBJkv?3WiGw~{rSILf%Xi+G_01fB+dsH?Uk{}v>n$adtST$&5f}5ec~xR z*P8kBq~v*dTjqIiRnk;$Nz1%QjegZ&$77q@)LVW#J}`s?x*vi{5SDGlSU!>P+CC3gHAFeegyUF$=x z%U%`b^{qbC51P%SHU7zr4&&VY+}T!@tv_;AQt*0y;uY3GPMDaLtC*OwGDe3@V<05s z+kH+t-nYIH6GKA>gGd4N2m`OWsBK{P1()92rxx-l7Vxx6TUjiPZDAF095+cjA+eCe zy@cVOezuV!(*C~qBJB~BUt|K=sIa!SLl{>rBk@ms8DyIQn}lVJ#%{_Nuxm9 zrnHD7wg0@u^k1eQAPWC@kRWK+a?b;VwX8DIg$6oYm{m!(8VACHnJ09}6B9CbuIAsV zwJRrHdZkjA#MNF<(Fp|8ur37nIxRpGyN|FV*rcB~sW%q71I;+IeEs0HhFH;jJ@lT@6`UPmb27^5>6c$G2@lg8jDN`&cOD{r;1^R+ew|>tN>S^>9gTsB**FA%%rv zoAZz2sqo8Ta)@^%9YLiq2>$De+CYEpCD31R@hFx?ebalm;5fhAGiY_{wQMK(l!sFF z*J8rvNnHFuv)~6M=^N2FIeCZr?lEQFmerA*mu`!ME`c)NAkR_VW6n>%_JKcZ{aEd( zt3eQ#dI5w=;T=E#b@HMFYm?0P717r(9+7K52_;?cQ?*{Si2?2UrlkMEei;mRA*KKb@gM@& zlAxUNQY8=2LdNOqyN5y1^=@BB(eduF84&CzdQ)|d;3El4o|KYSCrStak{BVlri1E^ z*Ap=JQCJNVihG?<6B|n)+H>v(Jd2vE5Z70?H7Qy-gtVhlV0+ufU`>q>HFAkvQzzS^ z6xdx6M4esd)i6ZhrG-os%OL^tDoWFsXVVf038Pk`SnPhbX)bV@8<&R>PbhNawX9V& zRvosJ=tQwKF{ursGEnxNj!!-eJ4tV#hFXY|;%mv{Gzvy-ae&y0)_2(|JYAd=*ub3y zJ-c@-)q%VpyD3I2$F0M{AAR1CvQVn~^3!LgtY9$gZ?>O%zWKs>wtl(Fj(c0xK)$rV5Ost$DWxHFofb@;uilPQzEh*=q}kUfK0Lf zKIS2H*+O{Wbq-EeG>7|&tqp$$>yJTDGK1>sn%S(Y!!5cS ze;Or{@oj@&5@#!b%1RT+U`u&AAJs9LxW=Dk6@`UA<0oI@3h8Lych`OS4WYfmR3XV5 zXK3sR@+9NSW8_<;K6yU2?)$AeS0b%jLEp-}Rh}(Ts)nkc?_IIlbA-n6Jjd?hh>)XR zhk#Cu8>D%fa%;-bbLd_lP3YXg#Ym+)_}#)7sA|6CxvIN^$J4|lBk5-^9DX@ro*#Xo z3QX{2-VxVShBg5hbbSckbg69hqO3|cKbhiXs`t9+D z+ct5vH+C|iksJg?BOUj^clk9`71h5)g9e?cm?dI@8q8>qgjhui`PDtXMVfTZRRWmh z0A#$f9&xQp6u-&Apr~#zYjm1kZeR_{a<-a~BD^2M5e*RO>B(r45faY~!4Fu4P_zTt z5(JhcVz9>|toT$z;5ooo3{66uo31uj27ot@dXC*T`$SzOsgu^Q${bW%6v9=P;$Yg_2}=q@VrOxDX{~?v9?6B)pbD>q0=-*tXx`=G{dn#q54SS>7jeV2U;?WodY2lp_H=oP(+*w(h zTZ6-zU1^(RgTprgIpPuT&&Tkm@TQ;(i^1`Em;5W!Lsa&-^<`=V5!_1)*t6y$5yc{& z#Ul7@rHo<{9jxNk4v5A+vi=A{sc9!o4FB7C0CPstz+J8=69rp>;q#!K`x z{4$18JvHvcRI;MZ`!3f30;1trLbkK`Wu_6NE#FmD(Hm8arHx%-k$gxl8uOP63A6qp za?c_mjEar{#K;g8nLvvJS~pcTvs9pxH8#63xNI)b*U+M%kSwu0l9mKAy%Ul7dPis% zE`NHFIt{BUhA8e$`G$~BF<7VsKy53J^zxgEy?l8vB|np1JR5;Qp_ySbFfrQ{y&W0( zuZI;*tkYe+-nrqt<>AonSISal0K&FKX+qQqV^ZXq>!~jCG2(9XNax$$>a1Xt?l6i{ zH78x+TcIJWkx6RLUj(0`1_beo6k6=}0K9ALkgP9J?Szd?lgOe5WRw!c6_hk*51;ko zlyrs(^+T8m4#No3Sr%I}SVbZkZQumP0zTG91wBZDVu|=zHGI$-_}Sgcuo3ovfQl^d z@>2WqLR+0dNfVera5E}TbCkMdVA=R$x!~3{l0&#?e@Ju$9w>9!s3zF00sD0Wk!&%0mx z)m_Hlz3QuL?oX>()xnv(IkDYe{>~e+ECJy@k2Yk-723>MnGY@^u1_S8YPPuIz;;|- z#C59npae_|4uEvFq73`igy|U8M!OB(%Tnj3j%x*kN*yni2kiZrSaFyU{&K!WO39 z`$Uha)8{qR*x6pC`G&HOMA_mY0wu#UR>^QNiMJn;MhPSyp~+N<*n(-{14A~IY|v2$ zQ{|>7l_Km-OmKU(gA*}pLOfo{x6DOoNI-qAFDgBsJ|!vd+BJQFtMvD^=>Pn9YUoZ& zLc%J&!!+gev3hGxn~a3V&-xELbW3OY+P8~I!*|{v9T}pm1G3U9M-pdg5<-1c5ue2! z>aIeWtKpZCXN+?VLR@lSjYD&J>%Ae+6TR(t4-hX%?E3lG`}Y65PGFWSMgm*m(roXfUGcu$3QCyUsa zg-ngMkB5Hvk}bh;Cle?mKgf6EZM9~B|Ik(S;Y&jW@ylu-|J0!wC57@o=Qlalc~i%K znk&sVu9WjAE`0C(JM?(55TJL_+Vp!sL0Q}LPN}ne!>jrV&pUgi%6YOU|C(C^Hh#9? z2OozUl%H0Gw6^V3oo1mzA`px$*ORwbQ%jFo2v!=1D*h}VE8J>#;7*^7Jfg(X z5_9Qg-iWjKGRr}b@F*=fKv{?X{U8S_dkLMO`J*tJS{TWS`fDk%#U2SDh@W&u5=G{8`}qh9rc(nR=YW#L_vAO*8FkXOS$riM6h+}MqL%3yF|k)xxvV^~ zHXV!x5DoA`E>bqp@)dZ6qL@gXsNr6eZy?uZTHALXXeC*j?x4j%L-)aKQu5g=ZAE(9 zlOCmP%y+>7xVI9a0`)tGFBY<;WhEvfPp?XeJjpB+$R(i9{p^59yXio0TRwq6QlkK` z&OZsO6r;Ab{lvoqM^q1Y8L+?XJ5m4ak+`UZxK^dRoQ?YA>wLvktSC#MVEL6svn{%R}5qXtpc+69Z^= zD>1C}6KV4V+>`d(Q=5Ek*0JF&GMC4*a>Y{@uog3h4a%eMY}1ZE_jqGJI{(hU;qL-U z+8wXrerEY%(UVr4xYSmt&w*YQIkl>*dCf~pA$S?0DUnh>&hZc;FYziYxQpce`IBDh z2Ji3Uwl6W6T~#xV3gOKRD?cC7Qj<)4_b1!(m6Rck*Svhg zi0bTZvn}49CnQKy?u5mBdvevu1dCi~uEpr_X5EA8wT7X(<2iWe=6|5S5x`a)mQzO!l&-eU^i@PFV^LBk%k34K(4h8y)`^c7sM;h-mrzM8yNdwSBnL<|BJ5fR|tJIS|J5TM_K zZr8e0NBxoeWETyFv<$eNIffF8C^O;00Cf_u4tcN@19Bg9Nv{zD@)c%8IEf=jWj$W* z>c8Sitt^PCSSZfuXYRwK?w#5{97bK;i?Lk6-;hI5iA#w;?D|AqDgKOxRr$sD#=LO`Z=!Xv0@_wa#dHP2zlK~5N%E@4-s=sY zvfQWdmvvWN`j~xXhd%q+$Lp#^{Z!UvB<@l4K@a7uqJvS#G^~4t_*D`|by#O%9s|kW z6CTp}jfIFdzbxX-77`_|tlM_3o9Nn9o@yqgdaqxLsLE*BWbS5eMGvR>;{7=mfdQK2 zH)oFm+JO>w##i@{xQTODG6=Ul7Cj&`5ymUeFnf{K-7oVO9}{MWJAUywg|}5u_FG3& z9ku;|tDqIWE;R`_$4}c@Ix$xF1gw7a+EwB zq9yRm#Hzez3Jcj`8L`1Oka^ZDX9+B@A3hkfRbP*zTJZu_=7$AEU4ym!*;jC$hmI(DLv-_D==ZM=RaJPDdmex{*N8YGX}7?H%fl$!F_&SK=@~RU zioEn4josLL00LP@*k%hmg%mGVoV`hck%~M$ejI`?A9><_SZEmVgwWxBv zNR``zi;;tY#I@}Bs*zrA$&@t_`TVL);J$8%a*gqBhA>MWqTxd#>!(ix)|L%+`%6ri z{*Pu3Ok(Er3Sp*GZAupPhl`yib9ebm1-@oCMk1G0$RN16&&#joJ{PJJ6eQ{$oQU4x zA_sRr3=T1&mVVNltQHc_WPP|@FmT7j9wQhX>TvZn9jN~$oJ}YvLipp;7W>3(OH*lR z!@?3EEd_B+&Hyim+zo&ibvJRxD0F-`vb)go?w#;h+J<}Y%FJIDUV$|mhF{)36-b~Z zIN1n2b_826e8Vt&oAw;DGaFgRXBxD47`_m7YUA;y)$2MFS3^+wxzAJn>^e)RW!fYG z8_<@2im#sGSuZtzSP1iioMNV5Z5D0809+cX)1^cRkcO8uqp`W zR4km4stWNH7gAzxv%~yApS#vzC4PNrL9U%VTXS%~`eDx?b7wPZrEF5TQ2oXO`FeeQ zdk3}F%8c11)R%CTaxEFyp?0O8%Bi&wj4p-d$`uIqm61>EFzdQ_X9=_fMg8t<|7SPC z2Pg+Gx=-O&Q*@>0{afzjT^B}2f{Eoxxq6=Mz^(NDLC3g!p5ZulS+8QjD2cwgJ5 zkCfA6F)z!VWFY9TUh}=t=2Obe`L?qeX>!tLmBec6v-bB+ktcBXAEc;g02LTGt~jBS z_%G@WUA4C*^L)DICF4=gptBQaOG*drNh8=3GUS);+? z0OpZ~LM4i|kjL_PoL2mGp}FbX(WdzyA_wKboi`u*>p#Q8@*A%Lxx&zwz++h%O9ExG zR)8|4vI!B7XC@sw;e7fP5?eS00pAx6;!i`J2)*5}E3(+n;%cB|9&sq-g^hO>JXq|$ zN@2+QXOq+0;^{m;CNA_e1O-3?G|>Px zf*TH_=?pr|Ote&=_>V1n5t)u~V!oKgs(aH?Ar7T(*!-B(b^_J2-gQE-R&4p1yzlok zeDyqf{BpUDgF@LZsa=nL4|@5u&8YcNr{hJxQgl(lZRh*({M#o0pluA5d?Xut8XW;2 zd;TA0fR>h*`pv%qRq_upR7&l><~%E!i5nFxQpe}RBb>~EW}vp`8A-6cKag0>1vXL& z??IH!Z@1Wr!hoRhAy6#7LQX0^(Tlj-2JhA0dStyQ@eDCR()F$}2}@E8h>*J%=P#^* z*z?fzB*ZA6nUG$OWm2g}GA@c&I;QU^Fj3)KsH6|MG{n8=pHV_ipHuPPki%P<_xZWy zUFnXes4`9(4WJaM@5YuBt!QL+C+Ti%MzV}LsD%dk!t6>=WZv(350U}1k5AEg}SPTI;f%U zdpnQpcXN&_XHxmM40GG7SM-xv_H@&-uW)k{k-RqaXz9B3@P&)yVq`XjhMb0$T8BQ=O!!yUa}SPfX%`<}{kv(rGo6ZBvSBSU1^ zVas-I%eGo{fBN8+_5`wOL5I5_cDTROaV)LZmH<==dHenFa{BPBPyKW+DwyG|PbBp_ zeltoM3z^Jcx@#PnDMZPU0{+8mXe^%Qxc6eVHUfdK7nCVGCI#+Sfj6~B41gukLSQ+r zc5?7tmPk@k&_j|@U*<00{R#DfuaDF6K^;I|#x%l7V4DfAshCOHb`CLO&lZi>XE?{$ z!6Dmp!(toUvCea>AdO7LfQRvf`uv5wazMr&DveOTk%>^GPa?+VcQK1xrDM@AM_4b| zNs`#M82nG$HY!W><(33co~7EcLxDvnacfJR5rYrqBk7%-%>Ft>c4{OTXld~xR+S)c%Ho;V|Avl~mk z<50%lbcB=By*N3F4(qoJ4X1<#wVDED{Cv;&T=4s#V?#Y!JHzmD<_Ko8mz_Gi!f!ak zPcz$fXHB$Fv_SDhEM3kvd2DANmOHZZ-9E-_`B1Xs5J2$2eq;G?r)NZaSL;h8L3iKY zbNhxzcVYQEFU! zB3;_OIyfWMLucSSvb2o7KwUd8&OJ9bw6;o18N;ePKl*)-cbK!;iwLV6Zg5_0LsIxA zr#?k7JVkNoWp(HU-0Bt)`hihdPF6lkc_oB1EML(1>K*^LJ)yZJVLZ```ZvMeRMDB7 zWm{TFcvV*Deb;^3P_+L#crQtl=)EH)HlN@f*`|0XTjr!EJG&OMa#UnoF*gHqsH8{u zDKwDCWMgS`clog+-#lMrq@mf>KL&9vpn6Dj6tO>F1{x9S6Bh_lhn?Y5)`cFHY{ zGqPzJs3Q^S-{h>(w1_lYQhdP=mEP$KP=h|QFkZBmV}g@b7&XzqWr7=Fl2~O~dfAic zi25f zFONgN$GdRLhAs0~E4fS(>e42+cnE&DFM(zs*A+>QtDe8m{!J`cW)Deyy=s_MoDb(d z9L$%_R&6@1t|>2~sQ`7!7yvV5XmTle&PEn>b9;xcpgtWls}0lUX99C*CVrijc$zEQ z;+M^Zzyk%iRdT%4+anXLp#^#+=^c~|Pm$e?GptVv<0;U+Umm?+$!Y`O+GAO^iBsi&L@=76s2C?nsL}&c zC0%%U1?r_BWGoz{2>&Du0(JobsHf3naWq4ucHZtN*4OBsk}mnXEJ7N_k(vnfl*s0N z144==Wr|pq!O&@%?|VN$*3AIKqW(f8WhcIyh}BnM@uZ>*bEQ?D;s{|b_Q7LURy-+W zZw!L(82y24nSzD-{v!G)cFbuJXJ^B^>S7j0AZwu5nVsYwUqTX$mN~FSZO0Uwt~OqV zEjbZl72=ErJ;$3~CY`A+jJ1Q9i=1B$BF8`NrEZH~t#vNgu#!Y;v1@Z*mTQOQ?R_(k zfV42T!zNIZaYGjmh3>;Oxm}v~~8Y(hpiA#m{qc6hqi==zF&cv_#nz z0M+6&v+{gN<1mnIg$t zWYg7%Fz6CK2&vE$EeZJDwjFYG{1UN0o1D-{3^j7oNmT|13=^S3+W88!nh|OZAz3w3 z)mJcHtP1Ne!`$$b^<|_5)V;uo``PaAN6V6A>u#16 zS^sT2%GH)1pW3rw*viehe^N*q+AtL973%RkVn?L;AH{X7(T-%=dxMV1W>9!6l&Eo` zcsJ5FvKF@q(5D9Mk7l;HTvFE%Yp~cU@%LF4W${X)j)Wjl5AY(|n~ibNMs~EyKbH;8 z*eBH_&iRGYy286Q=%ZoY?YtnQRh*R<1OSUIv>=IQZ_P-jjgqyBP-ribo)oL z`p3bHn8JP7PUJ*HxyzRHuere8DG}+4Gn8f3rfjy=@8V)sWQ)cAPdL8zgq83W<^*?> z`fV0*g|Rj| zM?LU#Ts!%eG1qS}vk$ZLI}oU(pF$*iDg@HQrN*n>vp5xtP6>Wm&~W0pOeR*qd7JK* z=RC)iYRiOwT6$%6l^TO+d~W{nFX}g?uKxWrFsFHI+WB?5er1n%`51W+UFWwdE96=? z8ZzA9@wvldrln}-yX@*H(~*qC@4wco9(3XHq#>K!LAd$(k|3whQts=QSS4*w*=)7Up*h@?I>y3YI%_H&LV-^j|}j^skX=lpx#qWXKZ;qE8o zd>Ph24Fgccv#_1i+QhC;{)jR$yyG*Kh%cY6e>WDPHNh`l%kSYim}=bighj>FqRmqL zA0YhKr@5xd<+pxSiW?5>Dn*Ua3Aq+^koSmGlNEYD;dhOBu2;rQz1W5QW}bion{|&< zV1Sg@<(-bf(FnKsrPOlYvvFar+X=#}e~%Ym;{>SalG?^z9PBtDc|bs4C4BBMX3K|R z)x|?I0OJ1CFeU$ zQcHBObx7=6NWn8X70*B5VltnMGj!m{hDd$jN#MFWd=TrG*tC}}-p0_PD^`tEvLB=z zSTs6kVPc@x^5G|Ev@L4BRad8|1Lzpz4zCE{mZj2#>)gM;1*H>0E)&`j1~dF9T#3X1 z6aDWNm$+kVs60m*24_7301W2*>pF0Wj*}Zpp0^u3xqJ0F2V<_n2$M;fW=%tS_~P!v zGCR8=a+d_Md(-ngiekYBqoti{+)o}`2fJ%dy~e+Zt`~So*T~I4&aar95UO&4c-j`q z>?DW!``j^xHQ6Va!Nj3NA`7q{1Kf3UKBw_>#A zKeps;L)uR3!!Ne(%sv5k{QoDhTP`GKxUU7Lq*0$@W*Fhtlv%xeFhC}<^~WP-8~s@P zPSo@hM4!Gq<7A`XG_Op@>dx5UA74v9%SIoHve;@0`~o1&nv4vc(e9LZ{Q7|iGB76? z6+hbqE|b;T@<~gZCaN5ro8ZO(s(M5~#{#L!1LfU`}>@4;9qE8ZiE;$9=`OByGjko!$lJo>3?r-ZlRz`jgMp!gHmD2gZiE;^3 z+jEc+F%elw`y$GvAsqMg=fj^MVT@@2-Lq#TQO^t4bVM(>)Fxj&w9qoo^neKv|KFZL z+wxhqCgs;2^Zm}Z=;X&)v?_DXIK)GiB~YMc*2qRV7sLa7uRG&QCS)&@59Yo8 z+Ad~@pAP$7DjJ7UbkekYY|G2%FIH88+O(61rHIdbksznul&>Z<{;IOp*WKWQ#*C^Z z&<3SjVTu=-pVf?MBwQxu3vQG$egfNO$&mB1xD1(YFuX=a#SeG^Z_lCvotK7Dvrj zaMw}0rwQ0aXQ;PKOKM4XwXZri)YgiITe0vzg$j`WAwJffDkqBZrPEQMFLKtLeEoHh zOt^!k*9=XHT7D+_+;8ruiQpx(aVdw|B<8AozO8#uYAFZ_Sj7EPu` zPKAezq!5D%at5$ll%>l%#!`Kb_t2>1r*sK>F4Ka#mE62d!5zkAv#l}TT}CEL8QeT; zKD-qwZT zpxrhgms(fmy$oK0A6{`PLoC14``)J#LU`rS-or5e77Sq**^$ug~fo=B6cUix^3{%7}u7G8ie zu3uaZsiLY=X^hmed2!HG+DGfloHgF9exC+dfd9AEHpx(AYuC~J?QyOQMg60;{d#|B z7}Ol!w(`OxUKpnxBupxlcpV6r!^Mgp<;1nQiG2u2l#DlM8*7psE*Q(z$Q;x9-4;g zqj6fdT_{P7P@#EMq`q6H(C?;z)c>YW!#mzAYm5Kes4;D{Kf`!k63I4Ab>K!FC)_WL zf^G(L&NnK_-V4YuF&`BE&*%T2d%{Pfp-GmvjI4>ORfRtZwEWM=Xg&#v|G&yU5RX*SVQy%=xt-d>ADA^T z2&7!e`suM{pG2x+FULGuIkB$ z50f+hyC*^-(P0gi_xML%B>RFXgrQhHY90dR632#HNV_x07+*MBxEzY#H6K)nO>5~h$U$QXO1S;6sj7fmJG z{90M4yd(+yl2Mzi&JNL;oSqdR1g&g0`v`z!87Ft=gvl{b($%!97 z#UNW*{YP8Ei^jbNJwuN_UNHZE&-qI{3in=a7Fpli@hqR8q?Y&k6*WJ9B(eY4KG69& z105$GqSXBmgE*<6UaCbWR6c5$08kj(ch0VqxhtQ{#=gZOIfP6WUShrK+-%>n{ndX) zq9J9oiUlB(aIyq8;7v%+P^7in!2?YXWjo$5i#rxrul0w$o4~i@tcWu?g!)E za8Ik|dIK3oq0kg8{=?B9+z^9v?G2#D2C%14+?qiG8b1H&#iIsfVuJRcB(nWjw&@{Q zGJt{RRhDUODa|7X7p|vTU8o;w7P+xjLWi2F6QRjpek_?VNd+S>-6&FG`LXqmr92T= z`#iU2z0o8zU{R>8z6^9Ue5xpe06LRNmtl7wTQ6lhe@~*Fu(?YJj$0KjoIRBm7tjvTPZ;kMDC#Ilniuo@ZP#^R>;PCxI(6>F4KSKVbkY6tY z`-~Lvtio-&^L4>eya7$}Ki|!}`hq#Oz z$MWEA?9l^>isw^_m0BO^SbuUFC#%uAQt0DLoBBC)MSOAv;-karLN;K^< zuL$@xpZ?B%$yS5bcY5(>o%`!;NyUrLUw@L-v4m0p8=a1e=6H739r4jrtA)Z1%a3C& za3)9-Wrmsx<|A+SGAXLw@3Go|Gth;bQqU=IyZuL(zwEAij@xHl^?Yuu{-i*%Y0+9F zhec%$q~?YIKM0{^bE-42-?9RZe}o_zknDZSL8m$rs4hk6@eiOIC+glOfc&m~{U7ZjP*9jDKk!X-gKZYbNo%xYz6Ben zsKW@pln@eQN^qNSA>688y;`a~DL)KYv*?S!pHo53bCQJ3VT+n+~dXE$f>&V{(J1>S1ay&`N2e*;OrIF1RUYQ6v9Q)lqu zgUlZHH1rdzjyT0f&^{^26nFPK9R$N1BA%G7^40q9A9VM;dj!b_)^XG3QsD?izBxBS9?tjy&6_$yf5WE z7aKcto{wRc`%`>TZP2^34B&>~Cu?6fX1M>;U#Z%@Ua$^Jt@r>6N_o@5ca?9Eo#QlA z?LSUjqOidL{<5DJ2-1x=2`i&D0&vTksXqiXREWcE$ex=&7ZKB*%wz7xym{-8ckf|V zSp!{CGmp&64N8Buxn|#uFVSg}rB;)6c)`(;x^5mFQm|cdkywrG`_HimBbW0}Ge^v&3 z-BKeltvC9Z80AY65FO{Jomxb{4D`_0KINrg85jU?zHRdk%}j=W`A8I{&um}RE# zgQh6@8)&UQe8bHWS8pt3AP3r!w?@n|;j6Z6`1efHe?8Ic6u zC9qSRyfl4|D<}0C$JRjLKLx$*9@2R9OXHDiglTY#06;oqtN9RhEVWhaU4u6P3)HiQ zmuBM)JsKxZCHFpsOQXVj=I|(Nj7XF`H<_?pibs+aOkH^4MrIv>rRTxQ(G5s!gr%jr zHm;H>5B4oJm60GE_5FCf{ZT|wogCEtf~uza+13)L=dUGUhl3E%B{L(y$kg@9K!M{? zh~+U<-6Dd`P&j;H6z=(rI#|POoY3DEAhs8AU*A_w!J^eOp#Q}CN!u&>M+Bgy^QRn+ z0owbOC3|hUk^S$Q2q6Oa#fvJ~j=rdjM&99VL0Yx&P&C7J&m%a=q41QH`-bYGf+EU` zbg+D?o}*5JtWbi0O(GpOgYu7e)1SM?Bqk>6!K#bocF4szGsQSLiq=j`*oOCZ7M1luHv66%XzO0!g_#? z<=3Ey?~s8~WHpN^zYu(;Iy6BjSQmk+S9rbr4#ONpVreluUv#-$Y2a`VyY_9&PY`{S&+E@XX?r+`hzJ|XY9#ivZ zUjW;ia{NaJpOsR>gMlG2z7>Py%5yO^@c=T^$uDz(6jaaq;&HWp_xe)j`HTE{%NAau zXI=vJi}(JN-i;Yq_ZXYl(jqRKeP7|es`n0==D+1o{5`G3U9=4^u=+F}7-u8<#66Up z$xE-1#*7~W2TgPyQ!n=vfP-JQgcs^XpLVurFdgEGzRP&xjndN6BDrznQH)zsq4K{= z)#%T%A}~glc=AHH9&=SbKQk@9&)cJdzQyb10Ks3PL$g^8UN+;?{d%Mja9o_1m)FBG z>kG5O7D7$jcZAGfKUcIf|Ejpx`DRW|z>8!zyltzNC!+0`D>0bRSLO1=SiHG!VvF0` zuP;sy!T|Y<{U7h4V{0Q2UECW0OPBKpt>IY5wfc9hysT?kMIB#WMhp@szulu0>KDE3 zwWX8LL9p+;_%tdb&+tH4j?Pnksv@p12k4yMrzs}Cw)dqpfNzsfQQxtH>W;#SDnL%QS@_gf#@)!hwF`h?fgJ1P@a z%Vbnx<MQ!ZUKr>8vxU&;ZZTP;5ZnO4zSLg7p8Gx41+x&jl4M#UmZKBpZu zM8Tj}Rk^bSyvvoeS7L5-XuW+0F()f4Zj}`K`<#Vmw8OPr*EVKa%0xeB~w(y@rTv`zKA ziHr)5mnYr$G1A=pG4 zxm@@zyIq2}x_ygC$-g%mWlNct{gU96o~uv%ZDQF=i`qb zY`|UYNt}KuLWgV`t;^4}+B3OAUc?|;TX}7>J-_dCyfh|!F|sN;@52k7V(t2@!ldYN z$nJU)+HE|d_~M{ai$s>1q-KlvAgYPe*(JzS^K?Wf`fTo->M6Q z14pNP5@?vUlBs{mK@Ss?Q%usW8Xd1nX6x!U^VgS}Y_-~U0iU>{aJ~K}6s7f(I!*rG z9#MD#oFc>2YTZ@HDf9h=ntShtS)v&Nm=Cfk?l>&9o43uY zx1Dw_?Ug-Y=bqc!8acd?hgnkyrOQEwBRHZW2+G_|5L0jNX}Hk(`ik3NF_Dfg@*Zi1 zT!2Iu@hHEN4#}z->7|Q!&am?w?2DPHxoLbN;Mq~Ym*!;Y@7}Wk-42him%hX6;L8+S z9biFwe`R3$DG|(bHM&oLp6wG9_-a*u z*q*Z2v~PnG#UN)l9dq@JWQR;zdO}=vmMvYsZ<)^ql`MKn8}F6EBvqCY{<5zrCENfc|972PhTonjF#22YzZ`r0WGM8 zg=!9oFYG1-u`Auv{)tquf(D_yq5>1qQb1$a`6N7 z*XkXz>FD*G^{eMEZUXGo@6@6?wtxaILEfGKm=p}?mN;hyZ)ONFMRJCc)ytL@bEDnl zsr?e=xtE@M6D3Tt?$%9yRxu!_2b+-4I(gSOsW=H!=ZT7`hkW074jD}eihdf`9qd(& zUeb;LW5tjMGP!BRSZ2{=7?F;_*o)Sj0ZQP9&UcjwKQfkirBPD>`Oh^2q(e0Bv=Pig z5K}i~N%Z@g{I^YXuv-t{xBPE3Z=2oQ^90t6kNOZaW&Fx{^5X+MDC>Fs+28l`w2%G3 zHq8^&&$0ZOw6wIW$3k)JKlGg4Q2-VT4uuk+BGc^ARNp>=lC}PD0@xau8?%w(&#~M z2$6%YtBj$i3mBmx!3G zCJf*hY%Z_97ybs z7qmr4E|sx;^%R7jFQ*;PaZTmy);u>HvgdqyQA)3riP7e;T^ev03eTI50;$9JG<{2NA_6{gs4G?9+mHm$hpr+A= zBE}EC5IA6v5MByaMUtq$QqWSmq4y7-CFwChRlkID6}|-ET!qs`>EkA1PeUr$Ij*?Z z{(yrmW{eFWTzqArQ!jofehS?aQI;*!hF~ymszFE=1wR^E+S}~Z;v7;k-R7)cmu>Hi zJ4hybm$PO#+w?twRrg!A?%hOybRz6Ip_kcWO9hl~$L6DuVM}Sp`D+UM!LmDxWT%z~ zqlz(~Uw=*Vpe>GM4gkON?+FLlVovr}-+<2!|D45XVS;C7)+aXq=%jo%Z9XJtH9iA6 z6%Mvty7O9}G!*%o6Lq|&IhMs4bqukEXUvMLF5vN!e2-h*m^BY{VC~WH7~Jb2JGSL; z4=A#A`-T=%E&UQm`I7ILjF&e`!f$)wv+u@4B)#b6TkM>q0)2_OqHm|6k9EnIZ^P;nDU`|OuJ!2LFkh%>ngzYrpW7ufg z2b9WPpRCJn%C}cj!aevX+wC_$IX(LOzbGH)HoMwHdW1{Z9S{a8KI(akkViQgr*b;; z=w$e0Hj>!VWjOBLARGNW7W2(1us=(CuvRugIM3bL5cMHN0ALdht*pqSaO44pMCBnK z4(=H)Hym$P?g`@bO0^m{A-N#9x`YuTXOSToF@f28VCN?@7%%j8aJd7Ptcff)tceBY zmyhggZjGb1$4z9PkzC^`LEzyS6_^jX&QnP!aFELaes@lbdzpv4UJ>~4R55TbbBi!g zY)p#sDyz>_Jqjg?x54u?k-#F>kYqRZ7OWVp?x#$ zIte`C`gS~@g0=U?G>*CT7*==NgKBOBQcIGdsH)!h-0}^GI{5>*DvyfiPf|)DSzDwm z5jgB!24Ki*ZmxH{O`QCSL7+ocIg!gP#na^5tl;kjnvZ{T`tAK&6RM}{Nh_swq!V`m z+uRlo02}Zrjj^#Y4Fs_9$`|_(9K1_}4~qR|z^1`p5RV?(C00y(wMs4vN-gp8h87t( zQ3OBn5jd^ic(0r)dHE?z0-5-ZPaO_>2pv$I`x-A5WQ)Z|#U}1E@w@)xEk#Z1w#UN% zj?t89u+qmBDbjvY$&BYk?2$9DG?1U`SUZ6j!k_saivlnpK0ik7@zSz})ha*&h+ICI zhn#A(i`2QXY|bB_stUa=tuOUv6#w?gM&zDg4H^A?mLy=_tcpOc@KEgNVZhKKYyoX* zyV4tB8GhXhKb!WL18}oy6jQ@4!Y+ zA{W<9E6}zB3W=ytjm3rNsg-sR@Dg@n%7CcQyNU}AsZHXzEE>RI^G}ju>mAGmZ%B)` zL?H8&P;J^?)u$FuVdzFS$mVTNYwn3#!x4jfiK!IOYu6GDoEr8i2s9MlJr{#)+SLo( z0&kikkx-Eo+ogMKupj7Qdpw3kT-b5H>`sGbxh_jUNoJyde+cRW(5|H&kz&D}DdMfJ z;6O-L8n;amFQGR%bRnxv`(ro8-l@(W4{LrY+C`Ge#uK~=p?>#);E8-xesPOB19*Rm z*tX7IO-H9s&-*5z_%*`hvbzZ+IM)mSE~wzHAW(yt?U={twd@T*GsMZmfm$bH8L>cb zR>%_Or7>d}C)wqSqR}i{2oq#18GL{)(&6FsH#|R+$ zhtd}7CuU5pxb+IonHyDpnm}F4MzyJz4j+l2DP?K80>L^$bh>UEUVmz(etnv7S~i3Q zZO}r91q0Qp?!peH_Z0m8jM(Abb1;5&A|w=^>72{U>;_V2z0Ks??;Bt*W@37# zVGB}jH7`TC)OESL)k@a;IW%jCv|-FGP_{>ULAOJZyL~@33-z;#mHZFSLsC6Nx900? zVn9u+xVIm|hERdZv=k?=9D}`LZuI+}Om9Yz?p@4$S($8Mopg@`QvM%%p1eCCZD53) zimJb*Y1$z(cn6dS35&^)_seFK2djkZrij{neqK}DJVyb=NWRVOYLyxy7#!~*B9S=h zg2$PrL#ICw$-kBLwh9(P{L-u_6pwLl4_!7F<|EKzEhhRYMTWj{P|o=%S_@ozSzT{V zhb|5^*Ri}Ok&>MKAdpG=eHSAKgon&W4WkI>Pku=L*Ll#t#xDnRi(wM>dxK^*VxE&E zq1qHR^n}wJ7b*qhe50QIdix=<=EZyjk~}TaF460B;KV~Dkj$q(U=R9|aHXQ~=W|&3 z9X+1(Y4%jQtI>K+#S?7~8NDIqKQviJjI`m#4&mV^+6e|j#utc#erf)2Gbe(_@CacF z=!J?Lneui>#5gB(;|^Cd9hGhYZYHrtr}J&87&AxKpBjmOjN`@LIvBVt8S3uba(?B@ zZ=@D5aWUrbVW|kS8Zqr;Zb5s}TF_$#YmP!a%a*hcwt_Wx_kp}=p`;PDs%%S3Il6#2 z-1>BNh%#a*{r2i354?MnoHfju$-gi({RoV3-#DBV4QPFjl(gjJeU(z)Nh}9cgHGM zE9L#SuxwSONwoeQW4*iX-ND}9gJ;DJn%!3G=tZrX{K%hEsStw8T|_#)pP18gJeEA)PPu0xtLr@EWd*k95z_1H@cxZPX(mzgTy=^e zYM^@qK1@AqD5^b`nR;KT4t~AW^<7l1F-SQ5aZqrn{#dX`iBVT>YN%xhsM!~Bj#IoK zR`K-6x{o9u0%hZC3_PiP2^)rlL+wOv3rq15gVjy%zFM~hu|7Z;JyR;Ex*r;j%F3(I z`GerdwkXwBt?@2a(QYTPD4^A3O?9pyDI$zSJ?xLaZ}MKkJtfvmo6k7Sfk;(mL19cJ z{FjVtqzENCB)m5?mXB1%LRiz$mcp>^{#$dM^nMf7_eCWU+^G?kVaOdKSnO<(Vki=y zxc?nXQDLdl-e3q>3IH}<{R9*b*~RZQE|Y(35t+p5n;o>7M2(QRh{pu{d%+B!gA3@y z9Vay~8h@o$z%IBq1uD&BBQn0j&%gbDt64n!X!N?q@cswfO3+4S4D!g$4oF(K2@B%mN9W|+ zcGYCSt&BwS0vkRMco{0RD*gt29q|Fbg#hW3K82kxzO}{D{i4ZlQ6H{NdIUB)GQN`Q zI-pYFe|_)<_HPpG_uD4(;;OhBH%8C;P<&)s9Hz*L{W|C02gAfZ#$KCr z{yqaBKv)ZqLZmgOF$6E@pmmtw!rD zl8F(>Yjrg2PWL=E}KCkLJ4F{pr%7nDC_~;_qhSUaw2M34RsPM+X&st6Mu2Ou1dm-?Uhxx zNOiP_7#2=dj6@7JdEJ3`%vZiab{E5!{7W$eQt-q+(j4QrPAE2rDUZiI?Pa&m!mgs>5=LbaE3v;a%r_;^%mqr5WZ1yF`0)} zWMNY0%UOqtoXMkl2n-3GKQ2@sa$;%@6VBEym3moDGW!~Qq01|*^h8m=@`F!;ArUT@ zsZJ~=xCxM>V*DAy{MeuiKQ3NP+#esoO(+eyV;-(D+APbVpFKdt(#RQDftpC6C*)ne zI6JyWO{@RJuHqOyj7;Et5GWSiPWON`U_dRGy^sj$1GXpPCno4tgLjm8(?Bpud94>U z{o=U7hH;}36-fdJ)Kq4Y6w9X{fy&BJ;Q=a*+5$D^2>d&&=Dc6T@+z7IB9K2LNTK-r zGza=JL}oYCgrW4Y+zC|Ty`sw~pc_k0yVdP!(i4xi<`tmhV*k9TkO|8Almj|NoX;-v z2^Zix!;LzzO*6hiIYl87uATc^vO|`%oiA=S*I(%IXK>L5qS}@WdNsdDGYF}HYu1ait z_fp>j>DJ3=1X7a<@=elk0*L3<4k$_QAn@;NyujM71p#_^wj@Bvf&Yy%t5opFvE)9H zf6Y%{m?;hh-@Hjr?|-mvyi4(%KSjG`$fhr(@G8;Rdb>)k4>B4vcRp8 zw6!nd;{DpN@;O71B%oi4elbI74CRoZ!^@MI_4}q96Lw)!!HL#DeM{qiqMm(Yh6K|L z*)CzES4DFa2Mg1|2_fT%RAc2&6j! zWN_mhX!kHcaP6Rg2t*|Ff5sr(&Ff~g;A(xw&Qt&gkR{vq zOXHHtP-*<-OFQ>)(X{=+iLc%9XrlOTSqr^S1A9!lNF0bzlYj+b)wq;duc1Puqc2LN zX$aT`ypsSk2UrAbDTUnmJT9{8`#G2Ae=Z-nfA@)({4DyO?5YB%AG+ej-HM_jIJvK5 zO|fclin|R^N{|=o z>;WrwTDWIS$}XL7oI&!FEm03|wBCf(bx#93&L3(R0JpL}zh2-Qqd&Pbyp*5@2^7)I zQ(@xTqj~=eJQjM+j>?Ht4JKw50!mR9%o%`7^KtOR#5kJg2Y-Q$P4Ijl6J8dWZpRe< ziSc9Zcf$~Hg#cR>&>=d3@0BAy|59#{eZMxJ)_TD9bhO%(fKyQfXiww$BWRILuS;gH zwd$Zwsz~a}hmLUh!QCa!=e+iP&u1F3P2&N!bRaARSoQYXSssDWYvFVMg0kqcVf(uzV7QR$ zH~*W@edO^m#XHV6rEl^?w?XG13fa@sgF>MI&MDr1ShoST>w9S^VszX7I;eu}`S9&z z?~nhvd&rg(!-EAA&Of|?;4m8OO0)i-5abP#@)lGL%at~pVlNu{UC()Ru^>>7_+1kq z3gnY$OG2gd85c{p=j<2ES#di1HCl-s`>z$>fCpi_wgy#}!N`N_cN8_+E$#HJ3t~U$ zsRPw8!upT=Jq9oz6(+zs{g-LL15!U!u0csa1>mFKzb-o&Ym^dMutq|AIu3$b6Tw;_ z?q4^c9oVX-BGNm9)ky!j6cDclX_Wh@Ip{j{hx))f)hw-eXe5$bej z?%}|I#X#k5rgHL|6U2C&OwJVy#Il10x7o*OmIYwUe+i&}&ky&wvQ}Q5pruC+xigQX zO4LoEG=m?B%Aqf#q_hlW?w-g^)*7@0%LKij7ASZh5>h^1Nju`J+p`q%1S;KHlSGrO zDib|0K4{R-n8MEEpeKUC5CdJ@EpFg@8Slhs#6(Rp0jZX;>fS{S3fhn zX+8gxxjMpg%pE=D-pPnd>Y%7!wIin+H{dGbccb{Qw@ZoyI_;eZXiF1q15167aJRs0n&Iw^ z@-I&D{oL8b@9W`~(3(OH3&Dc#QUXlaLgu28x3<2hbH%!P8%A5QwgL75OJHy%&5G@u zbqjvA4ETiwYs5lTU|BC^=Juu9wGay@d&n3%_OpzswSY3pqbhVQ-93RC~?%$!c%LJzTss&o4wj zvz;Km1BexAC-h-1wQ2>&RsU)@A`TRLHtLdfdV9R(o=;yTldMOFP)}PW^(|lag7UelS zu*bAiVD&M99?r?jyE^b*GK8n;>U1H+i|KHyX{WW*kM(T(@Zxf6D6hJh*-@1!IjLt~ zWyvl#`{}_LOL>BfHPFB+zh|wNt&mTMSIk zcGXF#OfYD1tW$ScVVu&U=yQ0&lk_@#?x?4T4TCNTGK+oVz$0-hw!VA1#e{YF!{+nU zH@WWBtb7b~iEB4(v|dg$W((66=$DxpFQ`-$E!q*Br1%Hv-T{jwoABK|WVP zy`Qx%JnkfqM%+`(Gjof#NTolCQ;Ixz|63qH80y%SdwX~N>@{-w%;@FT zxN4vA3l?}8s+Bm26+~X~0KwKMR}|8>S^O)KK61>8z>38X7FTi0Y#QI4u1j5YL}u-dWwX!k z(}NAq+8>0T*^-aH-V|%spuBy)+=M{;x|GiHv2d&C5T3kxL^oI_sKMo`X>ijorIE2T z4wy883TKz9PcCML?% zMFs}6!XuP{|A5mBHZw}iVZa{7plg7fUi;m#x3?FsiDATOi@&Xri=K+FC{O|TU$2{j zQX+}D^ZXIWB{zlE&(D6()VK`4#gV@jLjE49=|%g(hK-HPZ&9vISY`w#K6wx-7krZk zCLn${-zNaJ;iqODj=U2_;l?k3D24Bi&hdWV2|)W!YFhYS{8UhKA+2pU2)mva)GuDS zQaC|}|Ai5Q zfP?Ml>N_lB-+UFL%RB2v*FOoSeM_2%exV`$4=p8k%Wn5gc7KZKhIqT^$dUk@+9dU^ zsmh#RtVF>hS@6K_o_*r|T_*pK5*DXVyYl@BD7m$pu)m_}t!GG3xkRW3{pO^pIDQAGq35maT^A*j8pZ_ZM#^hbI+GwRzBmnFwt z&yp!c{`;Rgg1H>ADn&H1f}9YG!%DGndH@D$V1j@#SQHg)3fNBbe^~fI)Uhe{eBv2+L5q|3bCv=`~7cNS=3GPZbMLMcKZ)4f$Vngo_fuX$;u>T;msK6{|sU^WCt ze2$x=1H^g2V+Rf^0^}wov<{~WZG9N$b?@z>5cLw?h*r@9w{1qwTf>++#%g!s#zgO z%PHgM<=N}QaS$L$A^q0T;D|l%J7eRUSs?{VI>BW3bOpR;cE=vlYZhD(?BdWR(+&Aq zo5nIc7@g^u$n7bFgPnYRQU}M}@r#pW*l7$TUFhMW+0z0vc?2dyGtHNA?~NWM_x`-~ zL^YKd16@=$0fkR|>gQXwYXCTkm(1GaXs!ACD>q@oVROs0?!sk?ulA3BuUD;##)pBe zK0p8CxeP|-0Gyl4+IoO+wfOVrl?mUdczLTUZae8ezC)LYip~vj-5j$Sd@HE6T48z2 z_C5>{O3PwNR>PUPB7;m{{>_p|6YiJF4mMR!V*J2=+%I|OmUxwU+OC-3$MCyw{t9Zl zakL_UPVCNR2)YTTI#GNA(*aBS(^!^2sQ&3Hg%HW5QPwT{KsqR-mc%=C?zH2#k1T8?jyCs96$&_lp*UaB-1nT@0dsp-u5-4|15iH3kA+~Wo{SbA=Mzh=h@M}rP&Y$= zMC7!T#y3Co7vJ|?(o%z;QoKI(2;90*(UDepAS1D-trc$ZSU4^99(bB0IeE9<@-3+9 z4BArzq%j_o-Gx;Hc>B0Yte?`@GQZ%B1`f34>hcT-%~28U1YUMg#iVU9o}MtV^rdQ= z3dascvV>CW3Dq98t-kl2rpPd*AFe}}KtK!IB^(71r5dPeP&~REe*X=te_*Z`7|wW| zKN6IveQZ<5`h^s&$X{oVn6*d!unB*4G=C)h{Z%AAwA7~c<=MNM$95gJ3bby~?kE~_ zXAU;j%c$LRb#cfIQ^#6GO*lt2^v(u0&Z4uf)456Xf;vqZ~=H^~EvyKEvVSxCUN1=+YZ9_$$Jqm^8 ztH-sl2FqcY0P9Xkd;aTG{bOAM@E|c@Y;e4^Aux?;sHh<#lOZ0W?z1)i2cde|96gS@ z+6_kf@FlPU>exrbfBh~JlmcS#y7)4fRWs9QPk-)k3yrqqT@{aOc0RkRW+Rj7Ql|lE zWirB-#O!#VDKp3r{#$c)*Ei9_fwVh#2c$9-2~3hIX28fLxQOE_Z4#oi^mHvI%N@ue z{mH`ix6sYzb+>2Te6XfVLlAYG_vIZ(?#9DX8;_paP-H^hy$x@j7dcl)udh-vidp0O z#c-q8U-Li}?Es~A3#j3r51&Tt@t*q(lQ2~l+bw;GW|DfF=&uX!P-p$3-kBl|>(NCS zZUGLg6R)@dr38Elh%^8Ur+Yj`{gY8w+$UarV|+$At{_H@+1l4v7e_)s1oy@-M_8cD zcE!DRppPalh$$%&AL20LP$h!9?`10ap6&yQc%~Hm{FA+aNe0~8MRQepykVT64^ezV zA}5PsY>I&nx`oIdu4`|%-g-DsF7Ve>3%)YYWMJprkfg_DN)vbf z*-k#Ao;7PqUC$qb{+Fm{@%D&+x117GJrvF-Ow?PCSJK(ORHZgfSShStwQ!)kRha04Mi!m9ONHke)bz$toL3blUV?aAv3^6(v zth^$o_J}itgI*mHz!r7)q z;OMobCA7F3SJmn=2oT-T%Ph{{P*ldP-2*eDEH#GyQGdT*B*Y6ym~0ih3!6i^jUvHYp(#bbkt z-=|a-Z@-j>8YyZ)*cpi{o|MJ@UKt)lQ}h0|jFrDq1XwXApenr! z6G;GJ`SK5`Y8eK)d(Dk_Bmmi&u|T40_lqxxYyY!G@eN1^d#&KEz(Y79wu2dhm7ai4 zvbIY=SVU}-*!6u`b8nH#*Ja08MjA$-f34? zh0nfhpB763N@iART0P~a4eyzLxCzNtlO$bvu$98cSh z9O9MPO<)m}sL3t9*oD>PYETRw_z+swFBq1uYyY8ZA>PKfv*!%*08QdqMKc`)IB25T z;i1FF65&E1gx>QHK~gn^usuY-$Nct8Qnjgbl^D~;O5wN(fr2Hx!(!PHwqJM;HdkGQ z&rxC##w?cWk|R(5t=Iha&^M1y#YH9-ahPCwEDY0H41mp*7a6(=R=* zhA{MYs)<3K+G+J-1V$dUkra?a0}$tB5h9vGWKT>?N|-cu^D7OMOWvf6YUHnZtF8+b zhjj0|ESKAc#U!Er8V2A11mTl30oE5ai*_ATrKV5O>uK((VVWv35-6zYpw8PoFW=;k z0TBWDm>Enk>H+;50yC(N5GT~hysno5Ft3rzH(?)c#~_`X1Ttr`yS~9?;wHLBdG#P1 z)Lc>}{q|gMMQ~pW>zG*S^TZRGouWr@5>R0gnm!=$Jb*Q7Tuy~L&G_)j0sNw9vaJ15 zwH2-13%=~%Qfy8-8pl~(ywRcI7)2rSD^Nhx&1dQmy@a4m(Dw@G!cZo>Q2@7B<>HDov$E#P*cC|iV+j`zVg`PTO6JX*KG{alrT$&E< z^W`ch^PZoQU9qYIxq4`oNN(rFpPzd99ia`+OfBgQBKi4EHdX_C4>nSC1#(r>4e3?= z@XBgs5G*3%izT~`Fy@rq58TjZQ^3MOp2K_|1MUF`%Lps11Wknj%Fms`^T=X{gU(TS>mapazpNKE+8qyqyw?KA5CrhtoM|5MckFX+FVD?i`z%g9l`D%-&y{HWf>geoT>?Ch&H)N&4E=Si%-GB^W0bDy5 zf?^d90F&RBw#*5G0Xa_BY-+(5fk`{Wi~y7xyHhY00Nw})yY?5Lly-Rk%gxy8#IRv4 zMe`l^E{yL(yQS5v*U9ME_(FQsvA0A$(Bgg&?3lZe0K%?A`sB$I%31K;p!9zzcPts* z<4pl*NZ6M&o9f#muH|XqpaXzY#p>U?CIF$Vbp2u>diWC8Kbu}6eGee&qyX2QNdZYz zlckM7x?NrHDNIXO_r)7GTTX}ZA!VO*oZ4vQ$%L3{QF46zE=5JCuoi~Ew*>1PEzkdJ zJ_#X^TTlUP_0j4#UqG-gGGu&z2z`asxp=yGY!Pu-yBqlqJH_!Qj~XA`P^?1K+qvF}Ft z9oK9T;ADxw1@&&^Hz4PTA9#SK3p2v>{d@gSI2*}}!R&nBDZ3lVVRf&B&v6E06>yeX z8d5n$#9bq;V2XfzZ<3KTmgP=U_?m)DzTn>^Cm?vh;ukx#Eo`t%57?W`z*2dB#*{JE zSC^-Mwk_8TZ80uSb*3L}rO@dXmEW1{U#ZSk)iSkCv&voZ|A_7PP56>yh7UVhx=lDu zZ+_s=cuXI(?#ALBr1wpuri9*2D)j8Wek#-1^>+cE$1`$Z7B&K9dFL?`S^+1kM z6-YAYsOto!*Gn7+q!^vXOR<3m-rU$1jsB)xt^v&0Eo!N}+Ay}w{FZ0ub9EwnwKvPE z(<{s%mZP(~U25SL))_SWAsWh)N!Nu(BG={fpE*a;J&2qRW)H6_eba?^6XPmXBFML* zd7wS!kpCH&taE%3$Uk|lxBY6r_*##WdEVz#*P&uQ+25W{J%3VWekx?J+W4(p_?_*N zk5%FdJ_8>d0@D~?Nn%HHXkWz#XDZOp6HpRp|sP& zoaKE*nfKFU@Fz;XXK_ov2hBWRsOVA~>^2Q!XKJmu*VW}kg0JBs%^LABy1XEPUrC9d z%2_-o7Pe#w1hw9+?+}~PAGimOA>VqO4nm`S z(1OUr_9oo{`{s`mD~Erkr=0aeS{U8k8=n(6;^)%zYS!kPG$MNmWAQI{=cI&bI@RF9 zjhwz%+J%X5@b|%;qdKDuLFIsn>s1b9G+$eD?E0XEx(0FpdA>RHUFz)>lEg7Dkk|79 zyd5uz(gKW3rLdE}8^!!=Ao^4#DTSeyu(n4AEAd(U#y&0xJ;eiX4bb7RajwR{HCD1` zMso{B0W<%+=qvymEFy#o(Ee+o(NYq&9X9{iLvrBYL!jPhwABAXqMWFyf9NOU zYg_7vw&iS7f^q^*+B;HwL(~b^(^lv8EvS3J&Zk0UpB`c7W%nAN$;M;aEs>$dh7q=J z3)_?JF&`9)+`pXX!&$wiGNfKLP`vtT)~bF{dNx0zvGunnvefs?{A7$HVf7J>TrhH> zY7(FS!#x8s5BbeP zX9Kcc+v-n`bbh>wLg^hwy&h=!$+}_ZQ!K4K!$oxQURty0GVyt)ucp}n@7z<;4f@9S%&83zdgM{A-NNlOF?@cl*Ilk=#tX`7yT> z{E>E(i&uZy7APx!KdJG2{MTq*b3YS4o{@X5`maD9!%4QaXXHB!41eHm!RHHW3GY+clkxi0Ol^4|7#GGP7b}Ld-6S1`leq zvDW7H;jI%BdiYMelQ6XHIqbd#SI9f?HwrjjF$bY3H(__Mo?(!l_(z@)R;oF9c~7Qc zcHn@Ka$Kr!hd|KpkG51W01<222dc3)1cu8%|kcmPLrzFz}9Y|0VxhQ4O)zS!3)eNt@M`gVAm-iC7T zK;_1Ua#5i2+pzi!8rui3`Vbl-SP;3-usM5(o9P-uyPMP1J9A3B*<2KZg1>{QQAgS9 zvmVjQ;nCe98LrnS9eG!~X;(ysDp}e6iI({EJ~Cn*M9?<*yZ4#a+Ad5*S!iG-_5?Mz z_*oBb00$~DXno{GNGl*f<-FL#yx9qFhOP~NsKQ6%Y}`~W!6&G}CC@&H!m;QK?$-u> zgR>@+?5;v6=r1>Y%>N0=05<khh7^x4|_nx}onny)q(Uvbpc?)K$|;Y)UWx}o`E!}TdgJ!4KR zj5V+y@juOf@yGVR_{#^DUv-%A%eE7K38Z-b<6r#cy?_4CKRWJTbN@#j{A4}&_tyMG zL-ON}`gd;iV-3KMI^^FrC_mCb{ToO9PrrHO7ytDFj5VMi0gU*?Gavo`m^H}~&faQo P00000NkvXXu0mjfmSd+n literal 34427 zcmY&E^Y>e)12FPfnyBkDBx+Nq9q;qsgh{EWQRFEN^GP^=&N9R7f+hLRKh>mplHRPD>YR{8 zfANB)+U$#3!Do+$FI*NKKg=;@jGI6EVz+qZbw5*Rb)U7leEr>#MdA3v#KFRaS=fx* zVXgJnZLX6-^y~YB!NLM6vf$N`nzpm%z5z|9i_gb2q4ni=u~%;fHU$>};VO3`)(-Oi zM+2rCyITj7PN!YQp-R7JRy?a}jS5Td@IuK$)y@J>p;MazQK{y_`APD%ImRV9vmuGu z{4~Z9wIr_|UVuz2Pef;zU)JlX&ls$%o%uN5I80{ETc5qB7?XG39mw}qbK3t4rzVk| z`p0~8`hx~1>}4alMtHx`nPQ=T;K5x~p`+Ts&BSkm+uvU#E-RdtuD+Kqvbg;Cd7MiV z8bNk{;dxIdFJbH7zujtzd*^dJ@Rh3|OCikaF53Cf+biOg0gBsXSe7j)d+FqqSs_cN zke*sQYgEq|?~eR;sq^!v3AYJa(uL*QoCc{}1AUB{%re|rlAeS*>&hTm=|mH)>*OA)brF(cbQ2k8!7X^~V;vy4dzdnC%bi7{|!CXopZbWO&?| znXCi+q=+sXr$UgPv|~s@vR6X#HmS;`&_HWnLaOngkFqd%q?ZS9{kS_A;sQK!1AX#h3FP{&C~`LcaRfwUY-SbZ6sf zL(OrjT@Ptx+xJ;d8E|^6{}H*)DC|8vq$#fX0&zbJ)ciQ%)Oq!zI_JC^&i<@-xPO#)c*!@Em`!ua@t!e?GK zj1A__os&cMnfL>rAc>a3!XAH0FP^RpL$`RLA72-^2|+)zi_7vBJRQ$Nd$S93R&o#2 zqG(CNYv?va@cYSBkQE6yjV{t!6k8_0biDxZ%wFpqsPOEl2~x;IRub<{BrWnbfzF>Y zH9JFsp7EW;g9rayS9G2%eNS)YNYGFfU?KzJM$ruclJ;(N?MgI9 zv4BeQ@kq%$i4&b=gcyOs-H>=vaI|vVvn%rJBJ)$V=Dm+m{fKq7`!sPdwP!>m4Nh+) zji-o-DgXiDWrrY2mKdZfeoso}$9~8B^wTUBG z1ZAfrFY-+Y9DbJHf<7pXc}rP{!wW%N89Xng72*%I@w!rE?N3g|tYm8v{6v!m3bQl5 zMj2{izNQxsg1KT)O`fq(I{>~N`(K^qjP`$T%eb;CxO$gWT!!OjFj|At86T!I^h>$( zM-&T(HKlbzx?;=Ax(#t=eME3}qjOTrxu2GEGwJJm($wMQ{tnA>L;`dwaOHVS z8QBSWkpZw2eRDP3%pAP)LoV*)>a~Ipo*9|ArE~A&joN}H8$GYJ1v;%=M}&ohXy9%2 z`WYKWbY9%|-31I;nZS+8m%E~1KtV)aNQQ*U%8>$5n zdc8HdnJsvPE;)*VMOJdJFgaBhG;%Qk(>G5us0=`3U!5%86DM+~m}}8dMx2@|DdL1b zOHMT6$#3GgzLXKi5BlpUBQH+)EJZrqWdEbCGdF`6ES4_w21CU5SYT2DG%DFH2$x4 z8`Yv#Gsx!X$b?m`hx-mH$Ad_tgazTkLGVg4>I_;0DaB(W0L5)ZF+R8=3P>@lnMg;+ z&fAs8mB#+I8<}n={BYY8fcT8zaB?aJGJ+5zDTU$Njl}f2Te>EsjMU5ZsVow@50Z=2 z6}@oac6jG9T|VFM`oM>nzUa?=(JER{DlKSS26+m}3@Mcad+J1= z{Br31!?z-m1TWsl`Z_9=+DRv*tD#k0mpB7_qE~(-iL3F%ishF=ePh#vd;+9>Ocu(8 zZraHhwHe>EDea!EnD&XTdX8LW^X@t!IN@j<8(?xd87Id2aJKKEO=9lwNu=af0`y~q zVB`@W&YE#@s?}cw36>ysMIzdsi5vP}+Dl3dqm4y-;9@Pr`^1FQh-Z<2vtPqLjS`ad5-JDb<5vWRXGotmj%! zm|~*VWDV2Yt_dq{sc4-O*@HCYZPT>zm{>|m3Jv|x_Lsqzcx$g^ zT1Xxl5GCM>-6$vtK{0wV#Mx~fJ8q|&h0J_Enq>UOmrzpIm^vTgfpOlXd>I z0jI+kQWb0OKm2|_-lvhzRUqRCiX$u4%l7c_p5ve4!Ka5+uyq+9qL8DWX6b1*CUe$?%|KK0 zlKLb7`;ksFeBC3z@R@Rcnz@LzaB22Q(@y?JiP>I4m&YfB8?g_Im*^{A?M<&q+G!dl zxy*yPB6C#;q2Gk1W?MR^q|{D6v(@3=B^K!9KZW4I)t{3I3dEarn655v4D~DW^}m&; z%k7C*nTeESP>y#hC&#fcn!|aV#sQmCB;gYB;5tYW)n0ZHc z%8R59lV*cz9$`??9`DzrQ{(p~W!s{bEgbruEigv|Nqb16Kz7voN&tu?YtA`wSQU-W zj1@)@hGy+R5nsOKWbXzAQ#;H0u=OAd>H~gG)|0kV<;PC!%Ytm!2o(oNjgnvEX8>rQa+H|ALWE85^iwOX_A_hR5@g^OMvVmmeC(Zm8 zU2@O=q|CmPnK9l6Rglk!`R{zlKks~F5?@OCF@vwsN~v1ik7p^n14&DcT6)AGXi=9h zoj^-5nvmtLBDh?jNq$Jv5fKn3{|cIeB=3A!U}l@A&E@;(l$?{8zW8w;Z&bXkK>QFQ zB8;>Me1)htk^l=@zFVz@HJ}FrWYd4V^0nAZ z7iKEv1EOr+6WYexN^0>=+QJopNfgzt-YGyxkR zb@m{WG!NWJTwgv2!nqmQ%}$W)B#};eqS?%TbzA$sJtc8l9gkHKj}3hhfMAG-2u{&3 zIwepvNt+r}7D`i&R3nJN<&J^CSaExhZ;&~@0szv}JY2eD076U-Hr=Y|V`8YgS0kC- z8uW3iA6fkJPLPbV&v!1T#`I&A4y9+zO3L(mG8Vt;d33R}w$0JoqBm7aKg;pJlv_JJ?jKQ>e8 zT71FDk4MkG7HWy$palSzzrl`miZF^%y4IbpQwwrQ-}k>Rpaq?N-|Z2NNzIb}Xqo6^ zI2|`LAcVSFefh2===Wf&`0mvwqxn?(Z8(b$2)@XXQGB8 z4ZOy*eY#T_Eg=l3&v^1iLJ_fkjR3xvHq#`aw*aLgQS!_#vREOEmMeV1nu@j^6w8)O zDJt>>l{4pWr#%tTmwK~l?+Fr)v`p~t>I>`3ZNO&(ApKanL9DbK)u6SRDnW7;!2KK6 z`+X(*?)IpGy?jZ>`#xcO_-o!=v2wdsw|JC+I&hPKF7seR2Jz8 zSW@Fm{#3nz_xERU01NcxmbD#|E00B`lrey3>uEb>%ATfGt36xebc`0K9F346P8u+x zpuPn#z{^&}P_EK!2TWEnPR`GjV7D87R&0vJdOgHytOdne^3tD1hfx0@@FiwYa(sZw z1y$6v($6THi0-2p$9PN|2?1+)$0QCB^w1h37fJKjdI#6o7Ff zhoMQ$b>?Ky#YF$3*$u`Urf5Gtb=BX1=)4a_rQO1iOI&a`_>pFD6m9`}vPXHQ21S6@ zEK|suURLT}k5(HXUB}Hi7v$ju(1q1g134<68j+d+;CEMMarP9kKpWwm{fWLYNc z;ttwNqDyNfZ+lS;?d5|L)>6s_LyWeA)zV@ig|KXl;STGO{NcrF@Bqp3!?25{qZWwC z9QA!V?j483b?UD#k|($26Q+ZGuMYwh*xCH~7#k03Ao=mSHhr-~k)GxV&p`o%WQ*G< z=8xL*S-$woQYspK52rwm2Aj)Fkp@XVg}+2PgTt4o*cXp`q|Fda+<#WtyM1C!5xtoL z=3Ab1CU^)3rTi1Bda>c;AZ~$db%ItB2-K%ijhO3s+||FhFJV&bv67~gEzYPnxNt^W zo+mN!gLWN#U!7}kAOoFR(Pj^Mdm<`bae7-(Z_QJdKRL>wtrf- zp1&{gj*DHJ#F3^H61n$PUc^|G< zqPM7$;f;?>Sa~H85tM~rOBC{rus*2vGt$DQiwg>RoOt`?mbZF}S)LE*@GS?tbpJ{7z4LOdT*CU- z@kUa=XO~II+38=FZuUjwQtYnPZiEjyrL|@m2Uu##3F3)}zqb5@g^H^R>*C;rN)*W)*At zwqNt9LkWRQ42FZlfzSC8N1wUNm73`Ks2KXLzV*v&T8-gL8l5s%+a&&MvM^p=FT|iZ ztmT+4hDd+M<$^(re3PnmR)?QEni0KMKslv%muMq5!CYsnSYygqr;1Ta_b|kG1O<%K5o){OR5fnf$69lkg&`E3^}T*aOl$dP-@sZ%i2F(cdA!@qRfD7!QE4_Qwg~9T^m% zZ59bYNlTFg*jR68Cvr)~dFk_K4}RyYtqB~W?Ute0Yrypt5bGe-TBQM@i_{$$W&Qrv z*8Hqkci$0xK)Lt6-GH;8iL2E zrkVK3vbpP=@Sy`=$bR)M_|F5p+y3GzK~YTD$F`|@{g6!$M^l7TC+l<47M=<{NF#QG8(tQD*B2;xZ=|c|rLiRZd+YiR2?1AwjUX{O5_UoZTHX~=1B-DXpvsqcwOrd@WO)sa=r4wiv+Y0fDN>{k|w5&YMq9Z zA2BNoiNQTEj-WDHX*9W^dAx#q%LKmZf4hG7xY#MZCz!fNGupD9_YJ6>3M44WD+v!# zkGV`%hdD#I9TS#t29?ZlRZ5n(ymls@NZaH{Qwtqke0kfa&fOFsT-Bd6%x|q3d={ft zV>RS<7NY-g^TbT9`KHn7dqbd~d#;m2*PR_RX0Z*!KF&K`R^h+fbav*1 z>919Ic5&sCYaQ(UQFKxi90SsCMXBRHgwb-}*`g z>z_Z^E+qx}qoXp!+cU}*FO(2VE+d~mzZ*8p+kSm$!N?Uu^`MviLyBhy4hDI{k4v#! z*TLMDeP}0It|Gok?M4vv^SEt!UOa6|Fc?$)&za6JH=E&eI$2`Z*hn&idtGvYnts+e zazZEFfY6TJg*d{ekJ}n;^L?8ENpZw&)D0$-(a>zOh*BvrJED znczk1s`#w%HqrJsHz5w|EORs{-YMY~nza;70DDJ6Ngd$lBTW}UYBIU3chmuI^`jVI zjiz+R{~GqV^+o;SGsVOqVh4a70c@^@RNur|-+k?X6&92+%9lptK&YH|O4r(hJVm_u z!iwHfzzv8TG7P2$Q>&oOF=Lv=-N-z&2^bYn_w>5q1{r40(OvlZb?IH+1+3sOHQLxr z4-7@^9i%F~&=gHo8GQb!;Bft*>1(R>g?fFAr?^gNz3D>kSCQox1-E`DWu6if!R?oc z&AGLK?U!xMxt`A7JBlpp`S)|Fn!~f{A1Ks7rZFs`rAco4haxR4D)l|YU&Wg}yXrzu z8V|&*Z}m^84#XzHD=y#o=Qw?t2|4=tbq3q@x$((*a}Lc|-tj{CO?`Yq1T7IftUDu1 z2S9PLir6G3nse>c)q zrLJM~i7Qn(BG9HQ#WCs@k*r~-Hq*F{R&)F8Y!;mK+(DiV)(;+q28w7u5Gl$xha2)3 z+Jl{{$ab8K+@)pyQQdD?}{74+}^ zLpY-5DHwhM$wjew=IQ(+>e2T1Gya_Ha73oy8(Zt-zpix+Il=-jzxDq@|0Bo)Ly9)VdH+YbK`WP*69RN1|zSCvj|)Jn)KX z?514nu1NoJ<-_)@SrL7aaXwsK@d<}V0@ z-@m4^{8_!d;28Q{Jh#1>cWcQ}=bw0gVwA!yepSm~);+j!v<(>Ay+4Dn_yRb(X~-z_B;if* zsG`n6o_U_nBST72N>L2J4FtYtppF)ZMdCcxt$u)s94mSv zu5YTQAJg3(eF`U^9-8OmXPiEAQN)jh(k7(&e45PdwjFAba@&gZ4Zzv%4x(t4JS{@a zH~5O$m3*z%+k2v>FXqRGBBe;G2T&ZOaBRY<$#lmo+Wq!&Gv_d_)AQR2-|QF6tnxnx z0k`#Zhja9*(QU%j#zv^`k} zm%{H#tK&PmL^x-jGDhnuI_oPMDjKqTDaLQq`*y74!{iV{wH1Q@3h+Jx`J!-0y~Co3 z6|J7r1l0o}-AKzuDM)opjB?_?*snmNv}Gk#la)rQQ;o+$t$n#xNoqDqLDZDg4Nj{R z(H-5c61JQoE^Ma=9t5`MQraLqEn7t>i88$RI=vjavzIfuEQ zSA4!CG}I{fq^kNdOLL_Mtxh8oCghuZ9xaqG=ZgmWj_*uKRcLH>6y%=-y|SA6IQn5J z|NH6RjxPUAy-*pN)BPvE{&lW}9lSdY_H3_Gc-*NXT-r=dbG{2+nr}Y2lF$EszTNTJ zV3Vq|ef{6xH{U<%j8|BG#)P)f^nVTB4ZlmWLyE8q!C8cl4hY~wMek{a(p%huMXdPh zWPB{=Hutj92FbBvEBttwuWO}k>To-R+N5iu$uB3ZI4!S+R(0H%u)I0j zt{ID)+Nt(59(QyxH04`04*$hx%f}u{lNo9MaaTi{r3Nb?ZG!1~Y@SS;5CZH?;~iEc zAft98RJOrrDH&klvRUV~)>7M>qRPEmr*g82$EgL+2kl!_o}>~D51Ckr9My&|v~xOb zN65G0T&)wS*3!-fe=AyY#XGZ-DE(eu8$=WGt$pibLBC3{2g96?OhCYd<(w=su-Q)Y z06=ok`iiz~sMf#O`rvC==lAd*!TT%nd^|boVJF zqT&Ze1)2qhmYckA3wk)ML+u?sAAIGPO#AX@CGq*XuI-Ed9V&-OVRi$ThKsV!AXnyd z+yyR&l`5dF%3#>j;moRi;kulVX7kt#(YGZ>Zd{O9oE}b@TCJbq6}+P)H4ad3J8}b) z2QIM@$AP`YCK|F+iuxwwOex!~w0#3#N-7al;a>e>cgG6?0x@|XbYsPC&)zDsy`cbJ z!CLET&C@_w-i71Fs|-S>?WA(GkGf>|$352PLmF^y+Z%i2$wq(ZPjvhqZ(lO)G0A(U zPe1azob~yxIJQ3IS4~;tz>&}XCg*@|D~YWu_Dxzsf~Oga?61jSI%|%-^du(}zZeU; zUQ*lH9Tv4v{oeSKnrI>-CgwWn%lPsFJVinAGCx+${+S)1;9Z1Os81msm^7OB0qf8O zQq0l$TSMSXxd#zx_}mp8G^Q zYzX7Qu-S3mf|!gg+l{lf;NaRO;%)9=8cns6us|`lpTI^&0d6<@6t@ucmjq88i;R;} zFm*EJI%CB`jZlx>{<%tL7a^^r9TrvR&roFIkDH3Hi3BT0+i7b2iNIm6pGspTe$p8o zn6i`is=d4^z~lb-IiGr1>)Nj1vrxH!ua%fxb)qF9$HNWbDA)k8z&VS>nnJ+!1xALO zaEVC>(jjQm9kwe#G#gA3H2En)Nd|=H$s!TJ7o=HypN6bc5l>c}pFE2cH(JHm&N zb=6xx?G@tgK;)5kZc3+fsEIVl2cV4EN^ zb$!#6XGjBPMWkw>y2opCGe!#eF*`bG`fvhEg;0W!t0=qy#b@lEssZEA>TUYcmpsYu zD6dvg(f>3C{4O3O=DlO>ecdH$J@6@-03y z;&padkQB9)xsu>7P8?EfEhA#`aqjK#SNpDFtbjpp0@;hSSo<0-7BiLUtq>n{GLOIh z!1F?f#Rk}M(WhV~=MF^M$iX6TPZ3)}(HP}@V_L1qEc-H&>=a@x)AmmR*7Xw7=hxn2 ziLZYW{A0BI(uoX;nqz47XT;Wy#%yaP@NB95VTpSj^s-)^VPA30#?k z4bB)lAj!kJZcExL|w-Mj;-e0kR1jebO{)Oi@p#fBd86EG35;u6jtefSXK zR+%vz0}XHowfhoGToP#xsPob13a1>d3QAnJK0*&8&h1OZYjP|FUXjjeYuSu$Ix3hy zfGe0!e_8*hv)Y3%Ta13>vmX`k4M);Pj!s+GI;)i!&u#~IG2lEs-@?#C(=_H^?#dsd zn*uUraEEa85+U+ml$+iVpZ?Gv|EfBMiH+wO*D@mFF(8h^tI=xnqyqa}18mR||6R}Aj(Do*%WVwrmu*`!;jUr9 z7m>WO!68LLIdM~zhGKF9_BVV9V4S#ewNUg$UOv`;mE~-e!U-PLqPxe{25pC6xB2Ch z_1%T~wzSH&k)=gwRymm19X&Z(ku7GvX7az{v>83^6=lGSj5PKDOroC5XfR3sudB`i z!H$K$F~(ao**{?k=iz@m+`kES#-87f+OymxT)tOOIG++t4L=^ED7^VYB3E$RTQMPb zxeYTo3t8Q^5|m%utquQsJ6#xR6*N$>Z5>~@|MS%JV)RBt=hWlMjfIUkW5r7Ee)^>B zOUTuCr=X6DO#`cdtKT#QcU!F$@*(G`i}%M4%}xaX61Q_V%?Xm`zaNu_m&WMGD4X5v zZV3AN!VR(Y5Q-0-CblCZpSxrvk1Wt|?4Y;OdUYH?n zDH``PEUk#3FQkWc%@O(i3}_BKH@Y!;*`~n#e%P;o)>0_w+;Pp4Ibe#jwZxXh&2wNu zZimWng?5Y8mo}sLS68G&Zq>ik)vxv=xZj1WI3#WS2M0FS@~ig*zHzAu>olouPRi)P zHj(uNn=5iQ0sbxy@FgAkoRnu)`Z^Xeytdz>X*;TJcH_&!0$%$KY(6#$!ha*0_1>;H z5Gz&su;2Zo_KYN|Se{U#h)kKg5V%GGB&8%u46KNt&yhp@2Iv8&Yag zb?@bjN6D}z_?iZiBn-eZF==a$l@qNfHgf&=#7eQg9*r%7m#zV#E!ydF+Wb!bj!eII zB0*izUai#mG+7!y-#MB6GnjwO_LpK=a*g0n%XjT_jnFSc+PXaX!|k;*8A6JzPs`cw zI^+?r7q8R|j3%e>W7G!dFWCEYWe&yT_O`?`{ka|XN39IT{)P_S{j$46L^i63Exj-M z^*3g2xm}H1GD*Trqq+H>*DME3MgHzJS7xm* zK(ju?8h}n=%GwXcZ8%_{lv+GM8R*Txqo!!PZs+YPWoSbt5945Daj6p~^yKFDf182- zbkgam?lTWgl6YLj_1B3e(g?)vXm+-5^%NiL5ywD$U^nCYW8H^vz%hb&ddLrOU64DmjJ;To1%}K(qX5b}%VoNHdER2PS-ovTYXnHw#Mn=wj zWAUc8stKA!3Bo%>8;t?AQ9)qd3=ZvMh#p1*urBi%%GrNxf@Dy2z!x%EnZSrMOEMk7WpXU-~A!< z!n?VB0jCY8KFW<@mUbmOLz8Z-vSmAWXo2O}-HtT>dTrG9*0*SU757n{+lqIavS;^E zP5ri@@B$i?vh#AQY^!zb_vRmcfJW2_D2?ngl7IvL)f$Sr;Esj!(x*QNcE+2$R3sa7ZN5){DC-&*Qo<>{wXPD((JNM zJo5H-WUW4oGGuZCf)^3Lw$A<zye%`RxP6ybA$HEHyA2S3uR>ZB zNlP1vTG^nxBP@Ry5|3gX$l--gN?xG|!wQn+t-3{X92lc2VbH{*^$cnQc!qvd9h%|xJ z!b+OOB1l{1=Y1kJ;C*ete`YDO1I4mrP#Ei(qT!M*`^oEPUM+EDp2z_}jh%q6z~|;} zO->JJu*mE2!_F6jI^V>WB=pAM#i7SV$G&t|{QR_0BT)`$x_WSEBH=ZBP|#<}r=HTu zGdrUUCF`4{z{_VxmLsnaD|VrGD3@BE`cXL}ljLiGY+9mx20_>PJB{C~y$A3KZhe#f zT)DswCVAbr^71~60nDtdg|hD?JPCsVo&tnCT`49aBJ56mdwHv1q6@?rT@s$I&b{4> z7g*S|u0M$tizWo$@6vXtu6^X8E&C$R%9vv~8djh44^JKZ1&s6Bpge70c;N#Xsf`@d z7!X*VJZS?Wv?O!@bN9#1QeX_F6ts$o6>U~O1n%z`FDP|8!6*QxO|WN?wyZbo z0`pdzqrnQECGet~m@1dD^@FJEhx50|n1(b}12>3z) zlm=LV$&^@9rw;%d$DW1Bs^#Ux?3)N4UQ+7y@1&glzJr>@Ha%}C*%8&YC3vOr%DbAt zjbz$d3!f?xc3WIpEN+a9NI3Yk1WoR*uvwe&*sP6R6>h+HW?Q^ zk&2X63f5KrUi+VXH*A7rLUoewvvu#Xyi@12h7{eD{exCwkPO*Fe6V;}By6jq?Ge8B zjN7kSU9qD(vOb^Tb<TgCROx?unzc0W@?SrMc>nm(tSlOOK!TeR_gmWn+jIjX7dh4;+|tiH8;+?pw` z#Zg-RAq`6&>^=EDg-4^Y8LQ8<^i{RxiyLOE7G}cwa&C;j$m_J|SM`HGAEajVfkQv= z?+?HXvP$9n7B6KRd_rs_Zg6}3c!|O;!Np&DY2?KMrZ=YSH&$kNj#}r__hiC3U2YCz z49w+&N&O&Uml+4)OUI&kV3qo~E`U5GLt&B}Z{u<)h7Y zu62v#l5lS-NXe*gxdrc|`kZ`oIvlJ<(KfU5`2A6}(fBo@ z^>R<^a$Tv`jISo0&a`)|K!WKD0b#wD*m#trNb`%W6+@g3l)YC?r4|P_NqQgD zrbs=WfnL|X#%k1I-H+ex;^^iHdIgj`B;45~#Tw#%v&FVk`brNEoc7EG%oSxD0oZ35v#va(lYT9d4jHwN=(16;DDo z|Ma74JuCvn^%EX??Z9?{2Ut#%z>n^d6589xtSS;P#;)`t*q69cmmCBM*#6VjV2f+Z zPFbpMkR!&g{v!M1VI-q z68vZ73a#|7TEz!?v|38E7$Hm>c{c zZ@gZf1(w$bqio@kOwQf-;+px9h>~q{zXs3GhjHSvB<6x?nh9u6PuKFV4A&>WPH&|f z8a2}OyX?-znbuZU6MMb8Ra7uPY^E}42@@wz^x)$_(zPwOmxYG`0Us!M7g2MUtd2_(R`!GuY3 zHAkoIB8L9t#uIvPD7Pk63wDDR$qTFE@?jDEuGV8OX2R!oAxUXXIL;GDLKQrqpkwnh z8>K;3MFQ_0(1gclkACMZ|E8w0=^&5NUAGAXY- zaQjtXCb3LDU&bs1o{pCbSY>>(*I@E4*TKVgB|{r z(5;@(!AjV<5z8XsQg4Tg#Kq9SqK5e1hjptjCx>lJW3HI>*ye}p1(Y4{!Fqog*&Gc% zcgLBcE1pg6Votw@@;*yPCnL|Ompv0JOjuYTgtFcbz{O6~!azC~@e=ubG#Amtv zid$;%^+iMC#HQSH>j5@1kA<>FR0GL(#Bi5>Y zpEO|cJn_{}lA52uaK5>N%6Y(2g5!7XzsjBqPoYa4xmTLN0Ft(};Dagt#H8-)lt(mC z|B^nHER9$u3J9M8H{9`4a<7Ya_NBA(Nju-~ar#^i=WjihAu0BMnpeJL`N*=RHc=R4 zofK0c3Z&SZ6YTD*jJNoECi4Ktam@YLNJg}|d1J6LcWxl{`>f#i1eu6J^v?OG#un#A z#6)ZMK)B%|wq9Igp7FSP@Wac5ws(&f#~*qu{C&CMFMGiKYcs3$=WS#=UCV98%Ep$0 z_`%`eGilkgk#>Vdz?XkwEnGoH&V(Te$`c?!WM}gH^YuUqM^r@ZOXu@bXr7Ug(QAze z!Y$)&S}=f&w4E@PbEjTNO*cf&+)i2hLpp&pO*?TuND_s31ax>-osW1BN+qcFB z;`q9GrO%$O{Gsy60>SUBQlYf|s_U7Wx|&)KwdHg*@WVBazCI2DxFm<0J+kwp<)x;b z@`pMt47l|=>KC-cZq1yEVzOe0Uy4;YvV@7i#f8Dqz1Lh-=p}Md{{yCvgOOq=KD|Cm zTh|bCerH5eGRTs9*g}6Y?fSsv>700nT13MBbM8Z9VLp_iqM^?JT5Y|Yib@ryqCPJA zU%R%ZYWIL79{B9F_8H2McGX4Kjoov!&g4eCPVtt^O;CniwsPQ?;jZ8ILn=%2|N4~H zjj&9FmNmYAF}YSe&mjEm{^0)UV29G3e`LT@{qNA9*HMFKCld-Y z-pf}{7`Xq)p%<E+HkD`_qPtW_(n$Gn+Xp%0GAvurcXPx+XsRRP3hSHPbe`tc`z&W!bei=^!WJ=7qc4t zUDD6HqhD4x2S+{#ySmy5%rD6x&FaW;GV2t-K4qT!@YF;qS14=#iAg-c$Y(0_{KsD5 z?3(1pvJzRS< z5$bj6%83n6GY#loOj7f4&Ke@B)Y!P@y1)17qC1;anF!s#Jve2} zt$fE=xw_gL*HER1jEToH2^hWs;vBn-@cwTGAEk;Z(|Q(;^RMHp`I%xKlF-_iwfn$o zA{$^OuA>XF&6;sgv=lrxPH@reu`aDSz2`?Uq8cNbVi(IE)Sw?2oUx~)zhw{)6;^O; z`V4nO?sxL;C~AH*X2XB08ZT&G?aBQ5;_AV2t9G=5F{~#Xfm3Mz_P>P~%9{OfGWl4u1*-tuKC*KJvBvjw6DH&^11ZbwSN_p!DD{rPl#@4fy?39=r*z2m$q+~Lg zKa7-og;6~*DkCkbdBv6n#c#JqOw8NFxu2A{u?ipsF`OnGrsi#JpWOHq;S9>gNvgAN z+37J<6vsBai){p4rj4YJlt+-2TMM6?R{7UZE2We6NRbk1RQFuP>6+3yWx46I<$1|7 zXe5OgDEwWHuoj0lb)#hAs8J~-cXvJq1#S9QUHPVp-%OXEj6#-QeZ2f%V*@J3V~7J@ zy^D#$|DR1&l@^P-wy$1AqzQmrG}y&zh8Ct9rEIw_)-DLhlnnE#i>p1_+diz<$GqK+ zO*>=1=R29dB2^kQ(N2!&yWy!fcvt?e@j2GlIqi^dl%t^IkUrJz1bSOgG=G|o``exd zm)~M=^dmlYGAkk(F5Wo~hiBL-lG4u#AycA-1z+TT4fRv8%XiP8ILLC0vG&Z#8~EV$PSK6YW`=EeV^(!8leqEC;$U08D8Drg)Tf{#VD)xfZk5;DAyp;KQUzY&ry*35;eH? zKX>2mnS<&KmKrhT7YmU6j@{WG+L?YxrS#Qd!B^#w!=?}~zBaFh1e_#ft7ZGfJz^XA z_J4L%#bcVxU8#kL$izUIO;1=l56ML&5J+=Rx zHDq!UN(NRrg+1$;s3Powt`&)c#ayY;k$RF`Sbs>v|Axx4dOY>N5&DE|&!e0v5}=PU z=f*)6{s&8Cdbs3>b)|nE@ z%E>3NopLJyg|M)!-p~&_3)S?5+0=oT5D_= z{ofZ}L_^*GQ|!l1{jVL(s5sn@6+UGCMNAMMtZUIJ_uv7Eu2!917o(x}NbqwS3QE%7 z@efh7Oz#vPyfMS32_w1BNhr7nv60xl_xFatw=?*0LY=`BL^wSyjg*p7%=p76++g52 z?oU3Wzv-5`7Za8sJoPPMVaM^$!am1U&F}JGJVC`LCnhE(wFjN;2pYZOpNF9ZZBo&p z9DneB^!K#{>QoI*jD91Q-dK-vDJ-e8WoKnwxjNe=VUc(649)l;gRzPT1qMg&Gw@0b zVW*#&(fyATiEQZ+P+U|@h3)xwtT_axtaaK&^*$n~7Re#_v1Pw|JFdu;T1%J}u) z9rwi|`yVKt)AND2ghGZM9X^Hx8*5G7mZyHgzjN6ixH+FbRW zL>DcgvOpEOjL7v$wP=J4?vnWg+2FX3km;%p_DR}hnrrj@M9n`9d%!Bq!bls{6`A^-)+yDa0Ta*{`mFPMzgdyykOX1OtzyeDdkvmvjmM^>Lw zi3ZtRyTQ(5I8ZH4<%MBa+n(XqKcnzTSowu*_56fhZaLWKGCx-}tj448ADA!);+aJF zOU5Li^t5*ep1Jq&P)u%^{(LOW@IrO_WhI%&SK-K)Io{Y5XDf*fn_4uUtc~InpPxwb z&ZyZ;es5ME^$U1<67ns?u1$Yk5^h{0NNH*2Cn@J2Y2pgyOV6ZSqre*7K$RQ`mQh!}WddDx*goEr@P(nMsroB}6xR?~F)v(Go&* zVU*}hgeW0;@4byqiWVZG_vkeQ_l(cydw<{i+~+>mA0FnMIeVXd_Fn5<@4MDM4cd`{ zf=O-diEV15^lUIuXSS7*gSb=OdzG)IlB$%mX_=SU^;6_%oX9kAMT}n9_C!-An3hg! z-BtB=gJ{n1E+!fEm{j^K&!)e%S+r81@tR&(s&~1bzt7&f^lU$DhU{^;TBKTa7fw7G>k>bg=Wu5 zqOfG2%zu)jpw2-L6;k83ZcC^$MB7(t8c{}3b+#4oLI(L`13VTEHiIKIGJ67OYL*9+ zi<@K#Y6Rj*mqqH|5~)pM@rh_j^@42VB~3z*fvDthFv4I*p!@|DI2un_mXcMjlL6u# zI!Nyp`_D72LFvGC^?G#23Dp@nggeQ+qk4p6>9LW2v|o=!E7+x}#^fIN;H@9TS*0aM zxETwOgP*Lhr2yUHLU@@9IGJe2K()O#fBzv#Ezo4Q1J~<$e6_z62a4 zCV)fE{;C8~W|a3uRt`aq4sInDA!q82J2~Xgk0VNB712e$+u(eP9iL1YSsek1)k&1j zDb5qE3P+BGQVwy0i%|{U+ZtBCi?D<{(QNs!&Ym<0|DR6$w;!=~RUzcxha+v|1{NUN z@UL`_<(m0&c#GJb>;VV+iPA1n_z4GjY@lZ%s|{ruBpBrlJA1cYxKEgDXeXcxHwgF1=a3HMpb}eLTYhFs_HWWi%&b-o>PF= z+Wx#aIEB-(802?0J=(AlB!?IYsbxMH;S?aa&r@5Ws%)QR#7KM)PxG-+ z+bHvs95YNHO;9b~sQ#(=WK@IcTbrDgoP(eB-+%44XKe+_TcNY|#bqwH+0gB+&t=1j zw(BM1XGK-UnnF2z`r%3TPlaZGo`)d2C%z(4_#fJ-1gsX%GgOEiJHShZAsL;ZhZVOF z)mwS4Z8yQa$${ybgmG3D_ga6@$_<+JJKIdzw z-9lQI>Z@<)m7!EGR0l+HhMrP9%zpjL)(_7Aj8>Y8mm9pxOgX`aj9kK~D5WY2SqP%& zoX0EnL>Z~wsxWG8-am%S7a;D&IrIFWI2+^-N=s#Akj16R71n~2?XBF={g}iK^`gsT z?|Nqr%HJdeFj>41>UH_QM`N0CR&4BdAh``rHV8}G<0G{G7}zl68E0vm00QB#C?VW| z19fX~yYaJKVI*R%zn<*ICI91RtxtWxfmlkz?rQe7EMS#lEQiBMPG3|dn#lWqhWEM-)Cfn5eM># z+nG>#9$fy`Dubi$}c&^K@06h#WaC^CX7fUM6J z3~H+%6om{HeO3_sArJSSV|Hww_^g_?w|cbtcdjz9M2b>6Q)eydjItI=916cC?7cNr&Kj{fda^4 z0ETAf2`)H*6@?{%rs!Lxwg7PJ-DY}8F%oNSgX7YNP&U@Uy+E0QOf^iimsK%99dtW# zfEUy!Bnuel7h8QOF0OClrfWgfQA2N=@xXkzB@2)KIX46eowQxRA+arxWFJ zRC#!FJR*lQ8-No*qJOM|H6I5{zQE0k<(bmr(2%NEQdI@+olnCbH#&klTDf1a$$v|U z2JF^2vzU^CCD0MBVfLH~tiN^_`ShFm@9D1J0dI(oVHR@o)&EnpGE8Vg)9`~xNx>=n*8a<0Q^kVo60j@yZ!w{kw{>YCp z>d{nn&`u7$CP@rxH&`YFzLs+^j+ga#w2JEa!9fBeJ0u}x7sy-!c0Z8z+k7Zy9URS^ zWbm0mCX^mG4oe`1BWMDk&ytdO=&Tlx!or5sI(Yf(f?R)UL*14>&n_o7Cm8$&8FkDJ zj?vAA5jpLt@~^!Hb&dm%bN4%nu&*_osKV_mQ4F3+AOYS_}+Fr{Zu|pQh~)% z+Qtb4B@+~w>t-}I*o|mBU;SYY6dSkwk?KRAp3;`jr8c_E<#=5L()E27mVk3|a)R*i zD*NhGCo=wBv_-4#%wYXzcd_qOb!?qJDWCJ$y70XPjvM@8lvZnfy-0d-|NCySgm0s^ zHTyk2J|Ur<&57c6<%wF%_iwTKa^ zG#FP+lm*LZ*@;ieZAbNE7wE`q!8iI&J|_I2t5BM1i!aJf??VrGq^7$3NNv8IVOVi3 z+Jl~x8eKjiKRqNK>Szoj@E{@kAUU@V-3Q3JYgT<3(1jc#XVM)T^4St!VwrBo(!i+6 zZwMP3Ec46KK(Eb-M54Xo_wt|O7z(fxN8Y9h75H<%4?O*RmH4Z*0_7{){mzFnzn-B2 z$0HXXj}Ju+Y6i^E!`%AZ?hw1qO8ljGALm}l)ET;y+%-AuE(Q^gC9=DsrYNop9-@c% zN1?ncnwFv7`U4BersNq{Cni!|pmw7%=irb7fG4YOI3TDXk(R|hFxJvDHrkrBwsq>N zW92VK79R&<%k49aPXNh5U6k;j_W`VPc~swixeZV;aVe+@$PB8W?YYgf?%!hJ+@ia^F$~uKFT5W`w9?B)?VKvv z)!uN6(ysf{DTQ_Vz_h3aY(c`P0R)7;^Dz&}ahdesPCs8eb(eiy&rtee(XwCA49%9t z2A?1;qwiU_RI9~I`R68+P~Z@7G3h@;nwXsDjU^Qw`P0%0zEfQ74iL;wM)f8~Sd>Jt z^Qa#1UW-XoN)<*Z03qi%HFzbtuf6fPkC&`M{+GpeViWh3LH+J5Tyn9>&O>_|JXON0 zHP&xQ){BQKKLnIWmn9x)iN_*6=*n^Lh;A2wTz{GW!RYR&txyfSIoq3_xxye z8XqjB1nfzLcl}(m1D*1^kL2{u-Ki+)fQ?TNVV%<2f2r+4)<&2^1?Tn}_|n4t?nOpl z6rSXu9;h$d*u(N7AT%`AB+p>VrhcP#Xp?kB&wInFhxRZoXH}9+VbNJ0yoU+@e_$`x z4FcPnA#~U5e_^@l3}=Ij)iPr6M(&T9DEA290_FOo)zpvXEnkaT3yw=lM%`13J4BtW zj*@0|r?e_Xo!k-p74Tc>Es|yT(p7t7Am@_1!}KKcY_-Z4)@k$z{MIH|$xQG8&+c4% zKtjQDcao?c?l6sX$#;vm8`LaZp&o2_*R?1!I}sPil7N7qfQsr3UUS=d@2aF}i7yoq zX+!r$V|ZH{YJXVAmo@yZdhHf!K4)9g&L5$>oa0ROO#7X%vM6}9VmMei88Kw%+I1jE*Yq2Al$xf$?@9{L z+a3lP7x7saRiY{aBZFwW!vdXEao$MeQY0}1o0s}nW%5nGFuV?>XT{-?G*GfEUGjoA zhN(~9M`5MWwY+sMK=wN}vLixv$;1DA=5a)T??SK_{Obupc%r*7GA2=lh_^BHr$oGX z6GMeF|CnbAsuT6@8T9FxtwGpGCpGP7o5il=_gr~>YGh1vZ=|uG1Gd_Jrc;+|oIt~T zPZ**cTqEi=w6SJ&9dA(931ZUk%iRpOvV0JMY`q{iYyWeZzMSKNn;=bJ^IYAUtJQAt zU}fkPe2+{*SkgUuKcyT5+V@cnCeYFf)U0?~QW}D1JX-h*BxKzDVxrRDVHTkP7wQDz zEY9z56KDqRy2F3AUXPs|jLi&vof*`yY;&;!yX%?#D;1)G0?R%96K+vy)%Gs9qS#bN93((ji8+p7t#++p4^Pvz#+;#q*rf zenHhz{WljM{E%FKmxq9Zx2>1}WtADqD%wp7SoEFQ=474Y#}#)0!jI;bP8N6{u-dYK z5+aPU6Lu$p@UXr}$^7B9o)m~AklB{@d-}5HojIDWT%_!m{G&%5VI*`ezgmph^uGtU zVqJ9Y973?L-=bDr;6VGSWwXCtuOID1j`>Dllkq`Ln50R5kt)J}+513akEViCJkr~hif-$JVHdV0KP$whO)V1>f(Np*v(Gc$I&bTUfq5$e8sUo%JHlg_28BN93>R7 zco4ITfzAv$zy%mz@^Daqm1pt^g0A$v~O+G|5xG#}?#rB)0Kl}Nb6!O*| zH}DZlJVv%Ga944~>19ZwPWy!>Klhbz5wSlP;^zh=L)D@WLq2dnj-O-L2D=+)CxhG8 z3qFs#-7h<`)qvG;BSp#smbUbw_)VAqGjk;K?GIiHgMx36MIMG0Cq%A4xNrT{*{vuc z&2#Aq^#f-<96NxxLTP0r2mS^6JmNM^-bYch8WirgX3_3v1(3a9477XB^ zezE;VrM>vU@0%*X$vuI{SctohUd96IfycZ1i?_vWaGLt|Ci=*nc~gm{jn2HM3AQIp zwopbpy_~65i#{D_C9vh+Csc7co5Kod^c*D|DE{k+kO>f$vMKe2?N ziXe~m3>26Q5aO*$E{w@Hkf7hJd8?5;B#xSegZHf)oQVsS6@4}q{-Fa@tUZ|LFbw?r zz-h4%mHod;vJCLQ8y`0` z10Gx!k_TTCqKuYaz~kyWh`L@b|2eOtwR}h5{>}7I@&HHLD8lp?pAlGu4<1(ylX=8K zeAU%S^f7w2`K9Y@)A3?YQ~(FinrQtXJSFfe4<8k)IPrb!d(}GWsqYF?p1_>RvL6g6 zh9+u_3WR8h#U`%gWIZ)V$1@V@Tfu>sOdsE}%JJ>+U;%9cQalze$JNwple7z1Cs6FJ z@GB)XJYY}V$ES^UPRq7yljO|1KaK#;3;L}{SYcc>Nn%6Ni?TyNVr2C;ZLV)VOX0V( z6q&>{XIE-djqULQsH~>GMWcIOQxgL{wkHoSZ%`Qs3rCtD4k!>_$|c&oCisRAH~B<0 zTIT?tck(LZF#&(k44a-7R;#)tno4rtn3=0fb>dN32l}(jOA;&{$qP159vpo*6B{ba z9Le3>cn1u1d9oX{q+?nC{cgsm2}0l3%xylJr#R9m`?TO+%A(EuCLQ3<3CPEqZixba4^!X!+{$qTYE1zalFd%O09t_jD9X zWr;e;xX>}qF9G7|A&VgGy|u`>s<%m}mR&Qn;^Rq~_5v={g$AvwOI~0nEE?4Uq=}}i z;bQlO<_nT5Tr0#e^GloHHhhibgPoR|wlmVGyTczT_cLC_O|}MpiI-$;Rw$wQ&h@;E zXQ9Ispn?BFW1C@N$%)#cCFXTjzIUa z9}$V_F<)B8V`8m#nb|VO^*)GG}dnbt+1dincQYx@DybiN) z-#!JbgV0j*A(IbwX02peMBW$9$CQ;+ab*uO9n-AD1!*hs+1((flp;?GriC@aJbFlB zYVQlRt%L6eM^?(lVS{7j%DT{*P4{%-WC;+9uny1*t|oO{nQvYWKQ^UYT3d)EB9BC# z>y**lOtiNV7=K6v`LG#`%F3N@C3;IFY=!d*yweNvB~PbAdXd#24khz?Lx^}C0Xx4T z3eAQhek_^0eFd!Z&t%s{kJ$N;WR%^qJ1OKBi&Ed6fK7)E5LB)+lyP^1Fz)6!hu3oL zE1h40Yp|dYvS1=PVnHA)f;k`)u_$Gr5vK-an?tWb1^^2mWEO%HT6(~HgnFb1g?D6i zo1?G>00;2+e>;GqiHAH89w4()&%kmqdJhY5les@RpTYL4fRdpe@4fgj2{?tK~}|4v)sw)geSZhyhHv-KYZQ`;C6HW;$$oh)N>|lUT?4Jn7w9 zeEw7fj*4|K(fRuV0;pR>PUOYjw?uvJFM63$?Ka<;??Cpn3|8uqVR+vjj~oSl6b$PB zsPxgLQONaHFR^1Ap}@kL;S!CfThC_$*d0L)!TD&hJ02 zcl}+U!F z4LK$jk3pU=z&-cBTjP|!&(bh%tsfZL>5=zYL3a0P+3tn}2f(v^07g4D!ppodLg`3gI*eE1ND|_(KXac2FZ`JKyN=uEyNp2A0 zmWHSE=9Lj;m0~WTX@H5DAc>B_V?nMm?ssZx-+0?%(uNT+amWNb2{r_CCG}7-zIW{^ zOYMt1v^f2D_{J9XTI4J{VPcID>a#s3Fum=I2_a2T5|oxuIZpR__bke&zL8$>cK(<2 z>(!avKr(y$Mo1Rlt6|T{b?ZnW5!~l>PMW)qFaY~i$3tI`DhP=-&!<0L(?l}J)DV%*)^C;?YjfP{+M2L|clNcnV+gIe7*al_r-gpQ3s zI1fjZJmRa)?+4tYG%y3XF;y@kI_QD1uR@7pO4Nuxt<8sbEBa9pBw;967geNHnNX}f zLyycog{I+LX=>7gMu-WyboqiVyNO%*>g`YWT9l4sXSMov=^)nwMNOrMDDE02nm1`ARIv&YEu3l>tIuWceTx_etQw+aduhw6}nN&1#FNBqG zHv>rUa%ZelNF^ARLqB<}OpW)xG=<|nl%UYGKvt0Jvu$zK@PvT^RgA(8I`oxKGB?z1 z-fFtiMt@@BS=*nJnIXe6vv#+o-jVlvyFY5TUO;7IaoJxi5|iW31}XR44^Hxti~i81 z7e!ya#lFby&2Sh|8+ePxSWvm=t!w3Fgo^$YvvwM3ezm84t%jTIn+a`QBM}JcJLI_E z>g{n>)3T~hPm761oe86Yy~S%?7=l$^XZ&dNqe*In=!;M_QiGQ|yxtJaP{G(%++}g8 zcoqY*9@#iLkQxhhfpEI0!JUz_QA8N!qtT)^TYiLCd7`id8?b6RF3EjR%75*hDEzMm zV8RB~mNnq>hY(Iow92n8vA|Q=Z zH!>szsHA(}|585{gkk68#-*g3RNS!!6$=r1<1Z*el3H-q(?o4-w9*6%_!N@99;vvY5o91LS5W(@7<_4oyupm>-ekIm8 zD*umk4>c%E%J#TTF$~#XP|(oOP;6L!#r{vsiE~0YBc|gLhn9Dyrl5F!d43#D&T_@` ze;%|!L4CvRY|?T55kD-7HAT-8t*1y6U$xV73{F)86 zU?3g?jO#P#LVlNZVy2DY4|zp6VXZ`9<`1hy$hROZyNfm-&&AadNlUm*h1WVZGcH31 zwY=%Yd7Z)2c~0dFcKL<8T&858Px>rSYpakSbgZPQ!t?YpsH2%&;in=|aZ1k1X)5y| z<4_aDcgqvRhZRg3$cYU=j}0OG@yd=@-5r~*rc);&>hFjy@gvBbma~0YjJk3m;zE%h z;5|u!aVUO)@&z)43e%H&Qip&A7%)>e3CmAO)irvZAzfqfO}$ygXFDc1|RQzr0DAfmRM) zqO5Xiv`orbYYBoD?O}}aXi3&tGX_fjZ~7#o>OZJ^TUvkLC_tVQWP zN`T9uIop$L5>U_UH<`ht+RwFqHNV_N>0MI%CP5O_57I)?BtnU5?xUtU^cgtaKy`w=vn>wwy!f|G&PNfrr(+9>kdP1v2f9Yyi}B}fnpDtP zS@y4~H|UdD%T)p}pO+9KQRyDsLIc*;)`r1gbayR6cVUH1G#EYQMiFrjafn4isKKGs zF{I$EhzPleE12<4uY|W8C6k4#Dp^_)Qx@YsR}~&9i!+?ea^dFS^l&#gRL;i z>)jMw6#bAhF8Ja`KGlB#t<=Goy>CO(kY9{L=02NUuGi|APvQq|!-p~Qr^Brx-~Xsm z{H}RsoPQ(&cV*c;kcbR4Xh?8H2e77%s>e8B;Hbz%_TFS^H+dxyZ!0h? zW?aoE@87MJ<2zmG?r`$yi?tXk%d1#+*W+;3*Ud7T4F;bkh#e7L?+~&t%X4uP?#?H> zCp+1&>vw>r%u%eanPT9!A9H)`0nEY~TP^S+E?4?z*^-uXl4+xA0_&f=ijg;sJcCj$ z^1&-kzcAV!um=$G&TY1}LB+;X)z4Qq)@)RCa0iy&=YP^({X;q?A4rs9o?II2wYM_QzMSb3H^E!aOMY@LobL4W_F6@EW=jQXSn;Kj-kUc+0qp2B~& z2V?@>=NY5>^Z4sxq8!*yNG<;nBgY~2yJgC;H-A}Cc~P>V1WW?JRb=?{h7lq>Y+SLi zPfZF!d*Ne~TytZBz_bV@>e=bK>HXr+U+v#q*8{Kn_$Ew1qd0D#LPz(F`?l^Ra4`UO@J&cT0A{!Em&}}9^7zc?Lu8&g z_n!|gyoQAqR>43|;lH=^Dc*nEOP-2yj_(=ekMA+#1(t?k;SS90bjKVRj2p5K_$alX z8B#aXAjUdbSw&^SqmdBoc(WXDvQ>3nEK9(J zQ!$AL8W`N5G9CXfl<1L9eA&mx+zs0g#n7}%@ir*>$AXB(NX&m6@wh0fXefdmj#<61 ze$4vBfEAz0^ghr{l4+DJ3Ha{BEwH(fxGV;SSg<}s8Cm0kS#FqF;^{v5aM(|LI8QF7 zJmmlZpq1x{8=H}Oh7T_QP9wOF%Z+O`a#&g4T&gP0L@VtQpo~zkUE%*JB2-YegzTdt&)(-8ZVUk{`9SJUx^7iu_N^KNVLF#0ugr z*CX+^2Rj9eX5XlZASPVlFzGe2Fy_UbiK$UZ4`e##Ug1&;#k5)~E>(uAZK?Y7Ug&eYIsQmdzRst!-B z*_>*f6g+3Cs=Te!R7PB#xx#GY)Hyu7JjSbfxs+9mbzgVNPyA8KvDb1|W_?h#|D2oP z!O1`}DCThK!>1U*AhGAtQCt;5(geC^EKwEbDhb3MAyZB!iUXYvi)vJK*3mp{wA!IG z2R|Ce|2j!u{tl}?N)Swap3 z_56z$Skyo-K$!78wt7TgD}7M=-{KEz-?+{=W2RiX8L+86)ipXp%OWbNOruzn?c5m; zfG66h@iwl@8jdov5NT$%#(C?{MN)Tp1Ta z776Dlq1Vgm#!(FY4HPNZ@DHpKQ6`N5{jezO2HjiKz7K~slYmQ7HMsn=qew$IE3m)9 zkI=bPeGv9um0DrD3u?$np~}bc5~o`n_(nFu1GZG!lW;SbP@>61Txs&SD?@S&=4zOD zA=so7*uzrWXH_*Z))VEOnG9a$gfIj?NlbK@2S}v{97ibcTTod+SSKbz_>kTwA4v*_ zDpKT^IF?`>*StFwI2N;nd@#NN6FmS_QtVBJl4v777({_t1;2Q|ShIW02K8Z1VaSpU zMgH?w$ys zNTa*c^1=>siTe2J^QvCt7H1{_W!v8$Z5r;d(fE?*{)X7nu!dU-yw@tGFufO5;glAZ zP*kLDpja)Ts;}4Zs7;7RfXs2&-Bi$pA7K=c^lT*z6%iGCz2C=@KVo{OdWv1CpL)(B zaUo~0@=^ODt>xSw{zC_aciYH-eey%W9tuMGSo}k>k~mMeH1HNExGJIhH)90z2U>7T zZ3X+AbM*uI;x4!8?V?o|XMrb?(F+9#UG zhL;b<=-eSGb8P^?e|$izw-VIgr~|QN?TqO{v-w?0zBXv3r4*4gl7XYC@!?QkGH^KA zogX@So`7*_U71^nUzufurWY%N(mk2K zAKe3>D4n0eU6HbQsyan3%Q^t=EwNhss2pn@e9d}(c`Bo4kYUvH2^#Qt|J@#@e(PGxy3H0wguYX-cJH+-cGsA6jtv4ap$+uk&|= zev`#e1->iy0N)I#`5~ASUjnv_HJ1YBc3c_zk=mC5+}{7pT2-Y{S*TZ@pcoZ#zzH$? zD**So!`A=zG3!f&-R`t|<=$tqKO5gGC6UN`U#xJNZXHdQ0~xcsf`W4mwv67_osTC! zgsG5w@f|LGbAuZV4Y)Sph))$TSr!!jcF|L}t&?t`m||)!Ry9OeO9B3$tH{LV{q!b@QkQ$f&s|d$2J2cVz(>ZY{xI`A0{$YhJL1|v zw#++^IVPzWi$?m%`^KovrHO{Uid^<^K^Z?Ux1=AM6q%izOxrCLtguhHwPB^|@%laN zUpLo<-{;Tb8}U0o;>meBDTKC=uh!?$ASy|(^||D@U)V2@*yk}CEKDAu-W5u#e3sNts@ZaJT&tGJ>B>aHo(yI%8bN0 zn)$4_H2w7Cx(af#G^g}bXQ8N3rt*{{G46li;DZ~tOhK9 z^M{`f-HXA%ggksZ;;VyUEsf!l$xo8QsysRIMye(9Mk2glhAvTBzpdfa$-gDt&VP%| z`4a>-c&w2L5MJqJire_nLcn!|?x?L;^M(GWlzg*GGZ9S4Ks2k`VGsYJkLSCdm9$k# zE|;XVpcGyFtaLwmP%QHk5H?`0=-!(se#0(;?ZaTlPFIC@EmY` zwLMc0Fy{__w4YcXM3;*BJGzO=K5-XL6fJz%(c_fH?(k4&scCqTA#L^BP=of9@>InkxnDe5En5l$dAH6JsJ2%;;2;e*FCa~2I)QCRT5d)?$>EEVK#2HI%Ottg=v$#wPv6@x zSodV%NbQ`#m)Oz5%3?(KXt8+{_u5^gqG^5B~xXJ%MoYGrnl9J50k@!Q-@Rpt&*H8%`9SSaM#hcM%Zm3P%-o!-1(W`=8NHKbBc z)yKsZ*fdgs-ca#ncZse=rVWnMV%%YI9;0ld>Ie-i>dP9=cK&T7FbeNSCdipqDlDMDr&BT;J3CFnQCdWQ28F|oWM?7%S%wLJ;75w zv?31Ab$OuoMbkRcx2wT$^M#CCr-P9x80I1vOYp>3WOG2Z$*guNg@LAU<_o}{CvS5! zxSS^P0M+ikms|36GQF>=5`^M6>K?^KsylKB_ygb_`5c=^3Yuo0O{VOeo%=o_MR1w( zj5!Cf(q-mWKWeJsVa^896@DB7Qn7?G+*|ABv}RM$6pH|Z(b{`1QMH0$!WrXu#;W`Y zhMCic?zaKj+M5v)O{6uubANVpYlb0BsHv?qcgXf9Eu*O_cd2c~d*C!m$X_Jm1IVL_ z`vp@q*4NsV8%-3ZA#W<)=t(MrSs4f_?aSjfR==D zpw_>3+{}z5HCWKUkq0(-jtI3*bK$L70nA-J+!SBvK2J9q97v=Xc@tgZ^VQn~Ggx8P zIiUWs!2|av%Trh?FiSBvceC+dQvfI-f9%5D+LdlX^N(i1L|uf6#29zG!$yW#~P2R_L+ z|E)HxP z$V39iHmQx(bs|e?3Wq6G;NF$#J@UHP02Ry!)Eu;)gg14g0c-U6hyFlKN{v^Ri3Wd= z)+-scc28+K1SdI3Cww@JjSE~XpNP$oD~ehsI%wx>VyYC#H8!qwp&s}+sCWrb461b5 zIYW4b)fKeNW97-+!Q_4==+M-`W6!T{fhWwpkibD zJ>jy?_k=ZJG-Qr{Atw#M`p2#7%%#9wT3ew0P#*Vw`GQ0lqI zwMXh>@)2mD67FECKTEB{GAcnoRTr5no6=@4+`+k;y+{e9*6=FMm%B86B1#jzq+kNz zF5M6ZYF$?h^}yLsVTw7@PGW>0O^5R8*}lq0Hg*`fT6cgPvkgA;H?Gk1ICKvY<%f5r zgsG_TNF`6Av*Hpuh<)fa&60q^{o?n-2s!^gE@0XK6$1uSouJx$nA`Q$W#S9{2aR}J z5g~p$(Veu3B-T_{2gbsJ%lq$~`dOcjL3FYdsA~82zb#YWPc)Mh5Q-}3S*9*NK5S_^ zKOW+u8DrHbjACWMDX)VBlLOw=bcF%V)Bc7*!+Mez@>b193o}RPtPw5da87S}Fo2mJ zt_Dt}7O;vwE_(CtHzWXFYzmDoX6`{#@{$AAJ!-`o?$uyb4l||S_+HRZ->7$S3;4fugz^WxZ`{9#K+K?yca@ zTK#+YW)ZuKKzhH^Vc5e))tzoM%MHEw*rf;Ug)tC!-1#$_hy@d0 z-Wa6h3s`pCjT#z+$yqrvpFy!=Y?nh8W{@Q!k=?bu|F`NoYl9=N!X_xVZT+x|P^+~A z2vYQ3r`$&wv105P{l5f2F_*`CK@SG=X}&(4VXzK15Cj6MI|x!!K&8jTR2Mp|Rvax%K=5R{= zr}AU6?z#@zO6TgFsi~=~EY=g3o;P==Xs6igobh?<{l!C(gMFqzCpVI{{c2|aW@M0G z?OhFIwsuztRwz`MJCTs4-JztUqkDP0IbqqN@Sjg>fu6b>NPQ%En0g6a{5|J?MB53p z)A^!OP?+a`iu3Up+N?~iqO6*yXdN*0c1c$1)fF;xdSO*CSTJ}SVQNQlG`hfuvOT8l zy!rAt;DQH@wc&r~&4Ieg%z%?R^*4e*so5OhCMHa%!A@PK%Y=}r(E5H#IItOy@`TaX z^d_bN@4jY`=@UAzfTPs6e#^zg*Hz5C6gRt6u=Fu`MWLi6 zD;``n3BR`5Txg2D8D(V*ynD|LFrV*(Np;6GkOB8<0R=aSF$`63K0``J((qNHCP_U@ zweoh-L3>kU8Lbnr1wuuM2zVAtv@?tmEx(#zmK~_b4u~`)#wuZywb3T%cd?kpV}e(Y zTvjft%P!>f8hbj7S3g#;<&vRu! z3krUMZ;%Akc$f?PduyDD+w5K2I-uKVC*e|lo>irI&EA@);Vblj%IS$x1|~t3U?g%( z$RLeeIF(&Ah_CCZmi%=BZ0hFIAIT4ZmnJsYUS^ zR}*twlB)FOgSlt-bF~PQqm2+o9v6rvNl!(bQB;|7=`s{p-v6oz z_h^LkUxfQhu9^p9rlRlZPG!k2_9^x?s4S53Vao@IufMpIpq*8(qRb+YvBu)Oyb52_ zfAL;tk78yeBN)h>MOmb=iU>p%ePq z;*&^YcXsp1c?*#g1P55m4xgeIKf3lf2l$@W)(^FxQth_kn?70E&<8B%zap)x)h>k- zi4&z0-Hco*@D;PH@f;4RRoX{)XCL00!7)YGjYM{JcP}*4z1sN2_V4N!!K9HSQ~@t? z3c88gRviu!?!W~wM#o{9{bhvCzsiV;JsQ@jf-yB*c{lquaR3h=gW?$SJyBRJ(q7sa zBdm&<^#B26+$j&f1B_k#pS)JZFkD`yJ*8FuiE);+hP>X`!mAFp`hxi8?_XyfmJA!3 zCAL--+bum_7KXjgdRXkz`xk{^cALr&_HU%!Suc67)4t~4zQz91?jXT&_E(*+pSb-C z#h?hJDk(&I31u|J{X{Sh=bXCsw6~kAS4 zj~o(*tbgscu2x!dlfgbQwpx@v9GQI>bm4{&o#wxCyLju{lK-u8@~_bs`lI|wrqU*Y zW67-#juV;BzrgnUw138u*muS=1U&Hs9fp7U;QYJ+DFgLFtwYweR(IC>sKqvz5k1vM zJH1l6OJ$!vSkI0*|9Snb(_ohC?3>V-(<=wkpSLB$nvlN|OPj`)l|()~83VIy+j+); z4JfU?-)W0{M|1nMSm{H}-}6TDtF?h=_v@L!52^8Kr)u6(8o;c>sMAUC8MCZBnSl2} zSyT>BLamdjaUgnI>mANnA$E+pUmthc2|_wv!L>{|!roGjlL5J$Yim*b`5j#%+!rOo8`TuZe9)*Oh=2@XO!a*DUgtjqU1ubdRur9~A`+ K`7$}H;Qs{z6XqoV diff --git a/resource/version.h b/resource/version.h index bb51944c..327790e8 100644 --- a/resource/version.h +++ b/resource/version.h @@ -1,4 +1,4 @@ -#define APPLEWIN_VERSION 1,28,8,0 +#define APPLEWIN_VERSION 1,29,0,0 #define xstr(a) str(a) #define str(a) #a From b891f72a0af8eb69f09f3450f6d47b7d50330ab2 Mon Sep 17 00:00:00 2001 From: tomcw Date: Tue, 9 Jul 2019 07:49:20 +0100 Subject: [PATCH 18/21] Help: fix typo --- help/CommandLine.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/help/CommandLine.html b/help/CommandLine.html index fee1b3f2..4965a2a1 100644 --- a/help/CommandLine.html +++ b/help/CommandLine.html @@ -100,9 +100,9 @@ -rgb-card-invert-bit7
    Force the RGB card (in "Color (RGB Monitor)" video mode) to invert bit7 in MIX mode. Enables the correct rendering for Dragon Wars.

    -50hz
    - Support 50Hz(PAL) video refresh rate and implicitly PAL 1.016MHz.

    + Support 50Hz(PAL) video refresh rate and PAL 1.016MHz base CPU clock.

    -60hz
    - Support 60Hz(PAL) video refresh rate and implicitly PAL 1.020MHz (default).
    + Support 60Hz(NTSC) video refresh rate and NTSC 1.020MHz base CPU clock (default).

    Debug arguments: From 75c9669884d0362044ce14d7ec9efe846d8c38bd Mon Sep 17 00:00:00 2001 From: tomcw Date: Tue, 9 Jul 2019 22:18:36 +0100 Subject: [PATCH 19/21] Fixed 2x Windowed mode: to show correct track for drive-2 --- source/Disk.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/Disk.cpp b/source/Disk.cpp index 5b4f7f21..7a79506e 100644 --- a/source/Disk.cpp +++ b/source/Disk.cpp @@ -86,7 +86,7 @@ float Disk2InterfaceCard::GetCurrentPhase(void) { return m_floppyDrive[m_currDr int Disk2InterfaceCard::GetCurrentOffset(void) { return m_floppyDrive[m_currDrive].m_disk.m_byte; } BYTE Disk2InterfaceCard::GetCurrentLSSBitMask(void) { return m_floppyDrive[m_currDrive].m_disk.m_bitMask; } double Disk2InterfaceCard::GetCurrentExtraCycles(void) { return m_floppyDrive[m_currDrive].m_disk.m_extraCycles; } -int Disk2InterfaceCard::GetTrack(const int drive) { return ImagePhaseToTrack(m_floppyDrive[drive].m_disk.m_imagehandle, m_floppyDrive[m_currDrive].m_phasePrecise, false); } +int Disk2InterfaceCard::GetTrack(const int drive) { return ImagePhaseToTrack(m_floppyDrive[drive].m_disk.m_imagehandle, m_floppyDrive[drive].m_phasePrecise, false); } std::string Disk2InterfaceCard::GetCurrentTrackString(void) { From f073153c64bf8f91f362e1ca31b0cd75bd0d6f2e Mon Sep 17 00:00:00 2001 From: Nick Westgate Date: Sat, 13 Jul 2019 11:53:13 +1200 Subject: [PATCH 20/21] Add Windows Universal CRT SDK to VS 2017 build instructions I found I needed this to fix errors on standard headers as per: https://stackoverflow.com/questions/42777424/visual-studio-2017-errors-on-standard-headers --- docs/compiling.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/compiling.txt b/docs/compiling.txt index 0dbc3150..ed4bf3cf 100644 --- a/docs/compiling.txt +++ b/docs/compiling.txt @@ -27,6 +27,7 @@ MSVC 2017 Community * [x] Graphics debugger and GPU profiler for DirectX * [x] Static analysis tools * [x] VC++ 2017 v141 toolset (x86,x64) + * [x] Windows Universal CRT SDK * [x] Visual Studio C++ core features * [x] Windows 8.1 SDK * [x] Windows 10 SDK (10.0.15063.0) for Desktop C++ x86 and x64 From c03eb54103f9535120805de6e2175b6a07f6b720 Mon Sep 17 00:00:00 2001 From: tomcw Date: Mon, 22 Jul 2019 19:32:25 +0100 Subject: [PATCH 21/21] WOZ: Extended latch delay for 'Wizardry III' and 'Space Quest I' copy-protection (#662, #669) --- source/Disk.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/source/Disk.cpp b/source/Disk.cpp index 7a79506e..4ac08a95 100644 --- a/source/Disk.cpp +++ b/source/Disk.cpp @@ -1151,8 +1151,7 @@ void __stdcall Disk2InterfaceCard::ReadWriteWOZ(WORD pc, WORD addr, BYTE bWrite, } else // m_shiftReg==0 { - if (m_latchDelay == 0) - m_latchDelay = 4; // extend for another 4us + m_latchDelay += 4; // extend by 4us (so 7us again) - GH#662 m_dbgLatchDelayedCnt++; #if LOG_DISK_NIBBLES_READ